共用方式為


Lightweight webhosted services with ASP.NET Web API

The code for this post can be downloaded in the MSDN Code Gallery.

A couple of weeks back, Youssef Moussaoui posted about a way to create a lightweight service using the ASP.NET Web APIs. His code was built using the self-hosted support for the APIs, and with that we could write a service as simple as the code below.

  1. LiteWebServer server = new LiteWebServer("https://localhost");
  2. server.Get("/Hello",
  3.     (r) => new HttpResponseMessage() {
  4.         Content = new StringContent("Hello World!")
  5.     });
  6. server.Post("/Echo",
  7.     (r) => new HttpResponseMessage() {
  8.         Content = new StringContent(r.Content.ReadAsStringAsync().Result)
  9.     });
  10. server.Open();

Last week, a user asked a question in the forums which seemed similar to that – they wanted to remove the HttpControllerDispatcher from the web-hosted APIs, so that they could have a low-level message handler dealing with the messages from the client. I don’t know if that’s exactly what they wanted, but I decided to try to do it with what we have.

First of all, a little parenthesis: I’ve worked with services and WCF for many years, but the world of routing, HTTP modules and such is still new to me. This is one solution for this scenario, but certainly there are others, and likely some will be better. But if you have any feedback and feel that this scenario is important, please don’t hesitate in leaving a comment, or opening (or voting for) an issue in the User Voice forum for Web APIs.

For this example, I’ll start with an empty web application. If the goal is to have a lightweight server, we’ll skip all of the extra stuff which is added by the MVC templates. Then I added a reference to the NuGet package AspNetWebApi, which brings all the references needed by the webhosted web APIs. Then we can add a global application file (global.asax), which is where we’ll setup the lightweight server.

When trying to port Youssef’s solution to run webhosted, I tried to make it as similar to his as possible, such that his code could work (almost) without any modifications – see below. There are two main differences here (and a small one in which I made the Get/Post/etc. methods to return “this” so I could write it in a fluent way): instead of the parameter for the server constructor be the base address, in the webhosted case we pass the configuration object (the base address is given by the host); and instead of opening the server, I add a route which maps to all requests. This last step is important, because prior to the request getting to the Web API pipeline, a route must exist to dispatch it to it. Also, instead of the code living in a Main method, it lives in the Application_Start handler of the global.asax file. That’s pretty much all that is on that file.

  1. protected void Application_Start(object sender, EventArgs e)
  2. {
  3.     LiteWebServer server = new LiteWebServer(GlobalConfiguration.Configuration)
  4.         .Get("/Hello", (r) => new HttpResponseMessage() { Content = new StringContent("Hello World!") })
  5.         .Get("/", (r) => new HttpResponseMessage() { Content = new StringContent("<h1>Default Page</h1>", Encoding.UTF8, "text/html") })
  6.         .Post("/Echo", (r) => new HttpResponseMessage() { Content = new StringContent(r.Content.ReadAsStringAsync().Result) });
  7.  
  8.     GlobalConfiguration.Configuration.Routes.MapHttpRoute("all", "{*all}");
  9. }

Now for the LiteWebServer class. It’s really similar to the one in the original post, with a few differences. First, as I mentioned before, all operations to add handlers return “this” – more as a convenience than any behavioral change, and the code could live perfectly without it. Second, instead of creating the configuration object (and the HttpServer) itself, the class is passed the configuration of the hosted service. Finally, when a handler is being registered, that’s when we’ll add our message handler to the list of handlers in the configuration (with a static flag to make sure that we only add it once). Another minor difference is that we strip the leading ‘/’ character, since what comes from the routing parameters doesn’t have it, so we don’t have to deal with it when trying to match the incoming requests.

  1. public class LiteWebServer
  2. {
  3.     private static bool registeredInGlobalConfig = false;
  4.     LiteWebMessageHandler messageHandler = new LiteWebMessageHandler();
  5.     HttpConfiguration configuration;
  6.  
  7.     public LiteWebServer(HttpConfiguration configuration)
  8.     {
  9.         this.configuration = configuration;
  10.     }
  11.  
  12.     public LiteWebServer Get(string subPath, LiteMessageHandler handler)
  13.     {
  14.         RegisterHandler(HttpMethod.Get, subPath, handler);
  15.         return this;
  16.     }
  17.  
  18.     public LiteWebServer Put(string subPath, LiteMessageHandler handler)
  19.     {
  20.         RegisterHandler(HttpMethod.Put, subPath, handler);
  21.         return this;
  22.     }
  23.  
  24.     public LiteWebServer Post(string subPath, LiteMessageHandler handler)
  25.     {
  26.         RegisterHandler(HttpMethod.Post, subPath, handler);
  27.         return this;
  28.     }
  29.  
  30.     public LiteWebServer Delete(string subPath, LiteMessageHandler handler)
  31.     {
  32.         RegisterHandler(HttpMethod.Delete, subPath, handler);
  33.         return this;
  34.     }
  35.  
  36.     private void RegisterHandler(HttpMethod method, string subPath, LiteMessageHandler handler)
  37.     {
  38.         if (!registeredInGlobalConfig)
  39.         {
  40.             this.configuration.MessageHandlers.Insert(0, this.messageHandler);
  41.             registeredInGlobalConfig = true;
  42.         }
  43.  
  44.         if (subPath.StartsWith("/"))
  45.         {
  46.             subPath = subPath.Substring(1);
  47.         }
  48.  
  49.         messageHandler.Handlers[method].Add(subPath, handler);
  50.     }
  51. }

The message handler is also very similar to the original post, with the only difference in the SendAsync method. In this project, we first try to get the route data (which should contain all the “all” parameter we used to catch all requests, then fetch that value. Then we use that value to try to find the handler (instead of the absolute URI of the request), since the route data only gives us the relative path. If the route or the handler wasn’t found, the method returns a response with a 404 (Not Found) response.

  1. protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  2. {
  3.     HttpResponseMessage response = null;
  4.     IHttpRouteData routeData;
  5.     if (request.Properties.TryGetValue<IHttpRouteData>(HttpPropertyKeys.HttpRouteDataKey, out routeData))
  6.     {
  7.         string routeValue = (routeData.Values["all"] as string) ?? "";
  8.         LiteMessageHandler handler;
  9.         RoutingMap routingMap = Handlers[request.Method];
  10.         if (routingMap.TryGetValue(routeValue, out handler))
  11.         {
  12.             response = handler(request);
  13.         }
  14.     }
  15.  
  16.     if (response == null)
  17.     {
  18.         response = new HttpResponseMessage(HttpStatusCode.NotFound);
  19.     }
  20.  
  21.     return FromResult(response);
  22. }

That’s it, we can now F5 the project, and that will show us the default page (from the ‘/’ GET handler), or use fiddler to try the other handlers.

A few thoughts on “lightweight”

When I think about lightweight services I think of something similar to Node or Sinatra where you can write a hello world service with literally less than 10 lines of code (and that doesn’t mean stuffing many statements in a single, 2048-character line). I tried something similar to what Youssef had build for the self-hosted and frankly I think I had to take some shortcuts to make it work on webhost (the “all” route, passing the global configuration to the “lite” service at the application start, etc.). I don’t think this is as good as it can get, but hopefully that’s one starting point to getting additional features to the framework to make this scenario truly lightweight.

[Code in this post]