Business_logic

 

Admittedly, or not, we’ve all worked on projects during our careers that took the above meme’s approach of “just put it in the controller and we’ll figure it out later”. Unfortunately for some, this is a way of life due to project budget constraints, unrealistic product deadlines, and more senior developers refusing to change their ways because it “works.” It’s like how the old saying goes, you can’t teach an old programmer to decouple independent concerns.

On a recent weekend getaway to the mountains, I did what I always do on long drives when my wife inevitably falls asleep in the car: put on episode of .NET Rocks! and let Carl, Richard, and their guests fascinate me with the latest in the .NET ecosystem. On this fateful day, the guest happened to be Steve Smith talking about his relatively new project - ApiEndpoints. I’ve listened to a lot of .NET Rocks! over the years, and needless to say, a problem that has always bothered me throughout my relatively young career as a developer seemed to finally have a simple solution.

The Problem

As previously mentioned, we’ve all most likely worked on a legacy project at some point during our careers that makes the company gobbles of money with no immediate plans of being sunsetted in place of a greenfield application, leaving other poor souls to maintain the mountain of tech debt accumulated over years of ignorance. While we could go down the rabbit hole of how a project eventually gets to this near unmaintainable state, I want to focus on a single area these projects, more often than not, have in common: the fat controller.

Bloated controllers

Not to be confused with the Thomas the Tank Engine character of the same name, fat controllers are a code smell, anti-pattern, etc. (pick your favorite buzzword) that boils down to a single issue at its root - controllers that are doing way too much, violating the SRP to the fullest extent of the law.

Controller bloat, in essence, is the product of compounding controller files with a plethora of injected services and action methods that, while related by their respective domain or managed resource, have no real dependence on one another. I’m not sure about you, but I don’t think I’ve ever seen a controller action being called by another action within the same file. Sure, we might route resource requests at the API level to other methods with the same controller, but rarely is there a reason to directly call an action method explicitly from another. An unfortunate side effect of this phenomenon is a god class mentality developers take on, ignoring architectural boundaries, and injection of dependencies that service only a specific use case within said controller, ignored by 90% of the other actions.

What this eventually leads to (not in all cases, but a sizable amount), are controllers with hundreds to thousands of lines of code containing an uncomfortable amount of business logic, constructors with an unnecessary amount of injected dependencies, and a regular trip to our local pharmacy for headache medication due to the maintenance effort of these beasts.

ApiEndpoints to the rescue

Enter ApiEndpoints, a project started by Steve Smith with one goal in mind: decoupling from controller-based solutions by encouraging a package by feature vertical slice architecture from within our API project layers.

What this means, in plain english, is a mindset change from the traditional MVC patterns we see in large web API projects where there’s most likely a Controllers folder that might contain tens of hundreds (yes, seriously) controllers that act as the gateway into the lower level working parts of our application and act as the liaison for client requests. Traditionally, this sort of architecture is akin to package by layer which we see in a grand majority of projects within the enterprise, GitHub, and your friend’s sweet new app that’s going to make them millions of dollars.

What this boils down to, at the surface level, is an attempt to group related concerns and request work flows, i.e. how a request enters and trickles through the system interacting with our various application resources, within the same domain. What we’re used to seeing might be similar to the following:

\Controllers
\Models
\Views
\Services

// ...and any number of layer-based components

Our controller directory might be broken down further:

\Controllers
    HomeController.cs
    \Orders
        OrdersController.cs
        OrderProcessingController.cs
    \Products
        ProductsController.cs
        ProductInventoryController.cs
    
// ...again, any number of controllers nested within

Our Models, Views, and Services folders might very well contain the same, or very similar, structure. In this example, we’ve created a package by layer architecture within our application - though everything exists in a single DLL, these would be more often utilized and referenced as separate class libraries, JARs, etc.

What happens when a new business requirement comes in requiring a change, update, or addition to a specific feature? As you might have guessed, from our example we’ll most likely be making changes in four separate places/layers of our application, though the feature falls under a single domain. As with everything in software, your preferred package methodology will always have tradeoffs, and the tried and true, handy dandy, all encompassing answer to the question of which ideology is best is simply… it depends.

While we could dedicate an entire post about putting things where they belong and the tradeoffs of different packaging architectures, we’re focusing on just the API layer of our applications, namely everything under the Controllers folder. Our aim, with help from the ApiEndpoints library, will be to sort concerns within individual Feature folders. Specific to the API layer, a.k.a. our controllers, we want to decouple services, dependencies, and independent processes from bloated, monolithic controllers. Imagine our orders controllers containing the following actions:

OrdersController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using SomeAwesomeNamespace.Services.Orders;

namespace SomeAwesomeNamespace.Controllers.Orders
{
    [ApiController]
    [Route("[controller]")]
    public class OrdersController : ControllerBase
    {
        private readonly ILogger<OrdersController> _logger;
        private readonly OrderServiceOne _serviceOne;
        private readonly OrderServiceTwo _serviceTwo;
        private readonly OrderServiceThree _serviceThree;


        public OrdersController(
            ILogger<OrdersController> logger, 
            OrderServiceOne serviceOne, 
            OrderServiceTwo serviceTwo, 
            OrderServiceThree serviceThree)
        {
            _logger = logger;
            _serviceOne = serviceOne
            _serviceTwo = serviceTwo
            _serviceThree = serviceThree
        }

        [HttpGet]
        public ActionResult SomeActionThatUsesServiceOne()
        {
            // Do some processing requiring service one...
        }

        [HttpPost]
        public ActionResult SomeActionThatUsesServiceTwo()
        {
            // Do some processing requiring service two...
        }

        [HttpPut]
        public ActionResult SomeActionThatUsesServiceThree()
        {
            // Do some processing requiring service three...
        }

        // ...and any number of action methods to be utilized elsewhere
    }
}

Our controller contains three service-based dependencies only utilized by a single method. Our controller is now coupled to three services, independent of one another, and consumed in only a third of its methods on a per service basis. While this might be a bit of a contrived example, it’s easy to see how we might extrapolate this controller into a real world scenario, adding more services and methods that have nothing to do with one another, making it more difficult to change and modify this controller as it becomes more coupled to its injected dependencies. When the time comes to test this bad boy, it will inevitably become a mocking nightmare.

So… how can we improve upon the paved path the old guard has laid before us?

Endpoints as units of work

Continuing from our example above, let’s think about what our API routing structure might look like:

/api/orders
/api/orders/process
/api/orders/:orderId
/api/orders/:orderId/products
/api/products
/api/products/:productId
/api/products/:productId/orders

// ...and any number of routes our application might service

From the above, we could argue that based on domain, those routes probably belong in two separate controllers, product-based and order-based controllers. While that would suffice and get the job done for us, what about taking each of the above routes as an individual unit of work? Not to be confused with the design pattern of the same name, our definition of a unit of work in this context represents a processing silo in charge of one thing, and one thing only: /api/orders would be in charge of retrieving all outstanding/pending orders, /api/products/:productId, would be in charge of retrieving products given a unique identifying key, /api/orders/:orderId/products retrieves all the products on a particular order, etc. Each of these routes, while related by domain, performs a very specific task unrelated to its sibling routes with a good chance that each requires some sort of injected service that may, or may not, be utilized by the others.

While we could, again, dedicate an entire post to discuss API design semantics, let’s break away from our conventional thinking and explore building an API without traditional controllers.

Individual endpoints with ApiEndpoints

As I’m sure the fine folks reading this article would love for me to continue aimlessly writing about orders and products for a fictional company, I’ll shut up for now and finally get into some code. To start, let’s create a new web API project using your preferred project bootstrapping method. I’ll be using Visual Studio for Mac, so I’ll go ahead and select a new ASP.NET Core Web Application project using the API template, since we won’t be doing anything with views.

Once we’ve got a project ready to roll, let’s open up our solution and do a bit of immediate refactoring. Let’s start by adding a package reference to Ardalis.ApiEndpoints:

Business_logic

Once our package has been added, let’s create a Features folder at the root of our project, and immediately beneath that, a Weather directory. Let’s go ahead and create two more directories beneath our Weather folder to house our concerns that have to deal with everything related to weather in Models and Endpoints. By creating feature slices within our application, we can group things by concern rather than by layer so that every feature request coming in from the business will be easily contained within its corresponding domain. Let’s start by offering up an endpoint to retrieve a weather forecast, akin to the already existing method within the WeatherController.cs file underneath the Controllers folder. Go ahead and add a new file underneath our Endpoints folder called GetWeatherForecasts.cs, where we’ll place the action method’s code from the WeatherController’s Get() method:

GetWeatherForecasts.cs

using System;
using System.Collections.Generic;
using System.Linq;
using Ardalis.ApiEndpoints;
using DecoupledControllersWithApiEndpoints.Features.Beers;
using Microsoft.AspNetCore.Mvc;

namespace DecoupledControllersWithApiEndpoints.Features.Weather.Endpoints
{
    [Route(Routes.WeatherUri)]
    public class GetWeatherForecast : BaseEndpoint<IEnumerable<WeatherForecast>>
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        [HttpGet]
        public override ActionResult<IEnumerable<WeatherForecast>> Handle()
        {
            var rng = new Random();

            var forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();

            return Ok(forecasts);
        }
    }
}

As the method definition for Handle() is the same as the Get() action method along with the other parts I’ve directly copied over from the default WeatherController that the ASP.NET Core scaffold tools includes in its template, let’s focus on the unfamiliar parts of this file that ApiEndpoints brings to the table:

  • We’re still utilizing the [Route] and [HttpGet] attributes available to our controllers thanks to the Microsoft.AspNetCore.Mvc namespace
  • We’re inheriting from the BaseEndpoint<TResponse> class that ApiEndpoints provides for us, signaling on application startup that this is, in fact, a controller in disguise and will be treated just like a regular old ASP.NET Core controller
  • BaseEndpoint<TResponse> is an abstract class with a single method exposed for us to override in Handle() that returns an ActionResult<TResponse> type, akin to action methods from within a controller
  • If we follow the inheritance chain of BaseEndpoint, or any of its derivatives with higher order arity (thanks for the vocab upgrade in my personal arsenal, Jon Skeet) in BaseEndpoint<TResponse> or BaseEndpoint<TRequest, TResponse>, we see the base type ultimately pointing to ASP.NET Core’s ControllerBase type, solving the mystery as to why we have access to all the ASP.NET Core attributes and types in endpoints

We have a single named route thanks to the [Route(Routes.WeatherUri])] attribute, where I’ve defined Routes.cs at the root of our Features folder below:

Features/Routes.cs

namespace DecoupledControllersWithApiEndpoints.Features.Beers
{
    public static class Routes
    {
        public const string WeatherUri = "api/weather";
    }
}

While most likely unnecessary for our small demo application, I find it helpful to have a single place containing our API routes for reference in other parts of our apps, should we need them. We’ll add to this a bit later, but for now, this should suffice. I’ve also moved the WeatherForecast.cs model into the Weather/Models feature folder to keep with convention of grouping like things together.

Let’s spin up our application now using F5, or hitting a dotnet run in the terminal, and using Postman (or your favorite web request utility), let’s send a request to https://localhost:5001/api/weather and examine the response:

[
    {
        "date": "2020-09-23T12:52:27.408507-07:00",
        "temperatureC": 6,
        "temperatureF": 42,
        "summary": "Mild"
    },
    {
        "date": "2020-09-24T12:52:27.408951-07:00",
        "temperatureC": -19,
        "temperatureF": -2,
        "summary": "Freezing"
    },

    // ...and several other random forecasts
]

Thanks to the rng we’ve built into our forecast generator, your response will look a bit different than mine, but let’s not gloss over the fact that we’ve just performed a complete request/response cycle within our API without (not technically, but kind of) using a controller! Pretty awesome, huh? While this is great and all, let’s CRUDify our application a little bit with something a bit more interesting than orders and products and universally loved (might need someone to fact check that) by all: beer.

Adding a new feature

Let’s kick things off by adding a new feature folder called Beers underneath our existing Features directory. We should have the following structure in place now:

