Sunday, August 30, 2020

ASP.NET Core Web API Best Practices

 While we are working on a project, our main goal is to make it work as it supposed to and fulfill all the customer’s requirements.

But wouldn’t you agree that creating a project that works is not enough? Shouldn’t that project be maintainable and readable as well?

It turns out that we need to put a lot more attention to our projects to write them in a more readable and maintainable way. The main reason behind this statement is that probably we are not the only ones who will work on that project. Other people will most probably work on it once we are done with it.

So, what should we pay attention to?

In this post, we are going to write about what we consider to be the best practices while developing the .NET Core Web API project. How we can make it better and how to make it more maintainable.

We are going to go through the following sections:

Startup Class and the Service Configuration

In the Startup class, there are two methods: the ConfigureServices method for registering the services and the Configure method for adding the middleware components to the application’s pipeline.

So, the best practice is to keep the ConfigureServices method clean and readable as much as possible. Of course, we need to write the code inside that method to register the services, but we can do that in a more readable and maintainable way by using the Extension methods.

For example, let’s look at the wrong way to register CORS:

public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
}

Even though this way will work just fine, and will register CORS without any problem, imagine the size of this method after registering dozens of services.

That’s not readable at all.

The better way is to create an extension class with the static method:

public static class ServiceExtensions
{
public static void ConfigureCors(this IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
}
}

And then just to call this extended method upon the IServiceCollection type:
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureCors();
}

To learn more about the .NET Core’s project configuration check out: .NET Core Project Configuration.

Project Organization

We should always try to split our application into smaller projects. That way we are getting the best project organization and separation of concerns (SoC). The business logic related to our entities, contracts, accessing the database, logging messages or sending an email message should always be in a separate .NET Core Class Library project.

Every small project inside our application should contain a number of folders to organize the business logic.

Here is just one simple example of how a completed project should look like:

Project-Structure - Best Practices .NET Core

Environment Based Settings

While we develop our application, that application is in the development environment. But as soon as we publish our application it is going to be in the production environment. Therefore having a separate configuration for each environment is always a good practice.

In .NET Core, this is very easy to accomplish.

As soon as we create the project, we are going to get the appsettings.json file and when we expand it we are going to see the appsetings.Development.json file:

Appsettings-development - Best Practices

All the settings inside this file are going to be used for the development environment.

We should add another file appsettings.Production.json, to use it in a production environment:

Appsettings - production - Best Practices

The production file is going to be placed right beneath the development one.

With this setup in place, we can store different settings in the different appsettings files, and depending on the environment our application is on, .NET Core will serve us the right settings. For more information about this topic, check out Multiple Environments in ASP.NET Core.

Data Access Layer

In many examples and different tutorials, we may see the DAL implemented inside the main project and instantiated in every controller. This is something we shouldn’t do.

When we work with DAL we should always create it as a separate service. This is very important in the .NET Core project because when we have DAL as a separate service we can register it inside the IOC (Inversion of Control) container. The IOC is the .NET Core’s built-in feature and by registering a DAL as a service inside the IOC we are able to use it in any controller by simple constructor injection:

public class OwnerController: Controller
{
private IRepository _repository;
public OwnerController(IRepository repository)
{
_repository = repository;
}
}

The repository logic should always be based on interfaces and if you want, making it generic will allow you reusability as well. Check out this post: .Net Core series – Part 4 to see how we implement the Generic Repository Pattern inside the .NET Core’s project.

Controllers

The controllers should always be as clean as possible. We shouldn’t place any business logic inside it.

So, our controllers should be responsible for accepting the service instances through the constructor injection and for organizing HTTP action methods (GET, POST, PUT, DELETE, PATCH…):

public class OwnerController: Controller
{
private ILoggerManager _logger;
private IRepository _repository;
public OwnerController(ILoggerManager logger, IRepository repository)
{
_logger = logger;
_repository = repository;
}
[HttpGet]
public IActionResult GetAllOwners()
{
}
[HttpGet("{id}", Name = "OwnerById")]
public IActionResult GetOwnerById(Guid id)
{
}
[HttpGet("{id}/account")]
public IActionResult GetOwnerWithDetails(Guid id)
{
}
[HttpPost]
public IActionResult CreateOwner([FromBody]OwnerForCreationDto owner)
{
}
[HttpPut("{id}")]
public IActionResult UpdateOwner(Guid id, [FromBody]OwnerForUpdateDto owner)
{
}
[HttpDelete("{id}")]
public IActionResult DeleteOwner(Guid id)
{
}
}

Actions

Our actions should always be clean and simple. Their responsibilities include handling HTTP requests, validating models, catching errors and returning responses:

[HttpPost]
public IActionResult CreateOwner([FromBody]OwnerForCreationDto owner)
{
try
{
if (owner == null)
{
return BadRequest("Owner object is null");
}
if (!ModelState.IsValid)
{
return BadRequest("Invalid model object");
}
//additional code
return CreatedAtRoute("OwnerById", new { id = createdOwner.Id }, createdOwner);
}
catch (Exception ex)
{
_logger.LogError($"Something went wrong inside the CreateOwner action: {ex}");
return StatusCode(500, "Internal server error");
}
}

Our actions should have IActionResult as a return type in most of the cases (sometimes we want to return a specific type or a JsonResult…). That way we can use all the methods inside .NET Core which returns results and the status codes as well.

The most used methods are:

  • OK => returns the 200 status code
  • NotFound => returns the 404 status code
  • BadRequest => returns the 400 status code
  • NoContent => returns the 204 status code
  • Created, CreatedAtRoute, CreatedAtAction => returns the 201 status code
  • Unauthorized => returns the 401 status code
  • Forbid => returns the 403 status code
  • StatusCode => returns the status code we provide as input

Handling Errors Globally

In the example above, our action has its own try-catch block. This is very important because we need to handle all the errors (that in another way would be unhandled) in our action method. Many developers are using try-catch blocks in their actions and there is absolutely nothing wrong with that approach. But, we want our actions to be clean and simple, therefore, removing try-catch blocks from our actions and placing them in one centralized place would be an even better approach.

.NET Core gives us an opportunity to implement exception handling globally with a little effort by using built-in and ready to use middleware. All we have to do is to add that middleware in the Startup class by modifying the Configure method:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseExceptionHandler(config =>
{
config.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
var ex = error.Error;
await context.Response.WriteAsync(new ErrorModel()
{
StatusCode = 500,
ErrorMessage = ex.Message
}.ToString()); //ToString() is overridden to Serialize object
}
});
});
app.UseMvc();
}

We can even write our own custom error handlers by creating custom middleware:
public class CustomExceptionMiddleware
{
//constructor and service injection
public async Task Invoke(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (Exception ex)
{
_logger.LogError("Unhandled exception ...", ex);
await HandleExceptionAsync(httpContext, ex);
}
}
//additional methods
}

After that we need to register it and add it to applications pipeline:
public static IApplicationBuilder UseCustomExceptionMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<CustomExceptionMiddleware>();
}

app.UseCustomExceptionMiddleware();

To read in more detail about this topic, visit Global Error Handling in ASP.NET Core Web API.

Using ActionFilters to Remove Duplicated Code

Filters in ASP.NET Core allows us to run some code prior to or after the specific stage in a request pipeline. Therefore, we can use them to execute validation actions that we need to repeat in our action methods.

When we handle a PUT or POST request in our action methods, we need to validate our model object as we did in the Actions part of this article. As a result, that would cause the repetition of our validation code, and we want to avoid that (Basically we want to avoid any code repetition as much as we can).

We can do that by using ActionFilters. Instead of validation code in our action:

if (!ModelState.IsValid)
{
// bad request and logging logic
}

We can create our filter:
public class ModelValidationAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState); // returns 400 with error
}
}
}

And register it in the Startup class in the ConfigureServices method:
services.AddScoped<ModelValidationAttribute>();

Now, we can use that filter with our action methods.

To read in more detail about using Action Filters, visit our post: Action Filters in .NET Core.

Using DTOs to Return Results and to Accept Inputs

Even though we can use the same model class to return results or accept parameters from the client, that is not a good practice. A much better practice is to separate entities that communicate with the database from the entities that communicate with the client. Yes, the answer is to use DTOs.

The model class is a full representation of our database table and being like that, we are using it to fetch the data from the database. But once the data is fetched we should map the data to the DTO and return that result to the client. By doing so, if for some reason we have to change the database, we would have to change only the model class but not the DTO because the client may still want to have the same result. You can read more about the DTO’s usage in the fifth part of the .NET Core series.

We shouldn’t be using DTOs only for the GET requests. We should use them for other actions as well. For example, if we have a POST or PUT action, we should use the DTOs as well. To read more about this topic, you can read the sixth part of the .NET Core series.

Additionally, DTOs will prevent circular reference problems as well in our project.

Routing

In the .NET Core Web API projects, we should use Attribute Routing instead of Conventional Routing. That’s because Attribute Routing helps us match the route parameter names with the actual parameters inside the action methods. Another reason is the description of the route parameters. It is more readable when we see the parameter with the name “ownerId” than just “id”.

We can use the [Route] attribute on top of the controller and on top of the action itself:

[Route("api/[controller]")]
public class OwnerController: Controller
{
[Route("{id}")]
[HttpGet]
public IActionResult GetOwnerById(Guid id)
{
}
}

There is another way to create routes for the controller and actions:
[Route("api/owner")]
public class OwnerController: Controller
{
[HttpGet("{id}")]
public IActionResult GetOwnerById(Guid id)
{
}
}

There are different opinions which way is better, but we would always recommend the second way, and this is something we always use in our projects.

When we talk about the routing we need to mention the route naming convention. We can use descriptive names for our actions, but for the routes/endpoints, we should use NOUNS and not VERBS.

The few wrong examples:

[Route("api/owner")]
public class OwnerController : Controller
{
[HttpGet("getAllOwners")]
public IActionResult GetAllOwners()
{
}
[HttpGet("getOwnerById/{id}"]
public IActionResult GetOwnerById(Guid id)
{
}
}

The good examples:
[Route("api/owner")]
public class OwnerController : Controller
{
[HttpGet]
public IActionResult GetAllOwners()
{
}
[HttpGet("{id}"]
public IActionResult GetOwnerById(Guid id)
{
}
}

For the more detailed explanation of the Restful practices checkout: Top REST API Best Practices.

Logging

If we plan to publish our application to production, we should have a logging mechanism in place. Log messages are very helpful when figuring out how our software behaves in production.

.NET Core has its own logging implementation by using the ILoggerinterface. It is very easy to implement it by using Dependency Injection feature:

public class TestController: Controller
{
private readonly ILogger _logger;
public TestController(ILogger<TestController> logger)
{
_logger = logger;
}
}

Then in our actions, we can utilize various logging levels by using the _logger object.

.NET Core supports logging API that works with a variety of logging providers. Therefore, we may use different logging providers to implement our own logging logic inside our project.

The NLog is the great library to use for implementing our own custom logging logic. It is extensible, supports structured logging and very easy to configure. We can log our messages in the console window, files or even database.

To learn more about using this library inside the .NET Core check out: .NET Core series – Logging With NLog.

The Serilog is the great library as well. It fits in with the .NET Core built-in logging system.

CryptoHelper And Data Protection

We won’t talk about how we shouldn’t store the passwords in a database as a plain text and how we need to hash them due to security reasons. That’s out of the scope of this article. There are various hashing algorithms all over the internet, and there are many different and great ways to hash a password.

If we want to do it on our own, we can always use the IDataProtector interface which is quite easy to use and implement in the existing project.

To register it, all we have to do is to use the AddDataProtection method in the ConfigureServices method. Then it can be injected via Dependency Injection:

private readonly IDataProtector _protector;
public EmployeesController( IDataProtectionProvider provider)
{
_protector = provider.CreateProtector("EmployeesApp.EmployeesController");
}

Finally, we can use it: _protector.Protect("string to protect");

You can read more about it in the Protecting Data with IDataProtector article.

But if need the library that provides support to the .NET Core’s application and that is easy to use, the CryptoHelper is quite a good library.

The CryptoHelper is a standalone password hasher for .NET Core that uses a PBKDF2 implementation. The passwords are hashed using the new Data Protection stack.

This library is available for installation through the NuGet and its usage is quite simple:

using CryptoHelper;
// Method for hashing the password
public string HashPassword(string password)
{
return Crypto.HashPassword(password);
}
// Method to verify the password hash against the given password
public bool VerifyPassword(string hash, string password)
{
return Crypto.VerifyHashedPassword(hash, password);
}

Content Negotiation

By default .NET Core Web API returns a JSON formatted result. In most cases, that’s all we need.

But what if the consumer of our Web API wants another response format, like XML for example?

For that, we need to create a server configuration to format our response in the desired way:

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(config =>
{
// Add XML Content Negotiation
config.RespectBrowserAcceptHeader = true;
});
}

Sometimes the client may request a format that is not supported by our Web API and then the best practice is to respond with the status code 406 Not Acceptable. That can be configured inside our ConfigureServices method as well:
config.ReturnHttpNotAcceptable = true;

We can create our own custom format rules as well.

The content negotiation is a pretty big topic so if you want to learn more about it, check out: Content Negotiation in .NET Core.

Security and Using JWT

JSON Web Tokens (JWT) are becoming more popular by the day in web development. It is very easy to implement JWT Authentication due to the .NET Core’s built-in support. JWT is an open standard and it allows us to transmit the data between a client and a server as a JSON object in a secure way.

We can configure the JWT Authentication in the ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(opt => {
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
//Configuration in here
};
});
}

