Elegant way of producing HTTP responses in ASP.NET Core outside of MVC controllers

· 1704 words · 8 minutes to read

ASP.NET Core 2.1 introduced support for a little (or, should I say, not at all) documented feature called IActionResultExecutor. It allows us to use some of the action results -those that we are used to from MVC controllers - outside of the controller context, so for example from a middleware component.

Kristian has a great blog post about result executors, that I recommend you check out. From my side, I wanted to show you today a set of extension methods that were recently introduced into WebApiContrib.Core that make working with IActionResultExecutor and in general authoring HTTP endpoints outside of controllers even easier.

Controller helper methods 🔗

The most important of the ActionResult family is the ObjectResult - which internally handles content negotiation - so determines what media type is suitable for the response, and serializes the response accordingly using the selected formatter (JSON, XML, Protobuf or whatever you support). The typical controller, using an ObjectResult might look like this:

[HttpGet("contacts")]  
public IActionResult Get()  
{  
var contacts = new[]  
{  
new Contact { Name = "Filip", City = "Zurich" },  
new Contact { Name = "Not Filip", City = "Not Zurich" }  
};

// will do content negotiation  
return new ObjectResult(contacts);  
}

We could also return a POCO directly from the action, but the framework would still end up using ObjectResult to process that, so ultimately it is still the same thing.

[HttpGet("contacts")]  
public IEnumerable<Contact> Get()  
{  
var contacts = new[]  
{  
new Contact { Name = "Filip", City = "Zurich" },  
new Contact { Name = "Not Filip", City = "Not Zurich" }  
};

// will do content negotiation  
return contacts;  
}

Finally, and this is what this post is about, you could replace our usage of ObjectResult, or the POCO, with a call to Ok() on the base controller:

[HttpGet("contacts")]  
public IActionResult Get()  
{  
var contacts = new[]  
{  
new Contact { Name = "Filip", City = "Zurich" },  
new Contact { Name = "Not Filip", City = "Not Zurich" }  
};

// will do content negotiation  
return Ok(contacts);  
}

This is really still the same as before, because Ok() actually uses ObjectResult under the hood, it’s just expressed in a slightly different way. Now, if you have done any work with MVC controllers, I am sure you are used to those helper methods that are there on the the framework’s ControllerBase, like our Ok() or other - for example Unauthorized() or File(), to name just two of them.

They come in many, many variants (the base controller is 2700+ lines of code…), and are used as shortcuts into the many ActionResults that the framework offers. You can find a full list here. From my experience with various ASP.NET Core MVC projects, I would say that these helper methods are extremely popular among developers - they make the code very concise. They also hide certain complexity level of dealing with producing the HTTP responses, yet still make it very obvious to understand what is going on.

Controller feeling, without a controller 🔗

What we recently introduced into WebApiContrib.Core is a similar set of methods as found on the base controller, but ported as extension methods on top of HttpContext. This is done as a combination of IActionResultExecutor features and “manual” response creation. Meaning we either mimic the behavior of the method from base controller by manually crafting the HTTP response, setting headers, status codes and so on, or we really reach into the IActionResultExecutor infrastructure and invoke the relevant action result from the MVC framework.

The end result is very neat, and you get very similar helper method set that you can now enjoy from your non-controller code - primarily middleware but potentially some other places too.

So imagine you are creating a “lightweight” HTTP endpoint using the new Endpoint Routing feature. You can now use the new helper methods to produce the response. Here is a full sample application setup:

public static async Task Main(string[] args) =>  
await WebHost.CreateDefaultBuilder(args)  
.ConfigureServices(s =>  
{  
s.AddRouting();

// necessary to wire in ActionResults  
// and content negotiation  
// you can manually register other formatters here  
// for example Messagepack or Protobuf  
s.AddMvc();

// note: due to the current state of ASP.NET Core 3.0 (preview3)  
// you need to manually call: s.AddMvc().AddNewtonsoftJson()  
// to use JSON formatter. This will be fixed in the future in the framework  
})  
.Configure(app =>  
{  
app.UseRouting(r =>  
{  
r.MapGet("contacts", async context =>  
{  
var contacts = new[]  
{  
new Contact { Name = "Filip", City = "Zurich" },  
new Contact { Name = "No Filip", City = "Not Zurich" }  
};

// from WebApiContrib.Core  
// will do content negotation  
await context.Ok(contacts);  
});  
});  
}).Build().RunAsync();

In order to make this work, the only thing that is needed, is that it’s necessary to have a call to services.AddMvc() in the DI container setup, as the action result and the executor infrastructure is bootstrapped there.

Other than that, this Ok() extension method on HttpContext will behave exactly the same as the Ok() on base controller - including performing the full content negotiation. The example uses endpoint routing from ASP.NET Core 3.0, but it would work from any place in ASP.NET Core request processing pipeline, for example with an IRouter in ASP.NET Core 2.1 or any middleware.

This is an extremely concise approach, and a very elegant way of building lightweight HTTP APIs. We have actually already talked about that in one of my older blog posts, and this approach very nicely extends those older examples.

Another example we could quickly look at here, is returning a file stream - instead of dealing with it manually, including all the complexity of async reading of the stream or stuff related to Content-Disposition headers and so on, we can simply use an extension method now:

app.UseRouting(r =>  
{  
r.MapGet("download", async context =>  
{  
// some file path  
var path = Path.GetFullPath(Path.Combine("files", "myfile.pdf"));

// from WebApiContrib.Core  
await context.PhysicalFile(path, "application/pdf");  
});  
});

In this particular case, the helper method would end up using an action result, a PhysicalFileActionResult, which will take care of reading the file in a non-blocking way and make sure all HTTP response details are correctly handled. And just like before, PhysicalFile() mimics a corresponding method from the MVC base controller.

