Middleware

Middleware is software that's assembled into an app pipeline to handle requests and responses. Each component performs operations on an HttpContext and:

  • Chooses whether to pass the request to the next component in the pipeline.

  • Can perform work before and after the next component in the pipeline.

Request delegates are used to build the request pipeline. The request delegates handle each HTTP request. Request delegates are configured using Run, Map, and Use extension methods.

An individual request delegate can be specified in-line as an anonymous method (called in-line middleware), or it can be defined in a reusable class. These reusable classes and in-line anonymous methods are middleware, also called middleware components. Each middleware component in the request pipeline is responsible for invoking the next component in the pipeline or short-circuiting the pipeline. When a middleware short-circuits, it's called a terminal middleware because it prevents further middleware from processing the request.

The request handling pipeline is composed as a series of middleware components. Each component performs operations on an HttpContext and either invokes the next middleware in the pipeline or terminates the request.

Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseAuthorization();

app.MapGet("/hi", () => "Hello!");

app.MapDefaultControllerRoute();
app.MapRazorPages();

app.Run();
  • Chain multiple request delegates together with Use. The next parameter represents the next delegate in the pipeline. You can short-circuit the pipeline by not calling the next parameter.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) => {
    // Do work that can write to the Response.
    await next.Invoke();
    // Do logging or other work that doesn't write to the Response.
});

app.Run();
  • When a delegate doesn't pass a request to the next delegate, it's called short-circuiting the request pipeline. Short-circuiting is often desirable because it avoids unnecessary work. For example, Static File Middleware can act as a terminal middleware by processing a request for a static file and short-circuiting the rest of the pipeline.

If you don't call app.UseRouting, the Routing middleware runs at the beginning of the pipeline by default.

The Endpoint middleware in the preceding diagram executes the filter pipeline for the corresponding app type—MVC or Razor Pages.

  • Map extensions are used as a convention for branching the pipeline. Map branches the request pipeline based on matches of the given request path. If the request path starts with the given path, the branch is executed.

    • Map supports nesting.

    • Map can also be used to match multiple segments at once.

    • MapWhen branches the request pipeline based on the result of the given predicate.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Map("/map1", HandleMapTest1);
app.Map("/level1", level1App => {
    level1App.Map("/level2a", level2AApp => {
        // "/level1/level2a" processing
    });
    level1App.Map("/level2b", level2BApp => {
        // "/level1/level2b" processing
    });
});
app.Map("/map1/seg1", HandleMultiSeg);
app.MapWhen(context => context.Request.Query.ContainsKey("branch"), HandleBranch);

app.Run(async context => {
    await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
});

app.Run();

static void HandleMapTest1(IApplicationBuilder app) {
    app.Run(async context => { 
        await context.Response.WriteAsync("Map Test"); 
    });
}

static void HandleMultiSeg(IApplicationBuilder app) {
    app.Run(async context => { 
        await context.Response.WriteAsync("Multiple Segment Test"); 
    });
}

static void HandleBranch(IApplicationBuilder app) {
    app.Run(async context => {
        var branchVer = context.Request.Query["branch"];
        await context.Response.WriteAsync($"Branch used = {branchVer}");
    });
}

The following table shows the requests and responses from http://localhost:1234 using the preceding code.

RequestResponse

localhost:1234

Hello from non-Map delegate.

localhost:1234/map1

Map Test

localhost:1234/map1/seg1

Multiple Segment Test

localhost:1234/?branch=main

Branch used = main

localhost:1234/map3

Hello from non-Map delegate.

When Map is used, the matched path segments are removed from HttpRequest.Path and appended to HttpRequest.PathBase for each request.

  • Run delegates don't receive a next parameter. The first Run delegate is always terminal and terminates the pipeline. Some middleware components may expose Run[Middleware] methods that run at the end of the pipeline:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();

app.Run(async context => {
    await context.Response.WriteAsync("Hello from 2nd delegate.");
});

app.Run();
  • If another Use or Run delegate is added after the Run delegate, it's not called.

Test Middleware

Middleware can be tested in isolation with TestServer. It allows you to:

  • Instantiate an app pipeline containing only the components that you need to test.

  • Send custom requests to verify middleware behavior.

[Fact]
public async Task MiddlewareTest_ReturnsNotFoundForRequest() {
    // Arrange
    using var host = await new HostBuilder()
        .ConfigureWebHost(webBuilder => {
            webBuilder
                .UseTestServer()
                .ConfigureServices(services => {
                    services.AddMyServices();
                })
                .Configure(app => {
                    app.UseMiddleware<MyMiddleware>();
                });
        })
        .StartAsync();
    
    // Action
    // send a request using HttpClient
    var response = await host.GetTestClient().GetAsync("/");

    // send a request using HttpContext    
    var server = host.GetTestServer();
    server.BaseAddress = new Uri("https://example.com/A/Path/");

    var context = await server.SendAsync(c =>
    {
        c.Request.Method = HttpMethods.Post;
        c.Request.Path = "/and/file.txt";
        c.Request.QueryString = new QueryString("?and=query");
    });
    
    // Assert
    Assert.NotEqual(HttpStatusCode.NotFound, response.StatusCode);
    Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    
    Assert.True(context.RequestAborted.CanBeCanceled);
    Assert.Equal(HttpProtocol.Http11, context.Request.Protocol);
    Assert.Equal("POST", context.Request.Method);
    Assert.Equal("https", context.Request.Scheme);
    Assert.Equal("example.com", context.Request.Host.Value);
    Assert.Equal("/A/Path", context.Request.PathBase.Value);
    Assert.Equal("/and/file.txt", context.Request.Path.Value);
    Assert.Equal("?and=query", context.Request.QueryString.Value);
    Assert.NotNull(context.Request.Body);
    Assert.NotNull(context.Request.Headers);
    Assert.NotNull(context.Response.Headers);
    Assert.NotNull(context.Response.Body);
    Assert.Equal(404, context.Response.StatusCode);
    Assert.Null(context.Features.Get<IHttpResponseFeature>().ReasonPhrase);
    
}

Custom Middleware

Creating a middleware component by calling Microsoft.AspNetCore.Builder.UseExtensions.Use. The Use extension method adds a middleware delegate defined in-line to the application's request pipeline.

There are two overloads available for the Use extension:

  • One takes a HttpContext and a Func<Task>. Invoke the Func<Task> without any parameters.

  • The other takes a HttpContext and a RequestDelegate. Invoke the RequestDelegate by passing the HttpContext.

using System.Globalization;

namespace Middleware.Example;

public class RequestCultureMiddleware {
    private readonly RequestDelegate _next;

    // A public constructor with a parameter of type RequestDelegate.
    public RequestCultureMiddleware(RequestDelegate next) {
        _next = next;
    }

    /*
        A public method named Invoke/InvokeAsync. This method must:
            - Return a Task.
            - Accept a first parameter of type HttpContext.
    */
    public async Task InvokeAsync(HttpContext context) {
        var cultureQuery = context.Request.Query["culture"];
        if (!string.IsNullOrWhiteSpace(cultureQuery)) {
            var culture = new CultureInfo(cultureQuery);
            CultureInfo.CurrentCulture = culture;
            CultureInfo.CurrentUICulture = culture;
        }

        // Call the next delegate/middleware in the pipeline.
        await _next(context);
    }
}

// exposing the middleware through IApplicationBuilder
public static class RequestCultureMiddlewareExtensions {
    public static IApplicationBuilder UseRequestCulture(
        this IApplicationBuilder builder) {
        return builder.UseMiddleware<RequestCultureMiddleware>();
    }
}

Last updated