In order to use it inside the application, we need to invoke this code in the Configure method:
app.UseAuthentication();
app.UseAuthorization();

We may use JWT for the Authorization part as well, by simply adding the role claims to the JWT configuration.

To learn in more detail about JWT authentication and authorization in .NET Core, check out JWT with .NET Core and Angular Part 1 and Part 2 of the series.

ASP.NET Core Identity

Additionally, if you want to use some advanced security actions in your application like Password Reset, Email Verification, Third Party Authorization, etc, you can always refer to the ASP.NET Core Identity. ASP.NET Core Identity is the membership system for web applications that includes membership, login and user data. It contains a lot of functionalities to help us in the user management process. In our ASP.NET Core Identity series, you can learn a lot about those features and how to implement them in your ASP.NET Core project.

Using IdentityServer4 – OAuth2 and OpenID Connect

IdentityServer4 is an Authorization Server that can be used by multiple clients for Authentication actions. It has nothing to do with the user store management but it can be easily integrated with the ASP.NET Core Identity library to provide great security features to all the client applications. OAuth2 and OpenID Connect are protocols that allow us to build more secure applications. OAuth2 is more related to the authorization part where OpenID Connect (OIDC) is related to the Identity(Authentication) part.  We can use different flows and endpoints to apply security and retrieve tokens from the Authorization Server.  You can always read RFC 6749 online documentation to learn more about OAuth2.

Testing Our Applications

We should write tests for our applications as much as we can. We know, from our experience, there is no always time to do that, but it is very important for checking the quality of the software we are writing. We can discover potential bugs in the development phase and make sure that our app is working as expected prior to publishing it to the production. Of course, there are many additional reasons to write tests for our applications.

To learn more about testing in ASP.NET Core application (Web API, MVC or any other), you can read our ASP.NET Core Testing Series, where we explain the process in great detail.

Conclusion

In this article,  our main goal was to familiarize you with the best practices when developing a Web API project in .NET Core. Some of those could be used in other frameworks as well, therefore, having them in mind is always helpful.

If you find that something is missing from the list, don’t hesitate to add it in a comment section.

Thank you for reading the article and we hope you found something useful in it.

Tips and tricks for ASP.NET Core applications

 

This is a small collection of some tips and tricks which I keep repeating myself in every ASP.NET Core application. There's nothing ground breaking in this list, but some general advice and minor tricks which I have picked up over the course of several real world applications.

Logging

Let's begin with logging. There are many logging frameworks available for .NET Core, but my absolute favourite is Serilog which offers a very nice structured logging interface for a vast number of available storage providers (sinks).

Tip 1: Configure logging before anything else

The logger should be the very first thing configured in an ASP.NET Core application. Everything else should be wrapped in a try-catch block:

public class Program
{
    public static int Main(string[] args) => StartWebServer(args);

