Best Practices — WebAPI .NET 6.0 | Part II

Justin Muench
CodeX
Published in
7 min readDec 1, 2022

--

Photo by Clem Onojeghuo on Unsplash

My first post on Web API .NET 6 best practices covered REST, URI design, naming, HTTP verbs and return types of .NET 6, and a bit about the Minimal API. This post will cover MediatR, more about the Minimal API, and Background Services.

ASP.NET Core MVC Best Practices

Currently, most ASP.NET Core APIs use MVC — presumably, this will change in the future, and Minimal API will become the new standard [1].

When creating controller classes, you should always use the [ApiController] attribute; this adds some helpful features to the APIs [1]. It also enforces attribute routing, a recommendation for all API endpoints [1]. Two other excellent features of MVC are model binding and validation [1].

As part of best practices and MVC, controllers should also be minimized in size [1]. Provided filters in MCV can create constant APIs [1]. One of the most important best practices is using async / await [1]. You should only do without it if a method works synchronously [1].

Furthermore, you get client and proxy caching for the server responses [1]. Possibilities for compression of the response and content negotiation between client and server through headers [1]. In addition, MVC comes with a default error handler and a controller base class with many helpful functions [1].

Controllers

Controllers should be as small as possible [1]. This makes them easier to manage and ensures conflict-free merging [1]. The Logic in Action methods can be outsourced to keep the controllers small [1].

Controller methods should not contain conditional queries [1]; filters should replace multiple-used logic [1]. Action methods should not include business or data access logic [1].

Additionally, try catches within controller methods should be avoided, as they are considered conditional queries and are anti-patterns [1]. The idea is that large controllers have more dependencies and usually get larger over time [1].

MediatR

MediatR is based on the Mediator Pattern, published in 1994 in the book Design Patterns by authors Gamma, Helm, Johnson, and Vlissides [1].

The mediator design pattern is one of the essential and widely used behavioral design patterns. Mediator enables the decoupling of objects by introducing a layer in between so that the interaction between objects happens via the layer [2].

The MediatR package defines specific interfaces such as IRequest<TResponse> and IRequestHandler<TRequest, TResponse> [1].

Let's look at how MediatR can help us create clean controllers. A typical action method might look like this.

First, the incoming data is validated, then a mapping to the business model takes place (reminder: there should be a separation between the business model and the API model) [1]. After the conversion, the model is persisted and converted back to the API model with an appropriate HTTP response [1].

Many of these tasks would not need to be done in the Action Method, which is why we are now looking at refactoring.

We can remove the validation from the method, as this will be handled via an attribute [1]. In the next step, we could adopt a mapping service, working as well as mapping and delivering back a response DTO [1]. This service can now be replaced by MediatR [1]. MediatR provides a method called Send, which can find the appropriate service for processing the request on its own so that the injection of a specific service becomes overdue [1].

One advantage of this approach is consistency since only mediators need to be injected into the controllers [1].

MediatR - Demo

For a demo on implementing MediatR, I would like to refer to Nick Chapsa's video series.

ASP.NET Core Minimal API — Best Practices

Minimal APIs are an alternative to controllers and MVC patterns.

They are a lightweight option for implementing simple API scenarios inside the program.cs [1]. However, for non-trivial use cases, multiple files should be used [1].

The similarity is striking when comparing the definitions of MVC and Minimal API endpoint [1].

// Classical approach
[HttpPost("customers")]
public ActionResult<CustomerDTO> CreateCustomer([FromBody] CustomerDTO customer)
{
// some Logic....
}

// Minimal approach
app.MapPost("/customers", (CustomerDTO customer) =>
{
//some logic
})

As you can see, in both cases, the method or verb (in this case, "Post") and the route ("/customers") must be specified. This is followed by the adoption of a model and the logic for processing [1].

While MVC uses IACtionResult and ActionResult<T>, the Minimal approach returns IResult [1].

One challenge in organizing Minimal APIs is that Microsoft has no guidelines [1]. Having more than one or two endpoints in the program should be avoided.CS [1]. Instead, these should be structured into files and folders by resource [1]. You should create individual files per endpoint, and for that, you can use, for example, one of those two options [1]:

  • It is possible to use a Nuget Package called MinimalApi.Endpoint
  • Or you can use static extension methods.

