What is middleware?

Simply, middleware is code that becomes part of a pipeline to handle requests and responses in a web app. In ASP.NET Core, middleware is a big deal! All requests are handled by a pipeline of middleware.

Middleware Flow

If you’ve created an ASP.NET Core app before, you are probably aware of the UseDeveloperExceptionPage middleware. This middleware adds special exception handling. If an exception is thrown later in the pipeline, the exception bubbles up to this middleware, which returns a nice page with details as a response to the request. Because middleware is a pipeline, order matters. For the developer exception page, it’s important that it is one of the first middleware added to the pipeline.

Find the code for this post on GitHub!

Getting started

For this guide, I’m starting with a project created using dotnet new web. This command creates an ASP.NET Core project with a minimum amount of boilerplate.

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

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Hello World!");
    });
}

Even in this most basic project, we already have two middleware components: UseDeveloperExceptionPage and Run. We already know about UseDeveloperExceptionPage. Run is a terminal middleware. A terminal middleware stops processing additional middleware for the request.

Use is a another middleware we can use. It is not necessarily a terminal middleware, like Run. It can be terminal or non-terminal depending on whether the next middleware is called. Use will be how we write our first custom middleware.

Custom Middleware

Let’s add some custom middleware to our Startup.Configure method.

- public void Configure(IApplicationBuilder app, IHostingEnvironment env)
+ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILogger<Startup> logger)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

+    app.Use(async (context, next) =>
+    {
+        logger.LogInformation("Request started");
+        await next.Invoke();
+        logger.LogInformation("Request finished");
+    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("Hello World!");
    });
}

We added a logger and an app.Use section. This is always a non-terminal middleware, because we call next.Invoke for all code paths within the middleware. To try this out, run dotnet run and navigate to http://localhost:5000. You should see Hello World! on the page, and something like this in the console:

info: CreatingMiddleware.Startup[0]
      Request started
info: CreatingMiddleware.Startup[0]
      Request finished

Enhance!

So we’ve created a super simple custom middleware that logs when a request is started and completed. Cool… but what else can we do? Let’s make some changes to our app.Use block…

app.Use(async (context, next) =>
{
    if (context.Request.Path.ToString().StartsWith("/test"))
    {
        await context.Response.WriteAsync("Welcome to Test");
    }
    else
    {
        await next.Invoke();
    }
});

Now, our middleware is terminal when the request path starts with /test, returning the response Welcome to Test. To verify, navigate to http://localhost:5000/test. For routes that don’t start with /test, we just pass-through to the next middleware.

You’ve likely noticed that we have access to the entire HttpContext in our middleware. We can check whether the user is authenticated, pull information out of the route, and read the body of the request. Of course, those are just some examples of the information we can get from HttpContext.

That’s All for Now

Middleware is a pretty deep rabbit-hole, so I’m going to call this post here. I have more to write about, but that will have to come in later posts - I don’t want to overload post with everything. Enter your email below so you know when Part 2 comes out!