Middlewares in .NET Core

Middlewares in .NET Core

In this post we take a look at OWIN, middlewares in .NET Core, create our own middleware.

·

5 min read

Modern web applications, among many other things, demand

  1. a modular approach to building components
  2. loose-coupling between its components.

Pre .NET Core, all types of web applications in the .NET ecosystem (MVC, WebAPI, etc) were heavily dependent on the System.Web library which in turn was tightly coupled to IIS. The library tried to do too many things, did a great deal of heavy lifting behind the scenes, most of which would end up being redundant, unwanted, or unchangeable.

Enter "Open Web Interface for .NET" a.k.a. OWIN

From their website

OWIN defines a standard interface between .NET web servers and web applications. The goal of the OWIN interface is to decouple server and application, encourage the development of simple modules for .NET web development, and, by being an open standard, stimulate the open source ecosystem of .NET web development tools.

OWIN specification helps create a decoupled layer that allows two frameworks with different object models to work together. Decoupling components would mean iterations would be independent, a plug-n-play option would enable small, lightweight, high-performance hosting of applications.

.NET Core fully supports OWIN specification, this allows .NET Core to be hosted on top of a OWIN compatible server/host or for other OWIN compatible components to run on top of .NET Core.

The specification also introduces something called middlewares. We will take a look at middlewares in .NET Core, the middleware pipeline, and leverage that to create a middleware of our own.

What is a middleware?

From OWIN specification

Middleware — Pass through components that form a pipeline between a server and application to inspect, route, or modify request and response messages for a specific purpose.

Middleware is a code-block/logic/component that is assembled into an application pipeline to handle requests and responses. Each component

  1. Chooses whether to pass the request to the next component in the pipeline.
  2. Can perform work before and after the next component in the pipeline.

image.png

Middlewares in .NET Core

Middlewares in .NET Core are setup inside the Startup class. There are different ways to set them up

  1. Inline request delegates(which are middlewares) using Use, Run, Map
  2. Custom middleware class

Use
Helps chain multiple request delegates. The next parameter represents the next delegate in the pipeline.

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            // Do some work before calling the next middleware
            await next.Invoke();
            // Do some work after the next middleware execution completes
        });
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

Run
Helps short-circuit the pipeline. Nothing gets executed beyond this.

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Allo!");
        });
        // nothing beyond this gets executed
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

Map
Helps with conditional branching in the pipeline. Runs middlewares when conditions are met.

public class Startup
{
    private static void HandleMyRoute(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("I was forced here.");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        // a request with path "/myroute" will branch out
        app.Map("/myroute", HandleMyRoute);
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

Custom middleware class
A cleaner approach, we write our middleware logic in a separate class and then use it in our pipeline using the UseMiddleware extension. The invoke method(which takes in the HtttpContext) is important here and this is where we have our middleware logic.

public class MyCustomMiddleware
{
    private readonly RequestDelegate _next;
    public MyCustomMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task Invoke(HttpContext httpContext)
    {
        // Do some work before calling the next middleware  
        await next(httpContext);
        // Do some work after the next middleware execution completes
    }
}

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        // after authorization run MyCustomMiddleware
        app.UseMiddleware<MyCustomMiddleware>();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

Our very own custom middleware

Let's create a custom middleware class of our own, use it in our middleware pipeline and see it in action.

Middleware class
Our middleware logic will catch unhandled exceptions, wrap them into a problemdetails object before sending back the response to the client.

public class CustomExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly bool _seeStackTrace;

    public CustomExceptionMiddleware(RequestDelegate next, bool seeStackTrace = false)
    {
        _next = next;
        _seeStackTrace = seeStackTrace;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        try
        {
            await next(httpContext);
        }
        catch(Exception ex)
        {
            await HandleException(httpContext, ex);
        }
    }

    private async Task HandleException(HttpContext httpContext, Exception ex)
    {
        string details;
        if(_seeStackTrace)
        {
            details = ex.ToString();
        }
        else
        {
            details = "Exception was caught inside custom exception middleware.";
        }
        var responseProblemDetails = new ProblemDetails
        {
            Title = ex.Message,
            Detail = details,
            Type = "https://httpstatuses.com/" + (int)HttpStatusCode.InternalServerError
        };
        httpContext.Response.ContentType = "application/json";
        httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        await httpContext.Response.WriteAsync(JsonSerializer.Serialize(responseProblemDetails));
    }
}

Startup class
We will place the middleware at the start of the middleware pipeline so that it acts as an umbrella for all uncaught exceptions. EVERYTHING GETS CAUGHT

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        // we place our middleware at the start 
        app.UseMiddleware<CustomExceptionMiddleware>();
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

Controller
We expose a GET endpoint and explicitly throw an exception.

public class HashnodeController : ControllerBase
{
        [HttpGet("~/hashnode")]
        public int GetHashnodePostCount()
        {
            throw new ApplicationException("You shall not pass!");
            return int.MaxValue;
        }
}

Done with our setup, now it's time to see it in action.
As soon as we hit our /hashnode GET endpoint, in the error response we should start seeing our custom message wrapped as a problemdetails object. image.png A pretty neat way to make sure all uncaught exceptions are handled appropriately. Also helps declutter code from too many try-catch blocks.

If you have made it till here, here's a cookie for you :) cookie

Thank you for going through this post. I hope you found it useful. Cheers!

Additional resources

  1. Read about OWIN specification.
  2. Also take a look at project Katana, which contains a set of OWIN components built by Microsoft.
  3. Read about problem details.