Let's take a look at how the second option could look in a real-world scenario:

First, we created a folder called Endpoints within our project.

In it, we make a static class with the resource name+endpoints (like CustomerEndpoints.cs). In this, we bundle all endpoints of a resource.

Inside this class, we create a static method called Map[RESOURCE]Endpoints with this WebApplication app's parameter so that we can write an extension.

Inside this method, we then execute app.Map[Verb] instead of calling it in Program.CS.

public static class CustomerEndpoints
{
public static void MapCustomerEndpoints(this WebApplication app)
{
app.MapGet("/customers", GetCustomers)
.Produces<List<CustomerDTO>>(StatusCodes.Status200OK)
.Produces<int>((int)StatusCodes.Status404NotFound)
.WithMetadata(new SwaggerOperationAttribute
{
Summary = "Get all customers",
Description = "Get all customers",
OperationId = "GetCustomers",
Tags = new[] { "Customer" }
})
.WithName("ListCustomers").WithTags("Customers");
}

static IResult GetCustomers()
{
var customers = new List<CustomerDTO>
{
new CustomerDTO
{
CustomerId = 1,
FirstName = "John",
LastName = "Doe"
},
new CustomerDTO
{
CustomerId = 2,
FirstName = "Jane",
LastName = "Doe"
}
};

return Results.Ok(customers);
}
}

For Swagger to work cleanly with the Minimal API, the metadata must be set [3].

Within the Program.CS, the endpoints can then be registered via app.MapCustomerEndpoints().

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

//Added Endpoints
app.MapCustomerEndpoints();

app.Run();

Background Services

Often, the processing of a request can run out of sync, and only feedback about the incoming request is sufficient for the client.

In these cases, a background service can be used. This comes with ASP.NET Core and requires no additional installation of a NuGet package.

First, I created a folder called BackgroundServices.

In this, I create a class that implements BackgroundServices.

using Microsoft.Extensions.Hosting;

namespace ExampleMinimalAPI.BackgroundService
{
public class CustomerBackgroundService : Microsoft.Extensions.Hosting.BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
int runs = 0;
while (!stoppingToken.IsCancellationRequested && runs <= 10)
{
Console.WriteLine("CustomerBackgroundService is working.");
runs++;
await Task.Delay(1000, stoppingToken);
}
}
}
}

Now I have to register for the background service in the program.CS.

//Adding Backgroundservice
builder.Services.AddHostedService<CustomerBackgroundService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.MapCustomerEndpoints();

app.Run();

Since hosted services are added to the DI container with the singleton lifetime, only singleton and transient service lifetimes can be resolved [5].

If we want to inject a service with a scope, we have to resolve a service with the scope within the background task [5].

For this, it is necessary to inject an IServiceProvider instance. Through this, we can create a new scope instance with the method CreateScope, which is then resolvable in the BackgroundTask [5].

First, I created a folder with my service. This is registered classically via DI in the program.CS.

public interface ICustomerService
{
public Task<bool> SendWelcomeMailCustomer(Customer customer);
}

public class CustomerService : ICustomerService
{
public Task<bool> SendWelcomeMailCustomer(Customer customer)
{
// Send Mail Logic...
return Task.FromResult(true);
}
}

In the background service, I now adjust the ExecuteAsync method.

  protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = _serviceProvider.CreateScope();
var customerService = scope.ServiceProvider.GetRequiredService<ICustomerService>();

// Some Example Logic ...
await customerService.SendWelcomeMailCustomer(new Customer
{
CustomerId = 1,
FirstName = "John",
LastName = "Doe"
});

await Task.Delay(10000, stoppingToken);
}
}

BackgroundServices are suitable for running outside the process scope, such as I/O operations or sending mail [1].

Oops, so much text again. There will probably be the third part about security and testing aspects.

Want to connect?
You can contact me via my social media channels (Twitter, LinkedIn).

--

--

Justin Muench
CodeX

Software developer with a passion for .NET Core, SQL, and Python. Interested in AI and studying computer science and economics.