Tuesday, June 18, 2013

OWIN with static files, exception handling and logging

As I wrote already in Self-host ASP.NET Web API and SignalR together, the OWIN configuration is done in Startup.Configuration():
public void Configuration(IAppBuilder app)
{
  // Configure WebApi
  var config = new HttpConfiguration();
  config.Routes.MapHttpRoute("API Default", "api/{controller}/{id}", new { id = RouteParameter.Optional });
  app.UseWebApi(config);

  // Configure SignalR
  app.MapHubs();
}
It looks, as if there are some features get activated with app.UseXXX() (or app.MapHubs(), respectively). But this is not completely true. When we look into the implementation of these "feature activating methods", they finally call
public IAppBuilder Use(object middleware, params object[] args)
{
  this._middleware.Add(AppBuilder.ToMiddlewareFactory(middleware, args));
  return (IAppBuilder) this;
}
This method adds the features to a list. During the request processing, every feature in this list will be checked, if it can handle the request. Therefore the order of the "feature activating methods" can be important. Not in the example above, since WebApi and SignalR do not compete for the same requests.

Microsoft.Owin.Diagnostics

The package Microsoft.Owin.Diagnostics contains one useful feature catching and displaying exceptions. For sure this shouldn't happen, but when it would be nice to know about. But as always you should consider to use it only during development.

To switch it on, simply add the following line at the beginning of Startup.Configuration():
app.UseShowExceptions(); 

Another feature in Microsoft.Owin.Diagnostics is
app.UseTestPage(); 
This displays the message Welcome to Katana to the client. You should place it at the very end of Configuration.Startup(). Otherwise your other features wouldn't never reached.

But anyway, this feature is useful only in hello world status. Later I would prefer to get HTTP 404 instead of this message.

Microsoft.Owin.StaticFiles

Like most of the other OWIN packages, also Microsoft.Owin.StaticFiles is in prerelease status. But this package is special, you even cannot find it in NuGet. To install it, you need to enter the following command in the Package Manager Console:
Install-Package Microsoft.Owin.StaticFiles -Version 0.20-alpha-20220-88 -Pre 
But probably the package is hidden since it doesn't work really. It has problems when you request more than one file in parallel. The solution is to use one of the nightly Katana builds (e.g. 0.24.0-pre-20624-416). Probably this is even more alpha than the hidden version. But is works better. Obviously there was some improvement between version 0.20 and 0.24.
You can get the nightly builds from a separate feed: http://www.myget.org/f/Katana.
After adding the package, just add one line to Startup.Configuration():
app.UseStaticFiles("StaticFiles");
The parameter specifies, in which directory the static files will be searched. Since I named it StaticFiles, you have to add a folder with this name to your project. And for every file you add to this folder you have to set in its properties that it will be copied to the output directory:

When you start the project, you can fire up the browser and enter http://localhost:8080/test.htm (without specifying the StaticFiles folder), and you get simply the page back.

Logging OWIN requests

Sometimes it would be interesting to see the incoming requests in a trace. This can be achieved with a custom feature. The constructor is quite simple. It just stores the reference to the next feature:
private readonly Func<IDictionary<string, object>, Task> _next; 

public Logger(Func<IDictionary<string, object>, Task> next)
{
  if (next == null)
    throw new ArgumentNullException("next");
  _next = next;
}
The implementation isn't really complicated, too:
public Task Invoke(IDictionary<string, object> environment)
{
  string method = GetValueFromEnvironment(environment, OwinConstants.RequestMethod);
  string path = GetValueFromEnvironment(environment, OwinConstants.RequestPath);

  Console.WriteLine("Entry\t{0}\t{1}", method, path);

  Stopwatch stopWatch = Stopwatch.StartNew();
  return _next(environment).ContinueWith(t =>
  {
    Console.WriteLine("Exit\t{0}\t{1}\t{2}\t{3}\t{4}", method, path, stopWatch.ElapsedMilliseconds,
      GetValueFromEnvironment(environment, OwinConstants.ResponseStatusCode), 
      GetValueFromEnvironment(environment, OwinConstants.ResponseReasonPhrase));
    return t;
  });
}
First, it prints some data of the current request. The more interesting part is, that it then calls the succeeding features (return _next(environment)). And when the succeeding features were evaluated, it finally (ContinueWith) prints some response data. I added here also method and path, since otherwise it were difficult to find the corresponding entries. Maybe it would be even better to use some kind of unique id for this purpose. But in my projects, method and path are enough.
GetValueFromEnvironment is only a little helper, since in some cases the environment dictionary does not contains all values:
private static string GetValueFromEnvironment(IDictionary<string, object> environment, string key)
{
  object value;
  environment.TryGetValue(key, out value);
  return Convert.ToString(value, CultureInfo.InvariantCulture);
}
Since the Logger traces the beginning and the end of the processing, it should be activated quite at the beginning of Startup.Configuration():
app.Use(typeof(Logger));

Logging the Request Body

With POST requests, it can be very handy to log also the request body. For this, you have only to extend the Invoke method a little bit:
string requestBody;
Stream stream = (Stream)environment[OwinConstants.RequestBody];
using (StreamReader sr = new StreamReader(stream))
{
  requestBody = sr.ReadToEnd();
}
environment[OwinConstants.RequestBody] = new MemoryStream(Encoding.UTF8.GetBytes(requestBody));
The access to the request body is provided by a Stream. The only caveat with this stream is that it is not seekable. That means you can read it only once. And maybe the simple logging will not be enough for your requirements. Sometimes you will also process it afterwards...
Fortunately, this is no big issue: just replace the old stream with a new MemoryStream. For sure, this is not good idea with big request bodies. In such a case you will need a more sophisticated solution. But normally, it should be good enough. Moreover, you can use it only during development. In production you can disable it by configuration, par example.

Logging SignalR

With the Logger from above, you get a nice logging of the several SignalR requests (when it uses LongPolling instead of WebSockets):
10:51:18,181     11     Entry     GET     /signalr/negotiate
10:51:18,263     8      Exit      GET     /signalr/negotiate     82              
10:51:18,271     11     Entry     GET     /signalr/ping
10:51:18,275     8      Exit      GET     /signalr/ping          4               
10:51:18,540     11     Entry     GET     /signalr/connect
10:53:08,806     14     Exit      GET     /signalr/connect       110260          
10:53:08,826     11     Entry     GET     /signalr/poll
10:54:58,960     15     Exit      GET     /signalr/poll          110128          
10:54:58,969     11     Entry     GET     /signalr/poll
10:56:49,136     12     Exit      GET     /signalr/poll          110161          
The used Logger prints also timestamp and thread id (in the first 2 columns). It is easily to see, how SignalR waits for 110 seconds for an answer from the server (a notification). When it doesn't get one, it simply sends the next request.
You can also see that all requests (at least in this example) are handled by the same thread (11). But the response is created by other threads (8, 14, 15 and 12).

Summary

It is quite easy to add additional features to an OWIN host. And it is also not too hard to implement own features. Drawbacks of the whole stuff are (hopefully only at the moment):
  • the beta status of some packages
  • the lack of documentation
But for a real developer, the best documentation is the code, anyway.

You can find the source code at GitHub: https://github.com/Ritzlgrmft/OwinConfiguration.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.