Use IServer from ASP.NET Core to create your own web server

So here's the thing, although I'm not an expert in creating HTTP servers, I thought it would be a good idea to show how easy it can be to implement your own one, using the IServer interface from ASP.NET Core.

Say hello to the AwesomeServer

The server we will build is called AwesomeServer, a HTTP server that receives specific requests via files dropped in a folder on disk (instead of a network port) and forwards it as proper HTTP requests to the rest of the request pipeline. The returning response will also be written out to the same file.

In a previous post I briefly touched on custom servers using the IServer interface:

public interface IServer : IDisposable  
{
    IFeatureCollection Features { get; }
    void Start<TContext>(IHttpApplication<TContext> application);
}

TL;DR: For the impatient, you can find the full source code here

Here's a quick demo of the outcome:

Program.cs

Instead of using the default UseKestrel() method, we call our very own extension method UseAwesomeServer(), providing a folder path to monitor for requests:

public static void Main(string[] args)  
{
    var host = new WebHostBuilder()
        .UseAwesomeServer(opts => opts.FolderPath = "c:\process")
        .UseStartup<Startup>()
        .Build();

        host.Run();
}

What is interesting, is that when we run the application, it now listens on a folder location instead of a URL:

Hosting environment: Production  
Content root path: C:\sandbox\AwesomeServerSample\bin\Debug\netcoreapp1.1  
Now listening on: C:\process  
Application started. Press Ctrl+C to shut down.  

WebHostBuilderExtensions.cs

To keep to the Add...-Use... pattern, we create an extension method for wiring up an instance of the new server:

public static IWebHostBuilder UseAwesomeServer(this IWebHostBuilder hostBuilder, Action<AwesomeServerOptions> options)  
{
    return hostBuilder.ConfigureServices(services =>
    {
        services.Configure(options);
        services.AddSingleton<IServer, AwesomeServer>();
    });
}

That's pretty straight forward. Now let's look at the actual implementation of AwesomeServer:

AwesomeServer.cs

public class AwesomeServer : IServer  
{
    public AwesomeServer(IServiceProvider serviceProvider, IOptions<AwesomeServerOptions> options)
    {
        var serverAddressesFeature = new ServerAddressesFeature();
            serverAddressesFeature.Addresses.Add(options.Value.FolderPath);

        Features.Set<IHttpRequestFeature>(new HttpRequestFeature());
        Features.Set<IHttpResponseFeature>(new HttpResponseFeature());
        Features.Set<IServerAddressesFeature>(serverAddressesFeature);
    }

    public IFeatureCollection Features { get; } = new FeatureCollection();

    public void Dispose() { }
    public void Start<TContext>(IHttpApplication<TContext> application)
    {        
        var watcher = new AwesomeFolderWatcher<TContext>(application, Features);

        watcher.Watch();  
     }
}

In the constructor, we set IHttpRequestFeature, IHttpResponseFeature and IServerAddressesFeature. What's important to note is that the relevant address is set from the given IOptions<AwesomeServerOptions>.

Moving on to the Start<TContext>() function, we simply instantiate a new AwesomeFolderWatcher, which watches the actual folder at the given path by triggering Watch().

AwesomeFolderWatcher.cs

This class is doing the actual heavy lifting by watching a specific folder path using a FileSystemWatcher. Here's a snippet of the Watch() function:

public void Watch()  
{  
    watcher.Created += async (sender, e) =>
    {
         var context = (HostingApplication.Context)(object)application.CreateContext(features);
         context.HttpContext = new AwesomeHttpContext(features, e.FullPath);
         await application.ProcessRequestAsync((TContext)(object)context);
         context.HttpContext.Response.OnCompleted(null, null);
    };

    Task.Run(() => watcher.WaitForChanged(WatcherChangeTypes.All));
}

Let me break it down what is happening here: When the watcher sees a new file created in the folder, a context is created, then the HttpContext is set as a new AwesomeHttpContext (a special HttpContext specifically for files), then the application further processes the request with the given HttpContext and when it's done, the response OnCompleted event is called. Furthermore, we trigger the WaitForChanged function to actually start monitor the folder.

AwesomeHttpContext

This class implements the HttpContext abstraction and only sets the Request, Response and Features properties for simplicity. Here is a snippet of the constructor:

public AwesomeHttpContext(IFeatureCollection features, string path)  
{
    this.Features = features;
    this.Request = new FileHttpRequest(this, path);
    this.Response = new FileHttpResponse(this, path);
}

AwesomeHttpRequest & -Response

As the HTTP request will be sourced from a file, there needs to be a specific specification of how it should be defined. For this example, we will just use the format:

{HTTP Verb} {Relative Path}

The request is created from the first line of the file using the format specified above. Here's the constructor snippet from AwesomeHttpRequest, which implements HttpRequest:

public FileHttpRequest(HttpContext httpContext, string path)  
{
    var lines = File.ReadAllText(path).Split('\n');
    var request = lines[0].Split(' ');
    this.Method = request[0];
    this.Path = request[1];
    this.HttpContext = httpContext;
}

After the application has processed the request, the response will then be appended to the same file that the request originated from. Here is the OnCompleted() function from AwesomeHttpResponse, which implements HttpResponse:

public override void OnCompleted(Func<object, Task> callback, object state)  
{
    using (var reader = new StreamReader(Body))
    {
        Body.Position = 0;
        var text = reader.ReadToEnd();
        File.AppendAllText(path, $"\r\n\r\n--\r\n{StatusCode}\r\n{text}");
        Body.Flush();
        Body.Dispose();
    }
}

This simply writes out the relevant HTTP status and response body to the file.

In Closing

Although I cannot think of any particular use-case that this server implementation solves, but it is actually just really cool to be able to do these kinds of things in ASP.NET Core.

Be free, go implement your own IServer.

comments powered by Disqus