The full list of the available extension methods can be found below. And I really encourage you to try them - they are part of WebApiContrib.Core 2.2.0.

Task Accepted(this HttpContext c, Uri uri, object value);  
Task Accepted(this HttpContext c, string uri, object value);  
Task Accepted(this HttpContext c, string uri);  
Task Accepted(this HttpContext c, Uri uri);  
Task Accepted(this HttpContext c, object value);  
Task Accepted(this HttpContext c);  
Task BadRequest(this HttpContext c);  
Task BadRequest(this HttpContext c, object error);  
Task BadRequest(this HttpContext c, ModelStateDictionary modelState);  
Task Conflict(this HttpContext c, object error);  
Task Conflict(this HttpContext c, ModelStateDictionary modelState);  
Task Conflict(this HttpContext c);  
Task Content(this HttpContext c, string content, MediaTypeHeaderValue contentType);  
Task Content(this HttpContext c, string content, string contentType, Encoding contentEncoding);  
Task Content(this HttpContext c, string content, string contentType);  
Task Content(this HttpContext c, string content);  
Task Created(this HttpContext c, string uri, object value);  
Task Created(this HttpContext c, Uri uri, object value);  
Task File(this HttpContext c, string virtualPath, string contentType);  
Task File(this HttpContext c, Stream fileStream, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing);  
Task File(this HttpContext c, Stream fileStream, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag);  
Task File(this HttpContext c, Stream fileStream, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing);  
Task File(this HttpContext c, string virtualPath, string contentType, string fileDownloadName);  
Task File(this HttpContext c, string virtualPath, string contentType, string fileDownloadName, bool enableRangeProcessing);  
Task File(this HttpContext c, string virtualPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag);  
Task File(this HttpContext c, string virtualPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing);  
Task File(this HttpContext c, Stream fileStream, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag);  
Task File(this HttpContext c, string virtualPath, string contentType, bool enableRangeProcessing);  
Task File(this HttpContext c, Stream fileStream, string contentType, string fileDownloadName, bool enableRangeProcessing);  
Task File(this HttpContext c, byte[] fileContents, string contentType, string fileDownloadName);  
Task File(this HttpContext c, Stream fileStream, string contentType, bool enableRangeProcessing);  
Task File(this HttpContext c, Stream fileStream, string contentType);  
Task File(this HttpContext c, byte[] fileContents, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing);  
Task File(this HttpContext c, byte[] fileContents, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag);  
Task File(this HttpContext c, byte[] fileContents, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing);  
Task File(this HttpContext c, byte[] fileContents, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag);  
Task File(this HttpContext c, byte[] fileContents, string contentType, string fileDownloadName, bool enableRangeProcessing);  
Task File(this HttpContext c, string virtualPath, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag);  
Task File(this HttpContext c, byte[] fileContents, string contentType, bool enableRangeProcessing);  
Task File(this HttpContext c, byte[] fileContents, string contentType);  
Task File(this HttpContext c, Stream fileStream, string contentType, string fileDownloadName);  
Task File(this HttpContext c, string virtualPath, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing);  
Task Forbid(this HttpContext c);  
Task LocalRedirect(this HttpContext c, string localUrl);  
Task LocalRedirectPermanent(this HttpContext c, string localUrl);  
Task LocalRedirectPermanentPreserveMethod(this HttpContext c, string localUrl);  
Task LocalRedirectPreserveMethod(this HttpContext c, string localUrl);  
Task NoContent(this HttpContext c);  
Task NotFound(this HttpContext c, object value);  
Task NotFound(this HttpContext c);  
Task Ok(this HttpContext c);  
Task Ok(this HttpContext c, object value);  
Task PhysicalFile(this HttpContext c, string physicalPath, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing);  
Task PhysicalFile(this HttpContext c, string physicalPath, string contentType, string fileDownloadName, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag);  
Task PhysicalFile(this HttpContext c, string physicalPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag, bool enableRangeProcessing);  
Task PhysicalFile(this HttpContext c, string physicalPath, string contentType);  
Task PhysicalFile(this HttpContext c, string physicalPath, string contentType, DateTimeOffset? lastModified, EntityTagHeaderValue entityTag);  
Task PhysicalFile(this HttpContext c, string physicalPath, string contentType, string fileDownloadName, bool enableRangeProcessing);  
Task PhysicalFile(this HttpContext c, string physicalPath, string contentType, string fileDownloadName);  
Task PhysicalFile(this HttpContext c, string physicalPath, string contentType, bool enableRangeProcessing);  
Task Redirect(this HttpContext c, string url);  
Task RedirectPermanent(this HttpContext c, string url);  
Task RedirectPermanentPreserveMethod(this HttpContext c, string url);  
Task RedirectPreserveMethod(this HttpContext c, string url);  
Task StatusCode(this HttpContext c, int statusCode);  
Task StatusCode(this HttpContext c, int statusCode, object value);  
Task Unauthorized(this HttpContext c);  
Task Unauthorized(this HttpContext c, object value);  
Task UnprocessableEntity(this HttpContext c, object error);  
Task UnprocessableEntity(this HttpContext c, ModelStateDictionary modelState);  
Task UnprocessableEntity(this HttpContext c);  
Task ValidationProblem(this HttpContext c, ValidationProblemDetails descriptor);  
Task ValidationProblem(this HttpContext c, ModelStateDictionary modelStateDictionary);  
Task WriteActionResult<TResult>(this HttpContext c, TResult result) where TResult : IActionResult;

About


Hi! I'm Filip W., a cloud architect from Zürich 🇨🇭. I like Toronto Maple Leafs 🇨🇦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁢󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP