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;
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 and on
Mastodon.