    public static int StartWebServer(string[] args)
    {
        Log.Logger =
            new LoggerConfiguration()
                .MinimumLevel.Warning()
                .Enrich.WithProperty("Application", "MyApplicationName")
                .WriteTo.Console()
                .CreateLogger();

        try
        {
            WebHost.CreateDefaultBuilder(args)
                .UseSerilog()
                .UseKestrel(k => k.AddServerHeader = false)
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseStartup<Startup>()
                .Build()
                .Run();

            return 0;
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, "Host terminated unexpectedly.");
            return -1;
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }
}

Tip 2: Flush the logger before the application terminates

Make sure to put Log.CloseAndFlush(); into the finally block of your try-catch block so that no log data is getting lost when the application terminates before all logs have been written to the log stream.

Tip 3: Enrich your log entries

Configure your logger to automatically decorate every log entry with an Application property which contains a unique identifier for your application (typically a human readable name which identifies your app):

.Enrich.WithProperty("Application", "MyApplicationName")

This is extremely useful if you write logs from more than one application into a single log stream (e.g. a single Elasticsearch database). Personally I prefer to write logs from multiple (smaller) services of a coherent system into a single logging database and filter logs by properties.

Appending an additional Application property to all your application logs has the advantage that one can easily filter and view the overall health of a single application as well as getting a holistic view of the entire system.

Other really useful information which could be appended to your log entries is the application version and the environment name:

Log.Logger =
    new LoggerConfiguration()
        .MinimumLevel.Warning()
        .Enrich.WithProperty("Application", "MyApplicationName")
        .Enrich.WithProperty("ApplicationVersion", "<version number>")
        .Enrich.WithProperty("EnvironmentName", "Staging")
        .WriteTo.Console()
        .CreateLogger();

This will allow one to better visualise if issues had been resolved (or appeared) after a certain version has been deployed and it will also make it very easy to filter out any logs which might have accidentally been written from a different environment (e.g. a developer was debugging locally with the production connection string in their settings).

Startup Configuration

In ASP.NET Core there are two main places where features and functionality get configured. First there is the Configure method which can be used to plug middleware into the ASP.NET Core pipeline and secondly there is the ConfigureServices method to register dependencies.

For example adding Swagger to ASP.NET Core would look a bit like this:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddSwaggerGen(
        c =>
        {
            var name = "<my app name>"
            var version = "v1";

            c.SwaggerDoc(
                version,
                new Info { Version = version, Title = name });

            c.DescribeAllEnumsAsStrings();

            var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
            var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
            c.IncludeXmlComments(xmlPath);
        });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app .UseSwagger()
        .UseSwaggerUI(
            c =>
            {
                var name = "<my app name>"
                var version = "v1";
                c.RoutePrefix = "";
                c.SwaggerEndpoint(
                    "/swagger/{version}/swagger.json", name);
            }
        )
        .UseMvc();
}

Middleware and dependencies are obviously two different things and therefore their configuration is split into two different methods, but from a developer's point of view it is very annoying that most features are configured across more than just one place.

Tip 4: Create 'Config' classes

One nice way to combat this is by creating a Config folder in the root of your ASP.NET Core application and create <FeatureName>Config classes for each feature/functionality which needs to be registered in Startup:

public static class SwaggerConfig
{
    private static string Name => "My Cool API";
    private static string Version => "v1";
    private static string Endpoint => $"/swagger/{Version}/swagger.json";
    private static string UIEndpoint => "";

    public static void SwaggerUIConfig(SwaggerUIOptions config)
    {
        config.RoutePrefix = UIEndpoint;
        config.SwaggerEndpoint(Endpoint, Name);
    }

    public static void SwaggerGenConfig(SwaggerGenOptions config)
    {
        config.SwaggerDoc(
            Version,
            new Info { Version = Version, Title = Name });

        config.DescribeAllEnumsAsStrings();

        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        config.IncludeXmlComments(xmlPath);
    }
}

By doing this one can move all related configuration of a feature into a single place and also nicely distinguish between the individual configuration steps (e.g. SwaggerUIConfig vs SwaggerGenConfig).

Afterwards one can tidy up the Startup class by invoking the respective class methods:

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddMvc(MvcConfig.AddFilters)
        .AddJsonOptions(MvcConfig.JsonOptions);

    services.AddSwaggerGen(SwaggerConfig.SwaggerGenConfig);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app .UseSwagger()
        .UseSwaggerUI(SwaggerConfig.SwaggerUIConfig)
        .UseMvc();
}

Tip 5: Extension methods for conditional configurations

Another common use case is to configure different features based on the current environment or other conditional cases:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
}

A neat trick which I like to apply here is to implement an extension method for conditional configurations:

public static class ApplicationBuilderExtensions
{
    public static IApplicationBuilder When(
        this IApplicationBuilder builder,
        bool predicate,
        Func<IApplicationBuilder> compose) => predicate ? compose() : builder;
}

The When extension method will invoke a compose function only if a given predicate is true.

Now with the When method someone can set up conditional middleware in a much nicer and fluent way:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app .When(env.IsDevelopment(), app.UseDeveloperExceptionPage)
        .When(!env.IsDevelopment(), app.UseHsts)
        .UseSwagger()
        .UseSwaggerUI(SwaggerConfig.SwaggerUIConfig)
        .UseMvc();
}

Exit scenarios

Tip 6: Don't forget to return a default 404 response

Don't forget to register a middleware which will return a 404 Not Found HTTP response if no other middleware was able to deal with an incoming request:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app .When(env.IsDevelopment(), app.UseDeveloperExceptionPage)
        .When(!env.IsDevelopment(), app.UseHsts)
        .UseSwagger()
        .UseSwaggerUI(SwaggerConfig.SwaggerUIConfig)
        .UseMvc()
        .Run(NotFoundHandler);
}

private readonly RequestDelegate NotFoundHandler =
    async ctx =>
    {
        ctx.Response.StatusCode = 404;
        await ctx.Response.WriteAsync("Page not found.");
    };

If you don't do this then a request which couldn't be matched by any middleware will be left unhandled (unless you have another web server sitting behind Kestrel).

Tip 7: Return non zero exit code on failure

Return a non zero exit code when the application terminates with an error. This will allow parent processes to pick up the fact that the application terminated unexpectedly and give them a chance to handle such a situation more gracefully (e.g. when your ASP.NET Core application is run from a Kubernetes cluster):

try
{
    // Start WebHost

    return 0;
}
catch (Exception ex)
{
    Log.Fatal(ex, "Host terminated unexpectedly.");
    return -1;
}
finally
{
    Log.CloseAndFlush();
}

Error Handling

Every ASP.NET Core application is likely going to have to deal with at least three types of errors:

  • Server errors
  • Client errors
  • Business logic errors

Server errors are unexpected exceptions which get thrown by an application. Normally these exceptions bubble up to a global error handler which will log the exception and return a 500 Internal Server Error response to the client.

Client errors are mistakes which a client can make when sending a request to the server. These normally include things like missing or wrong authentication data, badly formatted request bodies, calling endpoints which do not exist or perhaps sending data in an unsupported format. Most of these errors will get picked up by a built-in ASP.NET Core feature which will return a corresponding 4xx HTTP error back to the client.

Business logic errors are application specific errors which are not handled by ASP.NET Core by default because they are very unique to each individual application. For example an invoicing application might want to throw an exception when a customer tries to raise an invoice with an unsupported currency whereas an online gaming application might want to throw an error when a user ran out of credits.

These errors are often raised from lower level domain code and might want to return a specific 4xx or 5xx HTTP response back to the client.

Tip 8: Create a base exception type for domain errors

Create a base exception class for business or domain errors and additional exception classes which derive from the base class for all possible error cases:

public enum DomainErrorCode
{
    InsufficientCredits = 1000
}

public class DomainException : Exception
{
    public readonly DomainErrorCode ErrorCode;

    public DomainException(DomainErrorCode code, string message) : base(message)
    {
        ErrorCode = code;
    }
}

public class InsufficientCreditsException : DomainException
{
    public InsufficientCreditsException()
        : base(DomainErrorCode.InsufficientCredits,
                "User ran out of free credit. Please upgrade your plan to continue using our service.")
    { }
}

Include a unique DomainErrorCode for each custom exception type which later can be used to identify the specific error case from higher level code.

Afterwards one can use the newly created exception classes to throw more meaningful errors from inside the domain layer:

throw new InsufficientCreditsException();

