Using MVC result executors in ASP.NET Core middleware

I recently came across a scenario where I needed to set up a quick and dirty HTTP endpoint to serve some data with represenations for both application/json and application/xml (don't ask).

I could of course just go ahead and set up a full-blown MVC application with a single endpoint. This would allow me to use model binding for query parameters and the output formatter pipeline for content negotiation. After all, MVC ships with both JSON and XML output formatters. Easy, but it felt a bit "heavy", and this seemed like it should be the perfect scenario for a piece of lightweight middleware. A microservice with a single middleware to serve some data in different formats. Perfect!

Once I started writing the middleware, I quickly realized how tedious it is to do content negotiation manually and thought

Why isn't there a way to reuse the MVC output formatters outside of MVC? That would save me a ton of work!

Basic content negotiation

I started digging through the code base and quickly found that MVC's result executors (which handle content negotiation for ObjectResult, using output formatters) are already registered in the container. This meant that I should be able to pull them from the container and execute object results from my middleware.

This is what I ended up with:

public static class HttpContextExtensions  
{
    private static readonly RouteData EmptyRouteData = new RouteData();

    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public static Task WriteModelAsync<TModel>(this HttpContext context, TModel model)
    {
        var result = new ObjectResult(model)
        {
            DeclaredType = typeof(TModel)
        };

        var executor = context.RequestServices.GetRequiredService<ObjectResultExecutor>();

        var routeData = context.GetRouteData() ?? EmptyRouteData;
        var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

        return executor.ExecuteAsync(actionContext, result);
    }
}

Using this extension method, I can wire up a lightweight MVC setup in my Startup class:

public void ConfigureServices(IServiceCollection services)  
{
    services.AddMvcCore()
        .AddXmlDataContractSerializerFormatters()
        .AddJsonFormatters();
}

And use it in a custom (inline) middleware:

public void Configure(IApplicationBuilder app)  
{
    app.Use((ctx, next) =>
    {
        var model = new Person
        {
            FirstName = "Kristian",
            LastName = "Hellang",
        };

        return ctx.WriteModelAsync(model);
    });
}

This yields the following with Accept: application/json:

{
  "firstName": "Kristian",
  "lastName": "Hellang"
}

And the following with Accept: application/xml:

<Person>  
  <FirstName>Krisian</FirstName>
  <LastName>Hellang</LastName>
</Person>  

Lovely, right?

With just a couple of lines of code, I was able to let the user agent pick the format it wants and have MVC's output formatters do the dirty work for me. Amazing!

Now, because our front-end client is written in ClojureScript, we prefer to consume as much of the back-end data as possible using application/transit+json. We already have a Transit output formatter for MVC lying around, so if we wanted to add support for that format as well, it's just a matter of adding a call to AddTransitFormatters.

Turn it up to 11

While looking through the code, I also found a pull request from Ryan Nowak (on the ASP.NET team), adding a new interface, IActionResultExecutor<TResult>, which allows us to "turn it up to 11" and execute any IActionResult from middleware, with more or less the same amount of code:

public static class HttpContextExtensions  
{
    private static readonly RouteData EmptyRouteData = new RouteData();

    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public static Task WriteModelAsync<TModel>(this HttpContext context, TModel model)
    {
        var result = new ObjectResult(model)
        {
            DeclaredType = typeof(TModel)
        };

        return context.ExecuteResultAsync(result);
    }

    public static Task ExecuteResultAsync<TResult>(this HttpContext context, TResult result)
        where TResult : IActionResult
    {
        if (context == null) throw new ArgumentNullException(nameof(context));
        if (result == null) throw new ArgumentNullException(nameof(result));

        var executor = context.RequestServices.GetRequiredService<IActionResultExecutor<TResult>>();

        var routeData = context.GetRouteData() ?? EmptyRouteData;
        var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

        return executor.ExecuteAsync(actionContext, result);
    }
}

Notice that in the new ExecuteResultAsync method, I'm pulling IActionResultExecutor<TResult> instead of the hard coded ObjectResultExecutor service. This means we can now execute any IActionResult, like OkResult or FileResult, directly from our middleware:

public override void Configure(IApplicationBuilder app)  
{
    app.Use((ctx, next) =>
    {
        var bytes = Encoding.ASCII.GetBytes("Hello World!");

        var stream = new MemoryStream(bytes);

        var result = new FileStreamResult(stream, "text/plain")
        {
            FileDownloadName = "hello-world.txt"
        };

        return ctx.ExecuteResultAsync(result);
    });
}

The code above triggers a file download of the file hello-world.txt and the content Hello World!. Pretty cool, right?

With this building block in place, it's really easy to add other convenience methods, like

public static Task WriteJsonAsync<TModel>(this HttpContext context, TModel model)  
{
    return context.ExecuteResultAsync(new JsonResult(model));
}

Unfortunately, the IActionResultExecutor<TResult> interface won't ship until ASP.NET Core 2.1, but hopefully this gives an indication of what you'll be able to do when it ships. I hope someone finds this useful. To be honest, I think these methods should be in the ASP.NET Core code base. Maybe it's time for a pull request? 😉