\Features
    \Beers
    \Weather
        \Endpoints
            GetWeather.cs
        \Models
            WeatherForecast.cs

For our beers feature, we’ll define the following endpoints a user can interact with:

  • A GET endpoint that returns all the beers in our database
  • Another GET endpoint that retrieves a beer given an ID
  • A POST endpoint that creates a beer within our database
  • A DELETE endpoint that removes a beer from our database given a valid ID

I’ve left out the PUT operation due to laziness and also as an implementation exercise for the reader. To make things simple on us, I’ll be using Entity Framework Core with an in-memory database (NOTE: don’t do this in production). As we’re focusing on endpoints within the scope of this post, I’ll leave the details of how I implemented the database and the associated entities here, here, and here. Namely, I’ve added the following:

  • Packages references to Microsoft.EntityFrameworkCore and Microsoft.EntityFrameworkCore.InMemory
  • An Beer.cs associated entity within a Models folder underneath our Beers feature folder
  • An ApplicationDbContext class implementing EF Core’s DbContext to manage Beer entities within our database

I’ve also added some simple seeding code to our database on application startup you can take a look at here to get us off the ground and able to test right away.

Let’s add our first endpoint to retrieve all beers within our database that will be located as a GET on the route /api/beers. Let’s create an Endpoints folder underneath our Beers feature folder that will house all of our API endpoints. Within that folder, let’s create our first endpoint in GetBeers.cs:

GetBeers.cs

using Ardalis.ApiEndpoints;
using DecoupledControllersWithApiEndpoints.Data;
using DecoupledControllersWithApiEndpoints.Features.Beers.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Swashbuckle.AspNetCore.Annotations;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace DecoupledControllersWithApiEndpoints.Features.Beers.Endpoints
{
    [Route(Routes.BeerUri)]
    public class GetBeers : BaseAsyncEndpoint<IEnumerable<Beer>>
    {
        private readonly ApplicationDbContext _context;
        private readonly ILogger<GetBeers> _logger;

        public GetBeers(ApplicationDbContext context, ILogger<GetBeers> logger) =>
            (_context, _logger) = (context, logger);

        [HttpGet]
        [ProducesResponseType(typeof(IEnumerable<Beer>), StatusCodes.Status200OK)]
        [SwaggerOperation(
            Summary = "Retrieves a list of beers",
            Description = "Retrieves a list of beers from the database",
            OperationId = nameof(GetBeers),
            Tags = new[] { nameof(GetBeers) }
        )]
        public override async Task<ActionResult<IEnumerable<Beer>>> HandleAsync(CancellationToken cancellationToken = default)
        {
            _logger.LogInformation("Received request to retrieve all beers...");
            return Ok(await _context.Beers.ToListAsync(cancellationToken));
        }
    }
}

Though seemingly a small amount of code with our associated endpoint HandleAsync() method, let’s breakdown what we’ve added:

  • We’re inheriting from the async version of BaseEndpoint with BaseAsyncEndpoint<TResponse> that exposes an overridable Task<ActionResult<TResponse>> Handle() method with a default CancellationToken provided by ASP.NET Core.
  • We’ve injected only the services this endpoint needs with a logger and our managing database context
  • We’re letting our application know this is a GET with [HttpGet]

As we can see, this endpoint is solely responsible for retrieving a list of all the beers within our database. While there’s not technical need for us to the the async variants of BaseEndpoint, I figured I’d demo some of the cool features it ships with, including exposing a CancellationToken for us to utilize with EF Core. I’ve added a few additional metadata attributes with [ProducesResponseType()] and [SwaggerOperation()] that allow us to build documentation into our endpoints using Swagger and Open API. To utilize Swagger, we’ll need a few packages in Swashbuckle.AspNetCore.Annotations and Swashbuckle.AspNetCore.SwaggerUI (optional, but nice to have). Once you’ve added those via NuGet, let’s go ahead and add their services to Startup.cs:

Startup.cs

using DecoupledControllersWithApiEndpoints.Data;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;

namespace DecoupledControllersWithApiEndpoints
{
    public class Startup
    {
        private readonly string _apiName = "Decoupled Controllers with ApiEndpoints";
        private readonly string _apiVersion = "v1";

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Existing services...

            services.(options => 
            {
                options.EnableAnnotations();
                options.SwaggerDoc(_apiVersion, new OpenApiInfo 
                { 
                    Title = _apiName,
                    Version = _apiVersion
                });
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
           // Existing middleware...

            app.UseSwagger();

            app.UseSwaggerUI(options => options.SwaggerEndpoint("/swagger/v1/swagger.json", $"{_apiName}, {_apiVersion}"));

            // More middleware...
        }
    }
}

With the call to AddSwaggerGen() within our ConfigureServices() method, we’re configuring our Swagger doc generation with defaults and adding the ability to utilize annotations on our endpoints with the [SwaggerOperation()] attribute. Next, we add the UseSwagger() and UseSwaggerUI() middlewares, with UseSwagger() generating our swagger.json file for our UseSwaggerUI() middleware to consume and generate a fancy prebuilt HTML page. If we start our application and navigate to https://localhost:5001/swagger, we should see something resembling the following:

Business_logic

Pretty cool! I’ve taken the image from what our final product will look like, as we’ve only added one documented endpoint for now. Swagger is a tool that assists with development teams in documenting their API contracts for consumers, making integration with consuming API teams much less of a headache and a tool I’d highly encourage using in your applications. I’ve also added a new route to our Routes.cs class:

Routes.cs

namespace DecoupledControllersWithApiEndpoints.Features.Beers
{
    public static class Routes
    {
        public const string BeerUri = "api/beers";
        public const string WeatherUri = "api/weather";
    }
}

With our application running, let’s navigate to our newly added endpoint at https://localhost:5001/api/beers using Postman:

[
    {
        "id": 1,
        "name": "Hexagenia",
        "abv": 7.4,
        "ibu": 120,
        "style": 1,
        "createdAt": "2020-09-23T17:32:40.3846416Z",
        "updatedAt": "2020-09-23T17:32:40.384693Z"
    }, {
        "id": 2,
        "name": "Hazy Little",
        "abv": 6.8,
        "ibu": 90,
        "style": 2,
        "createdAt": "2020-09-23T17:32:40.3847412Z",
        "updatedAt": "2020-09-23T17:32:40.384742Z"
    }, {
        "id": 3,
        "name": "Scrimshaw",
        "abv": 5.4,
        "ibu": 20,
        "style": 0,
        "createdAt": "2020-09-23T17:32:40.3847427Z",
        "updatedAt": "2020-09-23T17:32:40.3847427Z"
    }
]

With our first working endpoint now up and running, let’s spice things up a bit and add a retrieve endpoint to grab specific beers from the database. Let’s go ahead and add a RetrieveBeer.cs file under our Endpoints folder:

RetrieveBeer.cs

using System.Threading;
using System.Threading.Tasks;
using Ardalis.ApiEndpoints;
using DecoupledControllersWithApiEndpoints.Data;
using DecoupledControllersWithApiEndpoints.Features.Beers.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Swashbuckle.AspNetCore.Annotations;

namespace DecoupledControllersWithApiEndpoints.Features.Beers.Endpoints
{
    [Route(Routes.BeerUri)]
    public class RetrieveBeer : BaseAsyncEndpoint<int, Beer>
    {
        private readonly ApplicationDbContext _context;
        private readonly ILogger<RetrieveBeer> _logger;

        public RetrieveBeer(ApplicationDbContext context, ILogger<RetrieveBeer> logger) =>
            (_context, _logger) = (context, logger);

        [HttpGet("{id}")]
        [ProducesResponseType(typeof(Beer), StatusCodes.Status200OK)]
        [ProducesErrorResponseType(typeof(NotFoundResult))]
        [SwaggerOperation(
            Summary = "Retrieve a beer",
            Description = "Retrieves a beer given a valid ID",
            OperationId = nameof(RetrieveBeer),
            Tags = new[] { nameof(RetrieveBeer) }
        )]
        public async override Task<ActionResult<Beer>> HandleAsync([FromRoute] int id, CancellationToken cancellationToken = default)
        {
            _logger.LogInformation($"Received request to retrieve beer with ID {id}");

            // Grab a reference to the matching beer and invalidate the request if none is found
            var beer = await _context.Beers.FirstOrDefaultAsync(b => b.Id == id, cancellationToken);
            if (beer is null)
            {
                return NotFound($"Beer with ID {id} was not found");
            }
            
            return Ok(beer);
        }
    }
}

While the logic in our HandleAsync() might not be the most complicated piece of code you’ve ever seen, we want to focus on our implementation of a new derivative of BaseAsyncEndpoint in BaseAsyncEndpoint<TRequest, TResponse>, accepting a request object that allows us to pass in path and body-based HTTP request metadata.

Since we’ve inheriting from BaseAsyncEndpoint<int, Beer>, we’re telling this endpoint to accept an int in the path (thanks to the [FromRoute] attribute in the HandleAsync() method signature) and to hand us back a Beer type wrapped in an ActionResult. If we send a request to get an existing beer, i.e. https://localhost:5001/api/beers/2, we should get the following:

{
    "id": 2,
    "name": "Hazy Little Thing",
    "abv": 6.8,
    "ibu": 90,
    "style": 2,
    "createdAt": "2020-09-23T17:32:40.3847412Z",
    "updatedAt": "2020-09-23T17:32:40.384742Z"
}

Again, while we may only be utilizing a few services in each endpoint that all happen to be the same, the power in our individual endpoints is the ability to inject endpoint-dependent services into only the endpoints that require them - no more mega constructors!

With our GET endpoints out of the way, let’s give consumers a way to create their favorite beers in our database. First, in our Models folder, let’s add transport object that will hold user data for us to utilize within the body of the POST request. Let’s add a CreateBeerDto.cs class:

CreateBeerDto.cs

namespace DecoupledControllersWithApiEndpoints.Features.Beers.Models
{
    public class CreateBeerDto
    {
        public string? Name { get; set; }

        public string? Style { get; set; }

        public decimal Abv { get; set; }

        public int Ibu { get; set; }
    }
}

With our transport object in place, let’s add an endpoint to our Endpoints folder with CreateBeer.cs:

CreateBeer.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using Ardalis.ApiEndpoints;
using DecoupledControllersWithApiEndpoints.Data;
using DecoupledControllersWithApiEndpoints.Features.Beers.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Swashbuckle.AspNetCore.Annotations;

namespace DecoupledControllersWithApiEndpoints.Features.Beers.Endpoints
{
    [Route(Routes.BeerUri)]
    public class CreateBeer : BaseAsyncEndpoint<CreateBeerDto, Beer>
    {
        private readonly ApplicationDbContext _context;
        private readonly ILogger<CreateBeer> _logger;

        public CreateBeer(ApplicationDbContext dbContext, ILogger<CreateBeer> logger) =>
            (_context, _logger) = (dbContext, logger);

        [HttpPost]
        [ProducesResponseType(typeof(Beer), StatusCodes.Status201Created)]
        [SwaggerOperation(
            Summary = "Creates a beers",
            Description = "Creates a beer in the database using Entity Framework Core",
            OperationId = nameof(CreateBeer),
            Tags = new[] { nameof(CreateBeer) }
        )]
        public override async Task<ActionResult<Beer>> HandleAsync(CreateBeerDto request, CancellationToken cancellationToken = default)
        {
            _logger.LogInformation("Received request to create beer...");

            // Grab a reference to a new Beer entity instance
            var beerToAdd = new Beer
            {
                Name = request.Name,
                Style = Enum.TryParse(request.Style, true, out BeerStyle style) ? style : BeerStyle.Other,
                Ibu = request.Ibu,
                Abv = request.Abv,
                CreatedAt = DateTime.UtcNow,
                UpdatedAt = DateTime.UtcNow
            };

            // Add the beer as a tracked entity and grab a reference to the managed entity returned to us
            var beer = await _context.AddAsync(beerToAdd, cancellationToken);
            await _context.SaveChangesAsync(cancellationToken);

            return Created(new Uri($"/{Routes.BeerUri}/{beer.Entity.Id}", UriKind.Relative), beer.Entity);
        }
    }
}

Again, we see that we’re utilizing BaseAsyncEndpoint<TRequest, TResponse> along with the [FromBody] attribute. We’re letting our application know that POSTs to /api/beers should contain a CreateBeerDto typed body that we’ll consume to create a new Beer entity and insert into our database using our injected _context from EF Core. In return, we’ll hand back a 201 created response with ASP.NET Core’s Created wrapper, and let consumers know where to find the resource relative to our base URL with a Location header. Let’s fire up our application and send the following POST payload to https://localhost:5001/api/beers:

{
    "name": "Deschutes Fresh Squeezed",
    "style": "Ipa",
    "abv": 6.4,
    "ibu": 60
}

We should receive the following response from our API:

{
    "id": 4,
    "name": "Deschutes Fresh Squeezed",
    "abv": 6.4,
    "ibu": 60,
    "style": 1,
    "createdAt": "2020-09-23T18:41:17.6007042Z",
    "updatedAt": "2020-09-23T18:41:17.6007045Z"
}

Feel free to substitute with your favorite beverage of choice. Finally, let’s go ahead and implement a DELETE endpoint to round out (almost, the PUT operation I’ll leave out for the extra motivated folks) our individualized endpoints by adding a DeleteBeer.cs class to our Beers/Endpoints feature folder:

DeleteBeer.cs

using Ardalis.ApiEndpoints;
using DecoupledControllersWithApiEndpoints.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace DecoupledControllersWithApiEndpoints.Features.Beers.Endpoints
{
    [Route(Routes.BeerUri)]
    public class DeleteBeer : BaseAsyncEndpoint<int, NoContentResult>
    {
        private readonly ApplicationDbContext _context;
        private readonly ILogger<DeleteBeer> _logger;

        public DeleteBeer(ApplicationDbContext context, ILogger<DeleteBeer> logger) =>
            (_context, _logger) = (context, logger);

        [HttpDelete("${id}")]
        public override async Task<ActionResult<NoContentResult>> HandleAsync([FromRoute] int id, CancellationToken cancellationToken = default)
        {
            _logger.LogInformation($"Received request to delete beer with ID {id}...");

            // Grab a reference to the beer to delete from the database
            var beer = await _context.Beers.FirstOrDefaultAsync(b => b.Id == id, cancellationToken);
            
            // Invalidate the request if no beer is found
            if (beer is null)
            {
                return NotFound($"Beer with ID {id} was not found");
            }

            // Remove the beer from tracked state and update the database
            _context.Beers.Remove(beer);
            await _context.SaveChangesAsync(cancellationToken);

            return NoContent();
        }
    }
}

Thanks to ApiEndpoints and the flexibility it provides, we’re able to return ASP.NET Core types as well, utilizing the NoContentResult that will return to users a status code of 204 signaling a successful deletion. With our application fired up, go ahead and send a DELETE request to `https://localhost:5001/api/beers/2', and retrieve the list of beers once you’ve done so. Not too bad for a simple API in 20 minutes, if I do say so myself!

Wrapping up

We’ve explored some of the capability that ApiEndpoints allows us and the architecture it encourages in feature-based vertical slices within the API layers of our applications by rethinking how we write controllers. We’ve fundamentally shifted from controllers housing our action methods and discussed a bit about what a bloated controller could look like in terms of application code and why it might be a good idea to keep our controllers thin, or better yet, substitute them for endpoints that act as individual units of work.

By decoupling our controllers from a plethora of injected services and unrelated action methods, we’ve gained the benefit of avoiding the mega constructor by only injecting the exact services an endpoint might need, allowing us to flexibly add and remove services without unintended side effects in other API routes.

While there’s a lot of improvement to be made in our simple example application here, it’s easy to see the power ApiEndpoints brings to the table. While a more DDD-style application may be driven by a library like MediatR, I’ve been enjoying my time with ApiEndpoints to quickly spin up and prototype applications in single web API projects.

Feel free to checkout the code for this post here, and for the inclined, tackle an issue or two for ApiEndpoints here.

Until next, amigos!