Controlless is a little experiment with ASP.NET to see if we can make APIs without Controllers. Instead we could bind routes to DTOs, handle them with something like MediatR, then write the HTTP responses depending on the response type.
Just put these [RouteGet]
and [RoutePost]
attributes and your action filters straight onto your request DTOs.
[RoutePost("/films/{id}")]
[Authorize]
public class CreateFilmRequest
{
[FromRoute("id")]
public string FilmId { get; set; }
[FromBody]
public Body Body { get; set; }
public class Body
{
public string Name { get; set; }
}
}
[RouteGet("/films/{id}/actors")]
public class GetFilmActorsRequest
{
[FromRoute("id")]
public string FilmId { get; set; }
[FromQuery("page")]
public int Page { get; set; }
}
Then you register an IRequestHandler<T>
that does the work and returns a response object.
public class GetFilmActorsRequestHandler : IRequestHandler<GetFilmActorsRequest>
{
public Task<object> Handle(GetFilmActorsRequest request, CancellationToken ct)
{
// handle the request
return responseObject;
}
}
Or maybe you just register a generic handler that forwards everything to MediatR.
public class MediatorRequestHandler<TRequest> : IRequestHandler<TRequest>
{
private readonly IMediator _mediator;
public MediatorRequestHandler(IMediator mediator)
{
_mediator = mediator;
}
public async Task<object> Handle(TRequest request, CancellationToken ct)
{
return await _mediator.Send(request, ct);
}
}
You can use the [StatusCode]
attribute to respond with a different status code when a certain response type is returned.
[StatusCode(201)]
public class CreateFilmResponse
{
public string FilmId { get; set; }
[ResponseHeader]
public string Location => $"/films/{FilmId}";
}
Or if you don't have control over the type, returning from a handler is exactly the same as returning from a controller. You could return an IActionResult
for example. Or you could register a global IResultFilter
to change the status code when the type is returned.
public class ValidationFailureResultFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
if(context.Result is ObjectResult objectResult
&& objectResult.Value is List<ValidationFailure>)
{
context.HttpContext.Response.StatusCode = 400;
}
}
}
It's just two extensions.
.AddControllerlessRequests()
extends MVC to generate a controller for each request object with the[RouteXxx]
attributes..AddControllessHandlers()
registers all theIRequestHandler<>
implementations found in the assembly.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddControllerlessRequests();
services.AddControllessHandlers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}