Skip to content

connellsharp/Controlless

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

49 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Controlless

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.

Binding

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; }
}

Handling

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);
    }
}

Responding

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;
        }
    }
}

Setup

It's just two extensions.

  • .AddControllerlessRequests() extends MVC to generate a controller for each request object with the [RouteXxx] attributes.
  • .AddControllessHandlers() registers all the IRequestHandler<> 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();
    });
}

Related articles

About

ASP.NET APIs without Controllers

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages