Skip to content

Developing Api’s using Http Endpoints

When developing web based applications, it will often lead developers to creating web based API's in order to try and separate back-end and front-end based logic. For most .net based development houses ASP.net MVC is the default choice to develop their API's. However, over the years i have come to realise that this is not necessarily the right choice.

Often when I open code bases, I see the same mistakes being made time and again along with a number of other common anti-patterns.

MVC Controllers are essentially an anti-pattern. They're dinosaurs. They are collections of methods that never call one another and rarely operate on the same state. They're not cohesive. They tend to become bloated and to grow out of control. Their private methods, if any, are usually only called by a single public method. Most developers recognize that controllers should be as small as possible, but they're the only solution offered out of the box, so that's the tool 99% of ASP.NET Core developers use.

STEVE SMITH

On a recent project engagement, I think I have discovered the ultimate example of just how poorly developers can implement the Controller approach, along with another 100 or so very poor coding and solution practices. The projects are riddled with issues and suffer with the countless issues that poor code and implementations cause.

Maintenance is difficult, time and resources are stretched. The solution may work, but it is totally unreliable and the software team and the enterprise at large for the most part are going through every scenario that Bob Martin explains in the first chapter of his book Clean Code and eventually opting for The Grand Redesign in the Sky

Eventually the team rebels. They inform management that they cannot continue to develop in this odious code base. They demand a redesign. Management does not want to expend resources on a whole new redesign project, but they cannot deny that productivity is terrible. Eventually they bend to the demands of the developers and authorize the grand redesign in the sky

Bob Martin - Clean Code

Clean Code

A Handbook of Agile Software Craftsmanship

software developers irrespective of programming language or discipline should read this book

ASP.NET Core API MVC Controllers

Fortunately, on this same project I had the exciting opportunity to start a new series of projects to create new set of APi's, and the organisation was very keen to on improving and re-evaluating the approach to solution design. I seized the opportunity to illustrate how to implement better alternative approaches to implement API's making use of Clean Architecture principles and approaches.

Let me first provide an example of what a typical API project and controllers looks like, when developers follow, the out of the box implementation of MVC API controllers, by providing a typical real life example.

This is just one of the supposed Micro Services in the project. Which as you can tell it is far from being Micro, in that the service itself has far too many responsibilities and to be honest, just looks like most peoples lofts or spare rooms - an odd assortment of junk that is collected into one place.

Now we get to the controllers themselves. Lets take a snapshot look of what can typically be found in controller.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using NSwag.Annotations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using SomeOrganisation.Admin.Jobs.Dtos;
using SomeOrganisation.Admin.Provider;
using SomeOrganisation.Cache;
using SomeOrganisation.Controllers;
using SomeOrganisation.Dto;
using SomeOrganisation.Extensions;
using SomeOrganisation.Filters;
using SomeOrganisation.Framework.Core.DataAccess;
using SomeOrganisation.Framework.Core.Http.CustomHeaders;
using SomeOrganisation.Framework.Core.Jobs.Enums;
using SomeOrganisation.Framework.Core.Jobs.Filters;
using SomeOrganisation.Framework.Core.Jobs.Models;

namespace SomeOrganisation.Admin.Jobs
{
    [Route("/[controller]")]
    public class JobController : BaseUserController
    {
        private readonly IJobControllerService _jobControllerService;
        private readonly IFilterProvider _filterProvider;
        private readonly ILogger<JobController> _logger;

        public JobController(
            IJobControllerService jobControllerService,
            IUserGroupCache userGroupCache,
            ITeamCache teamCache,
            IFilterProvider filterProvider,
            ILogger<JobController> logger)
            : base(userGroupCache, teamCache)
        {
            _jobControllerService = jobControllerService;
            _filterProvider = filterProvider;
            _logger = logger;
        }

        [HttpPost("start/{type}/{name}")]
        [SwaggerResponse(HttpStatusCode.Created, typeof(JobRecord), Description = "The created Job record")]
        [SwaggerResponse(HttpStatusCode.Conflict, typeof(JobRecord), Description = "The conflicting record")]
        [SwaggerResponse(HttpStatusCode.InternalServerError, typeof(object), Description = "Failed to start the Job")]
        public async Task<ActionResult> JobStarts(string type, string name)
        {
            return await _jobControllerService.CreateJob(type, name, UserSomeOrganisationIdAsString);
        }

        [HttpPut("pause/{id}")]
        [SwaggerResponse(HttpStatusCode.OK, typeof(JobRecord), Description = "The Job record")]
        [SwaggerResponse(HttpStatusCode.NotFound, typeof(object), Description = "The Job record could not be found")]
        public async Task<ActionResult> JobPaused(Guid id)
        {
            return await _jobControllerService.JobPaused(id, UserSomeOrganisationIdAsString);
        }


        [HttpPut("resume/{id}")]
        [SwaggerResponse(HttpStatusCode.OK, typeof(JobRecord), Description = "The Job record")]
        [SwaggerResponse(HttpStatusCode.NotFound, typeof(object), Description = "The Job record could not be found")]
        public async Task<ActionResult> JobResumed(Guid id)
        {
            return await _jobControllerService.JobResumed(id, UserSomeOrganisationIdAsString);
        }

        [HttpPatch("{jobId}/assignees")]
        [SwaggerResponse(HttpStatusCode.OK, typeof(void), Description = "Returns nothing")]
        [SwaggerResponse(HttpStatusCode.NotFound, typeof(object), Description = "The user record could not be found")]
        public async Task Assignees(string jobId, [FromBody] IEnumerable<string> assigneeIds)
        {
            using (_logger.BeginScope("UpdateAssignees"))
            {
                await _jobControllerService.UpdateAssignees(jobId, assigneeIds, UserSomeOrganisationIdAsString);
            }
        }

        [HttpPut("updateStatus/{id}/{status}")]
        [SwaggerResponse(HttpStatusCode.OK, typeof(JobRecord), Description = "The Job record with the updated status")]
        [SwaggerResponse(HttpStatusCode.NotFound, typeof(object), Description = "The Job record was not found")]
        public async Task<ActionResult> UpdateStatus(string id, string status)
        {
            return await _jobControllerService.UpdateStatus(id, status.ToEnum<JobState>(), UserSomeOrganisationIdAsString);
        }

        [HttpPut("update/{id}/{name}/{value}/{updatedBy}")]
        [SwaggerResponse(HttpStatusCode.OK, typeof(JobRecord), Description = "The Job record with update info")]
        [SwaggerResponse(HttpStatusCode.NotFound, typeof(object), Description = "The Job record was not foun

The actual file is even longer than than, in fact is over 6 times longer than the above!

Even the most rudimentary glance once over this file, you'll notice that none of these functions actually belong together in one file and this file resembles the project in that it is also an unsightly collection of junk. Even worse is that this junk is further spread across several other files. Most notably another file named a JobControllerService, which inexplicably seems to do nothing else but shove the controller logic and characteristics to another file for absolutely no benefit.

Warning

As a Contractor/Freelance/Consultant I have seen a number of examples of bad code, in a lot of instances these may not always be put down to bad developers, but often just poor patterns guiding developers down the wrong path. In the case of the above this was certainly a contributory factor, along with some poor architectural choices, which could've been the result of a number of other failings in the approach to solution design in general. It's important to stress that in the process of Agile Software Development and emergent design there is always a risk some bad decisions and choices being made, due to time constraints. Software Development is a process of constant evolution, refinement and improvement.

The root cause of a number of these scenarios is organisations and teams not paying enough attention to Technical Debt and not implementing any of the 8 best practices to reduce technical debt

I don't want to spend too much time analysing and dissecting this code, because it really is a case study for one of the worst code bases I have ever had to work on. Diving deeper into this code continually presents a number of WTF moments.

In my personal opinion the root cause of a number of these problems started right at the beginning when the project started and its initial creation, using the MVC API template!

The MVC Web Api template approach is the worst. It just guides developers down the wrong path, and as a result leads to cluttered and spaghetti code.

Ardalis API Endpoints

I have become a big fan of the Adralis ASP.NET Core API Endpoints by Steve Smith as it helps to guide developers down a Clean Architecture Path to developing API Endpoints.

The primary reason for this is that it enforces the Single Responsibility Principle

The single responsibility principle (SRP) instructs developers to write code that has one and only one reason to to change. If a class has more than one reason to change . It has more than one respnsibility

Adaptive Code - Agile coding with design principles and SOLID Principles - Gary Maclean

Applying principles from this book, will help you create code that accommodates new requirements and unforeseen scenarios without significant rewrites.

Get Started with Ardalis ApiEndpoints

I typically start a new API end point project with a very bare minimum project a very simple project with nothing more than a Startup.cs and Program.cs

New Web Api Project

Then I typically add the following references which are enough to get me started. The point from this Tutorial is the Ardalis.Endpoints

<Project Sdk="Microsoft.NET.Sdk.Web">

    <PropertyGroup>
        <TargetFramework>net5.0</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Ardalis.ApiEndpoints" Version="3.0.0" />
        <PackageReference Include="AutoMapper" Version="10.1.1" />
        <PackageReference Include="MediatR" Version="9.0.0" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.1" />
        <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.1.1" />
        <PackageReference Include="Threenine.Data" Version="2.0.219" />
    </ItemGroup>
</Project>

Due to the fact that we will be developing a REST based API, we probably want to take the time to set up and configure our SwashBuckle Swagger. The key aspect here is that we EnableAnnotations which will help to enrich our documentation for the API.

In Building Microservices: Designing Fine grained systems Sam Newman stresses the importance of when decomposing systems down into finer grained microservices we'll be exposing seams of functionality in the form of API's and because of this ease of discovery and good documentation are important. Swagger provides functionality to annotate you API methods and generate the documentation for you.

 public class Startup
    {
        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)
        {
             services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo {Title = "api", Version = "v1"}); 
                c.EnableAnnotations();
                c.CustomSchemaIds(x => x.FullName);
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "api v1"));
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
        }
    }
}

With all this grunt work taken care of we can now start coding! lets create our first end point. In this quick example, we will start developing a hypothetical Headless CMS Rest API, we will just simply create an initial endpoint to retrieve a Blog post by passing an Id.

Building Microservices

Designing Fine-Grained Systems

Microservice technologies are moving quickly. Author Sam Newman provides you with a firm grounding in the concepts while diving into current solutions for modeling, integrating, testing, deploying, and monitoring your own autonomous services.

Information

If you want to reduce the amount of grunt work in the future, take a look at How to create project templates in .net core, because I walk you through the process of creating a reusable project template in .net core and the side bonus, I use the APi Endpoints project a template and provide a re-usable template!

Developing Endpoints

This will be just enough the illustrate the basics of Ardalis API Endpoints. So lets get started developing our first Get Endpoint.

The first step is to create a folder to store all your endpoints. It seems the convention for this is call your folder "Endpoints" but I have seem some people/teams call them either one of the following :

  • Endpoints
  • Activities
  • Resources
  • Features
  • API

for the purpose of this article, and for brevity I am simply going to call them "Endpoints" , then I usually create another folder under neath for my specific routes. In this case, we'll start with Article

We now create our first Endpoint Folder, personally I like to name my folder after the REST function they are going to full fill i.e. Get, Post, Put, Patch, Delete. Although this isn't a hard and fast rule, it just helps me to focus and quickly tell what the goal and purpose of the folder at a glance in the project explorer. I also make use of Jetbrains Rider File Nesting

The primary purpose my Endpoint is to provide JSON Response Object which will contain data that was requested by parameters defined in a Request object. I have grown to appreciate what has been enabled by API Endpoints by Steve Smith and the Request EndPoint Response (REPR) Pattern and even borrowing a bit of the Vertical Slice Architecture and CQRS is isolating all the logic to endpoint within one folder, thus implementing the Feature Slice pattern. I am finding this approach so much easier to work with, opposed to the sprawling layering, which conventional Onion Architecture or Clean Architecture leads you down.

In this example we will be providing 2 route parameter

Query object

The article query object is initially going to contain 2 Parameters. Which will be used to provide the details of the article we would like to get. In our example we will query the Article by Id and Category, which will be provided as a Url route parameter.

using Microsoft.AspNetCore.Mvc;

namespace api.Endpoints.Article.Get
{
    public class Query
    {
        [FromRoute(Name = "id")] public string Id { get; set; }
        [FromRoute(Name = "category")] public string Category { get; set; }
    }
}

We make use of the Model Binding Attributes to indicate where the values will be coming from. Note because I am making use of the Name property and setting the name to lower case. The reason I am doing this is because Model binding is case sensitive, and later on in this article I will illustrate why this is important.

Response object

The article response will be a DTO (Data Transmission Object) we will use to provide data back to the calling application. In our case this will contain the basic structure of a blog/article. This may be a simple POCO (Plain Old C# Object) . In our case we're going to keep this very simple for illustration purposes

using System;

namespace api.Endpoints.Article.Get
{
    public class Response
    {
        public string Title { get; set; }
        public string Description { get; set; }
        public string Summary { get; set; }
        public string SubHeading { get; set; }
        public string Content { get; set; }
        public DateTime Published { get; set; }
    }
}

Developing the Get Endpoint

We can now get on with developing our end point, and this is where we will now start to make use of the Ardalis Framework. We will create an Asynchronous Endpoint making use of our Request Response objects.

We will make use of the Fluent Generics Pattern to create our Endpoint. fluent interface design is a popular pattern for building flexible and convenient interfaces. Its core idea revolves around using method chaining to express interactions through a continuous flow of human-readable instructions.

Alexey Golub has a great post on Fluent Generics in C#, its well worth a read and in my opinion does a great job in explaining the concept.

We inherit the BaseAsyncEnpoint and provide our Request and Response objects and if we implement the members we will get one method created for us which is the HandleAsync which forms the basic of our Endpoint.

using System.Threading;
using System.Threading.Tasks;
using Ardalis.ApiEndpoints;
using Microsoft.AspNetCore.Mvc;

namespace api.Endpoints.Article.Get
{
    public class Get  : BaseAsyncEndpoint
        .WithRequest<Query>
        .WithResponse<Response>
    {
        public override Task<ActionResult<Response>> HandleAsync(Query request, CancellationToken cancellationToken = new CancellationToken())
        {
            throw new System.NotImplementedException();
        }
    }
}

We'll add everything to make our API Endpoint fully functional. In this particular case we are simply going to hard code the return object just for illustration purpose.

We will add all the relevant swagger information to help document our endpoint as much as possible.

You may also notice the I have added the async keyword to the HandleAsync , and I also decorated the Request parameter with [FromRoute] attribute. The reason I have done so, is because there seems to be some bug/quirk/irregularity with the ASPNET Core Model binding, which only seems to affect the [FromRoute] parameters, in that it doesn't always seem to bind them unless Request object itself is marked as [FromRoute] , all other seem to work fine

In further future articles I will provide details on how to mix various Parameters together on a request object. However, for now we will keep it simple.

using System;
using System.Threading;
using System.Threading.Tasks;
using api.Endpoints.Article.Request;
using api.Endpoints.Article.Response;
using Ardalis.ApiEndpoints;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

namespace api.Endpoints.Article.Get
{
    [Route("/article")]
    public class Get : BaseAsyncEndpoint
        .WithRequest<Query>
        .WithResponse<Response>
    {
        public Get()
        {
        }

        [HttpGet("{category}/{id}")]
        [SwaggerOperation(
            Summary = "Retrieve an article by id ",
            Description = "Retrieves a full articles ",
            OperationId = "EF0A3653-153F-4E73-8D20-621C9F9FFDC9",
            Tags = new[] {"Article"})
        ]
        [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Response))]
        [Produces("application/json")]
        public override async Task<ActionResult<Response>> HandleAsync([FromRoute] Query request,
            CancellationToken cancellationToken = new CancellationToken())
        {
           return await Task.Run(() => new OkObjectResult(new Response
            {
                Content = "blah blah blah",
                Description = "This is a Fine Description",
                Published = DateTime.Now.AddHours(-10),
                Summary = "this is a fine Summary",
                SubHeading = "This is a sub heading"
            }), cancellationToken);
        }
    }
}

We're now ready to run our first end point and test it out. If we hit F5 and start he project and open our browser and navigate to http://localhost:5000/swagger. We will see our End Point in all its glory!

If we execute the Endpoint we see our values returned. Our Endpoint behaves just as any other.

Conclusion

Developing API endpoints using the Ardalis API Endpoints framework helps developers to focus on the Clean Code and SOLID principles.

In future articles I will elaborate more on the finer details of using the this framework in helping you to minimise bugs and writing cleaner, maintainable and readable code.

Gary Woodfine
Latest posts by Gary Woodfine (see all)