This has now the benefit that the ASP.NET Core application can look for domain exceptions from a central point (e.g. custom error middleware) and handle them accordingly:

public class DomainErrorHandlerMiddleware
{
    private readonly RequestDelegate _next;

    public DomainErrorHandlerMiddleware(RequestDelegate next)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public async Task InvokeAsync(HttpContext ctx)
    {
        try
        {
            await _next(ctx);
        }
        catch(DomainException ex)
        {
            ctx.Response.StatusCode = 422;
            await ctx.Response.WriteAsync($"{ex.ErrorCode}: {ex.Message}");
        }
    }
}

Because every domain exception includes a unique DomainErrorCode the generic error handler can even implement a slightly different response based on the given domain error.

This architecture has a few benefits:

  • The domain layer can throw meaningful exceptions
  • The domain layer works nicely with the higher level web layer without tight coupling
  • Domain exceptions are clearly distinguishable from other errors
  • Domain exceptions are self documenting
  • The web layer can handle all domain errors in a unified way without having to replicate the same try-catch block across multiple controllers
  • The additional error code in the response can be parsed and understood by third party clients
  • The custom exception types can be easily documented through Swagger

Tip 9: Expose an endpoint which returns all error codes

When you followed tip 8 and implemented a custom exception type with a unique error code for each error case then it can be extremely handy to expose all possible error codes through a single API endpoint. This will allow third party clients to quickly retrieve a list of the latest possible error codes and their meaning:

[HttpGet("/error-codes")]
public ActionResult<IDictionary<int, string>> ErrorCodes()
{
    var values = Enum
        .GetValues(typeof(DomainErrorCode))
        .Cast<DomainErrorCode>();

    var result = new Dictionary<int, string>();

    foreach(var v in values)
        result.Add((int)v, v.ToString());

    return result;
}

Other Tips & Tricks

Tip 10: Expose a version endpoint

Another really useful thing to have in an API (or website) is a version endpoint. Often it can be extremely helpful to customer support staff, QA or other members of a team to quickly be able to establish what version of an application is being deployed to an environment.

This version is different than the customer facing API version which often only includes the major version number (e.g. https://my-api.com/v3/some/resource).

Exposing an endpoint which displays the current application version and the build date and time is a nice way of quickly making this information accessible to relevant people:

[HttpGet("/info")]
public ActionResult<string> Info()
{
    var assembly = typeof(Startup).Assembly;

    var creationDate = File.GetCreationTime(assembly.Location);
    var version = FileVersionInfo.GetVersionInfo(assembly.Location).ProductVersion;

    return Ok($"Version: {version}, Last Updated: {creationDate}");
}

Tip 11: Remove the 'Server' HTTP header

Whilst one is at configuring their ASP.NET Core application they might as well remove the Server HTTP header from every HTTP response by deactivating that setting in Kestrel:

.UseKestrel(k => k.AddServerHeader = false)

Tip 12: Working with Null Collections

My last tip on this list is not specific to ASP.NET Core but all of .NET Core development where a collection or IEnumerable type is being used.

How often do .NET developers write something like this:

var someCollection = GetSomeCollectionFromSomewhere();

if (someCollection != null && someCollection.Count > 0)
{
    foreach(var item in someCollection)
    {
        // Do stuff
    }
}

Adding a one line extension method can massively simplify the above code across an entire applicatoin:

public static class EnumerableExtensions
{
    public static IEnumerable<T> OrEmptyIfNull<T>(this IEnumerable<T> source) =>
        source ?? Enumerable.Empty<T>();
}

Now the above if statement can be reduced to a single loop like this:

var someCollection = GetSomeCollectionFromSomewhere();

foreach(var item in someCollection.OrEmptyIfNull())
{
    // Do stuff
}

Or converting the IEnumerbale to an IList and use the ForEach LINQ extension method to turn this into a one liner:

someCollection.OrEmptyIfNull().ToList().ForEach(i => i.DoSomething());

What tips and tricks do you have?

So this is it, this was my brief post on some tips and tricks which I like to apply in my personal ASP.NET Core development. I hope this was at least somewhat useful to someone?! Let me know what you think and please feel free to share your own tips and tricks which make your ASP.NET Core development life easier in the comments below!