blog post image
Andrew Lock avatar

Andrew Lock

~6 min read

Using action results and content negotiation with "route-to-code" APIs

In this post post I show that you can combine some techniques from MVC controllers with the new "route-to-code" approach. I show how you can use MVC's automatic content negotiation feature to return XML from a route-to-code endpoint.

What is route-to-code?

"Route-to-code" is a term that's been used by the ASP.NET Core team for the approach of using the endpoint routing feature introduced in ASP.NET Core 3.0 to create simple APIs.

In contrast to the traditional ASP.NET Core approach of creating Web API/MVC controllers, route-to-code is a simpler approach, with fewer features, that puts you closer to the "metal" of a request.

For example, the default Web API template includes a WeatherForecastController looks something like the following:

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly ILogger<WeatherForecastController> _logger;
    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Empty<WeatherForecast>(); // this normally returns a value.
    }
}

You can see various features of the MVC framework, even in this very basic example:

  • The controller is a separate class which (theoretically) can be unit tested.
  • The route that the Get() endpoint is associated with is inferred using [Route] attributes from the name of the controller.
  • The controller can use Dependency Injection.
  • The [ApiController] attribute applies additional cross-cutting behaviours to the API using the MVC filter pipeline.
  • It's not shown here, but you can use model binding it automatically extract values from the request's body/headers/URL.
  • Returning a C# object from the Get() action method serializes the value using content negotiation.

In contrast, route-to-code endpoints are added directly to your middleware pipeline in Startup.Configure(). For example, we could create a similar endpoint to the previous example using the following:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            // create a basic endpoint
            endpoints.MapGet("weather", async (HttpContext context) =>
            {
                var forecast = new WeatherForecast
                {
                    Date = DateTime.UtcNow,
                    TemperatureC = 23,
                    Summary = "Warm"
                };

                await context.Response.WriteAsJsonAsync(forecast);
            });
        });
    }
}

This endpoint is similar to the API controller, but it's much more basic:

  • Routing is very explicit. There's no conventions, the endpoint responds to the /weather path and that's it.
  • There's no filter pipeline, constructor DI, or model binding.
  • The response is serialized to JSON using System.Text.Json. There's no content negotiation or serializing to other formats. If you need that functionality, you'd have to implement it manually yourself.

If you're building simple APIs, then this may well be good enough for you. There's far less infrastructure required for route-to-code, which should make them more performant than MVC APIs. As part of that there's generally just less complexity. If that appeals to you, then route-to-code may be a good option.

Filip has a great post on how using new C#9 features in combination with route-to-code to build surprisingly complex APIs with very little code. There's also a great introduction video to route-to-code by Cecil Phillip and Ryan Nowak here.

So route-to-code could be a great option where you want to build something simple or performant. But what if you want the middle ground? What if you want to use some of the MVC features.

Adding content negotiation to route-to-code

One of the useful features of the MVC/Web API framework is that it does content negotiation. Content negotiation is where the framework looks at the the Accept header sent with a request, to see what content type should be returned.

For most APIs these days, the Accept header will likely include JSON, hence why the "just return JSON" approach will generally work well for route-to-code APIs. But what if, for example, the Accept header requires XML?

In MVC, that's easy. As long as you register the XML formatters in Startup.ConfigureServices(), the MVC framework will automatically format requests for XML as XML.

Route-to-code doesn't have any built-in content negotiation. so you have to handle that yourself. In this case, that's probably not too hard, but it's extra boilerplate you must write yourself. If you don't write that code, the endpoint will just return JSON no matter what you request

Image of the API returning JSON when XML was requested

I was interested to find that we have a third option: cheat, and use an MVC ActionResult to perform the content negotiation.

The example below is very similar to the route-to-code sample from before, but instead of directly serializing to JSON, we create an ObjectResult instead. We then execute this result, by providing an artificial ActionContext.

public void Configure(IApplicationBuilder app)
{
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("weather", async (context) =>
        {
            var forecast = new WeatherForecast
            {
                Date = DateTime.UtcNow,
                TemperatureC = 23,
                Summary = "Warm"
            };

            var result = new ObjectResult(forecast);
            var actionContext = new ActionContext { HttpContext = context };
            
            await result.ExecuteResultAsync(actionContext);
        });
    });
}

This code is kind of a hybrid. It uses route-to-code to execute the endpoint, but then it ducks back into the MVC world to actually generate a response. That means it uses the MVC implementation of content-negotiation!

In order to use those MVC primitives, we need to add some MVC services to the DI container. We can use the AddMvcCore() extension method to add the services we need, and can add the XML formatters to support our use case:

public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
      services.AddMvcCore() 
          .AddXmlSerializerFormatters();
  }

  public void Configure(IApplicationBuilder app)
  {
    // as above
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("weather", async (context) =>
        {
            var forecast = new WeatherForecast
            {
                Date = DateTime.UtcNow,
                TemperatureC = 23,
                Summary = "Warm"
            };

            var result = new ObjectResult(forecast);
            var actionContext = new ActionContext { HttpContext = context };
            
            await result.ExecuteResultAsync(actionContext);
        });
    });
  }
}

With this in place, we now have MVC-style content negotiation-when the Accept header is set to application/xml, we get XML, without having to do any further work in our endpoint:

Image of a request for XML returning XML

Be aware that there are many subtleties to MVC's content-negotiation algotithm, such as ignoring the */* value, formatting null as a 204, and using text/plain for string results, as laid out in the documentation.

So now that I've shown that you can do this, should you?

Should you do this?

To prefix this section I'll just say that I've not actually used this approach in anything more than a sample project. I'm not convinced it's very useful at this stage, but it's interesting to see that it's possible!

One of the problems is that calling AddMvcCore(), adds a vast swathe of services to the DI container. This isn't a problem in-and-of itself, but it will slow down your app's startup slightly, and use more memory etc, as you actually register most of the MVC infrastructure. That makes your "lightweight" rout-to-code app a little bit "heavier".

On the plus side, the actual execution of your endpoint stays lightweight. There's still no filters or model binding in use, it's only the final ActionResult execution which uses the MVC infrastructure. And as we're executing the ActionResult directly, there's not many additional layers here either, so that's good.

Actually executing the ActionResult is a bit messy of course, as you need to instantiate the ActionContext. There might be other implications I'm missing here too - you might need to pass in RouteData for example if you're trying to execute a RedirectResult, or some other ActionResult that's more closely tied to the MVC infrastructure.

This is just a proof of concept, but I wonder if it's the sort of thing that we'll see supported more concretely as part of Project Houdini. Exposing the content negotiation capability to route-to-code APIs seems like a very useful possibility, and shouldn't really be tied to MVC. Let's hope 🤞

Summary

In this short post I showed that you can execute MVC ActionResults from a route-to-code API endpoint. This allows you to, for example, use automatic content negotiation to serialize to JSON or XML, as requested in the Accept header. The downside to this approach is that it required registering a lot of MVC services, is generally a bit clunky, and isn't technically supported. While I haven't used this approach myself, it could be useful if you have a limited set of requirements and don't want to use the full MVC framework.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?