Migrate HTTP modules to ASP.NET Core middleware

This article shows how to migrate existing ASP.NET HTTP modules from system.webserver to ASP.NET Core middleware.

Modules revisited

Before proceeding to ASP.NET Core middleware, let's first recap how HTTP modules work:

Modules Handler

Modules are:

  • Classes that implement IHttpModule

  • Invoked for every request

  • Able to short-circuit (stop further processing of a request)

  • Able to add to the HTTP response, or create their own

  • Configured in Web.config

The order in which modules process incoming requests is determined by:

  1. A series events fired by ASP.NET, such as BeginRequest and AuthenticateRequest. For a complete list, see System.Web.HttpApplication. Each module can create a handler for one or more events.

  2. For the same event, the order in which they're configured in Web.config.

In addition to modules, you can add handlers for the life cycle events to your Global.asax.cs file. These handlers run after the handlers in the configured modules.

From modules to middleware

Middleware are simpler than HTTP modules:

  • Modules, Global.asax.cs, Web.config (except for IIS configuration) and the application life cycle are gone

  • The roles of modules have been taken over by middleware

  • Middleware are configured using code rather than in Web.config

  • Pipeline branching lets you send requests to specific middleware, based on not only the URL but also on request headers, query strings, etc.
  • Pipeline branching lets you send requests to specific middleware, based on not only the URL but also on request headers, query strings, etc.

Middleware are very similar to modules:

Middleware and modules are processed in a different order:

Authorization Middleware short-circuits a request for a user who isn't authorized. A request for the Index page is permitted and processed by MVC Middleware. A request for a sales report is permitted and processed by a custom report Middleware.

Note how in the image above, the authentication middleware short-circuited the request.

Migrating module code to middleware

An existing HTTP module will look similar to this:

// ASP.NET 4 module

using System;
using System.Web;

namespace MyApp.Modules
{
    public class MyModule : IHttpModule
    {
        public void Dispose()
        {
        }

        public void Init(HttpApplication application)
        {
            application.BeginRequest += (new EventHandler(this.Application_BeginRequest));
            application.EndRequest += (new EventHandler(this.Application_EndRequest));
        }

        private void Application_BeginRequest(Object source, EventArgs e)
        {
            HttpContext context = ((HttpApplication)source).Context;

            // Do something with context near the beginning of request processing.
        }

        private void Application_EndRequest(Object source, EventArgs e)
        {
            HttpContext context = ((HttpApplication)source).Context;

            // Do something with context near the end of request processing.
        }
    }
}

As shown in the Middleware page, an ASP.NET Core middleware is a class that exposes an Invoke method taking an HttpContext and returning a Task. Your new middleware will look like this:

// ASP.NET Core middleware

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Threading.Tasks;

namespace MyApp.Middleware
{
    public class MyMiddleware
    {
        private readonly RequestDelegate _next;

        public MyMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            // Do something with context near the beginning of request processing.

            await _next.Invoke(context);

            // Clean up.
        }
    }

    public static class MyMiddlewareExtensions
    {
        public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<MyMiddleware>();
        }
    }
}

The preceding middleware template was taken from the section on writing middleware.

The MyMiddlewareExtensions helper class makes it easier to configure your middleware in your Startup class. The UseMyMiddleware method adds your middleware class to the request pipeline. Services required by the middleware get injected in the middleware's constructor.

Your module might terminate a request, for example if the user isn't authorized:

// ASP.NET 4 module that may terminate the request

private void Application_BeginRequest(Object source, EventArgs e)
{
    HttpContext context = ((HttpApplication)source).Context;

    // Do something with context near the beginning of request processing.

    if (TerminateRequest())
    {
        context.Response.End();
        return;
    }
}

A middleware handles this by not calling Invoke on the next middleware in the pipeline. Keep in mind that this doesn't fully terminate the request, because previous middlewares will still be invoked when the response makes its way back through the pipeline.

// ASP.NET Core middleware that may terminate the request

public async Task Invoke(HttpContext context)
{
    // Do something with context near the beginning of request processing.

    if (!TerminateRequest())
        await _next.Invoke(context);

    // Clean up.
}

When you migrate your module's functionality to your new middleware, you may find that your code doesn't compile because the HttpContext class has significantly changed in ASP.NET Core. See Migrate from ASP.NET Framework HttpContext to ASP.NET Core to learn how to migrate to the new ASP.NET Core HttpContext.

Migrating module insertion into the request pipeline

HTTP modules are typically added to the request pipeline using Web.config:

<?xml version="1.0" encoding="utf-8"?>
<!--ASP.NET 4 web.config-->
<configuration>
  <system.webServer>
    <modules>
      <add name="MyModule" type="MyApp.Modules.MyModule"/>
    </modules>
  </system.webServer>
</configuration>

Convert this by adding your new middleware to the request pipeline in your Startup class:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseMyMiddleware();

    app.UseMyMiddlewareWithParams();

    var myMiddlewareOptions = Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>();
    var myMiddlewareOptions2 = Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>();
    app.UseMyMiddlewareWithParams(myMiddlewareOptions);
    app.UseMyMiddlewareWithParams(myMiddlewareOptions2);

    app.UseMyTerminatingMiddleware();

    // Create branch to the MyHandlerMiddleware. 
    // All requests ending in .report will follow this branch.
    app.MapWhen(
        context => context.Request.Path.ToString().EndsWith(".report"),
        appBranch => {
            // ... optionally add more middleware to this branch
            appBranch.UseMyHandler();
        });

    app.MapWhen(
        context => context.Request.Path.ToString().EndsWith(".context"),
        appBranch => {
            appBranch.UseHttpContextDemoMiddleware();
        });

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

The exact spot in the pipeline where you insert your new middleware depends on the event that it handled as a module (BeginRequest, EndRequest, etc.) and its order in your list of modules in Web.config.

As previously stated, there's no application life cycle in ASP.NET Core and the order in which responses are processed by middleware differs from the order used by modules. This could make your ordering decision more challenging.

If ordering becomes a problem, you could split your module into multiple middleware components that can be ordered independently.

Loading middleware options using the options pattern

Some modules have configuration options that are stored in Web.config. However, in ASP.NET Core a new configuration model is used in place of Web.config.

The new configuration system gives you these options to solve this:

  1. Create a class to hold your middleware options, for example:

    public class MyMiddlewareOptions
    {
        public string Param1 { get; set; }
        public string Param2 { get; set; }
    }
    
  2. Store the option values

    The configuration system allows you to store option values anywhere you want. However, most sites use appsettings.json, so we'll take that approach:

    {
      "MyMiddlewareOptionsSection": {
        "Param1": "Param1Value",
        "Param2": "Param2Value"
      }
    }
    

    MyMiddlewareOptionsSection here is a section name. It doesn't have to be the same as the name of your options class.

  3. Associate the option values with the options class

    The options pattern uses ASP.NET Core's dependency injection framework to associate the options type (such as MyMiddlewareOptions) with a MyMiddlewareOptions object that has the actual options.

    Update your Startup class:

    1. If you're using appsettings.json, add it to the configuration builder in the Startup constructor:

      public Startup(IHostingEnvironment env)
      {
          var builder = new ConfigurationBuilder()
              .SetBasePath(env.ContentRootPath)
              .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
              .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
              .AddEnvironmentVariables();
          Configuration = builder.Build();
      }
      
    2. Configure the options service:

      public void ConfigureServices(IServiceCollection services)
      {
          // Setup options service
          services.AddOptions();
      
          // Load options from section "MyMiddlewareOptionsSection"
          services.Configure<MyMiddlewareOptions>(
              Configuration.GetSection("MyMiddlewareOptionsSection"));
      
          // Add framework services.
          services.AddMvc();
      }
      
    3. Associate your options with your options class:

      public void ConfigureServices(IServiceCollection services)
      {
          // Setup options service
          services.AddOptions();
      
          // Load options from section "MyMiddlewareOptionsSection"
          services.Configure<MyMiddlewareOptions>(
              Configuration.GetSection("MyMiddlewareOptionsSection"));
      
          // Add framework services.
          services.AddMvc();
      }
      
  4. Inject the options into your middleware constructor. This is similar to injecting options into a controller.

    public class MyMiddlewareWithParams
    {
        private readonly RequestDelegate _next;
        private readonly MyMiddlewareOptions _myMiddlewareOptions;
    
        public MyMiddlewareWithParams(RequestDelegate next,
            IOptions<MyMiddlewareOptions> optionsAccessor)
        {
            _next = next;
            _myMiddlewareOptions = optionsAccessor.Value;
        }
    
        public async Task Invoke(HttpContext context)
        {
            // Do something with context near the beginning of request processing
            // using configuration in _myMiddlewareOptions
    
            await _next.Invoke(context);
    
            // Do something with context near the end of request processing
            // using configuration in _myMiddlewareOptions
        }
    }
    

    The UseMiddleware extension method that adds your middleware to the IApplicationBuilder takes care of dependency injection.

    This isn't limited to IOptions objects. Any other object that your middleware requires can be injected this way.

Loading middleware options through direct injection

The options pattern has the advantage that it creates loose coupling between options values and their consumers. Once you've associated an options class with the actual options values, any other class can get access to the options through the dependency injection framework. There's no need to pass around options values.

This breaks down though if you want to use the same middleware twice, with different options. For example an authorization middleware used in different branches allowing different roles. You can't associate two different options objects with the one options class.

The solution is to get the options objects with the actual options values in your Startup class and pass those directly to each instance of your middleware.

  1. Add a second key to appsettings.json

    To add a second set of options to the appsettings.json file, use a new key to uniquely identify it:

    {
      "MyMiddlewareOptionsSection2": {
        "Param1": "Param1Value2",
        "Param2": "Param2Value2"
      },
      "MyMiddlewareOptionsSection": {
        "Param1": "Param1Value",
        "Param2": "Param2Value"
      }
    }
    
  2. Retrieve options values and pass them to middleware. The Use... extension method (which adds your middleware to the pipeline) is a logical place to pass in the option values:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();
    
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBrowserLink();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }
    
        app.UseMyMiddleware();
    
        app.UseMyMiddlewareWithParams();
    
        var myMiddlewareOptions = Configuration.GetSection("MyMiddlewareOptionsSection").Get<MyMiddlewareOptions>();
        var myMiddlewareOptions2 = Configuration.GetSection("MyMiddlewareOptionsSection2").Get<MyMiddlewareOptions>();
        app.UseMyMiddlewareWithParams(myMiddlewareOptions);
        app.UseMyMiddlewareWithParams(myMiddlewareOptions2);
    
        app.UseMyTerminatingMiddleware();
    
        // Create branch to the MyHandlerMiddleware. 
        // All requests ending in .report will follow this branch.
        app.MapWhen(
            context => context.Request.Path.ToString().EndsWith(".report"),
            appBranch => {
                // ... optionally add more middleware to this branch
                appBranch.UseMyHandler();
            });
    
        app.MapWhen(
            context => context.Request.Path.ToString().EndsWith(".context"),
            appBranch => {
                appBranch.UseHttpContextDemoMiddleware();
            });
    
        app.UseStaticFiles();
    
        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }
    
  3. Enable middleware to take an options parameter. Provide an overload of the Use... extension method (that takes the options parameter and passes it to UseMiddleware). When UseMiddleware is called with parameters, it passes the parameters to your middleware constructor when it instantiates the middleware object.

    public static class MyMiddlewareWithParamsExtensions
    {
        public static IApplicationBuilder UseMyMiddlewareWithParams(
            this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<MyMiddlewareWithParams>();
        }
    
        public static IApplicationBuilder UseMyMiddlewareWithParams(
            this IApplicationBuilder builder, MyMiddlewareOptions myMiddlewareOptions)
        {
            return builder.UseMiddleware<MyMiddlewareWithParams>(
                new OptionsWrapper<MyMiddlewareOptions>(myMiddlewareOptions));
        }
    }
    

    Note how this wraps the options object in an OptionsWrapper object. This implements IOptions, as expected by the middleware constructor.

Incremental IHttpModule migration

There are times when converting modules to middleware cannot easily be done. In order to support migration scenarios in which modules are required and cannot be moved to middleware, System.Web adapters support adding them to ASP.NET Core.

IHttpModule Example

In order to support modules, an instance of HttpApplication must be available. If no custom HttpApplication is used, a default one will be used to add the modules to. Events declared in a custom application (including Application_Start) will be registered and run accordingly.

using System.Web;
using Microsoft.AspNetCore.OutputCaching;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSystemWebAdapters()
    .AddHttpApplication<MyApp>(options =>
    {
        // Size of pool for HttpApplication instances. Should be what the expected concurrent requests will be
        options.PoolSize = 10;

        // Register a module (optionally) by name
        options.RegisterModule<MyModule>("MyModule");
    });

// Only available in .NET 7+
builder.Services.AddOutputCache(options =>
{
    options.AddHttpApplicationBasePolicy(_ => new[] { "browser" });
});

builder.Services.AddAuthentication();
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthenticationEvents();

app.UseAuthorization();
app.UseAuthorizationEvents();

app.UseSystemWebAdapters();
app.UseOutputCache();

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

app.Run();

class MyApp : HttpApplication
{
    protected void Application_Start()
    {
    }

    public override string? GetVaryByCustomString(System.Web.HttpContext context, string custom)
    {
        // Any custom vary-by string needed

        return base.GetVaryByCustomString(context, custom);
    }
}

class MyModule : IHttpModule
{
    public void Init(HttpApplication application)
    {
        application.BeginRequest += (s, e) =>
        {
            // Handle events at the beginning of a request
        };

        application.AuthorizeRequest += (s, e) =>
        {
            // Handle events that need to be authorized
        };
    }

    public void Dispose()
    {
    }
}

Global.asax migration

This infrastructure can be used to migrate usage of Global.asax if needed. The source from Global.asax is a custom HttpApplication and the file can be included in an ASP.NET Core application. Since it is named Global, the following code can be used to register it:

builder.Services.AddSystemWebAdapters()
    .AddHttpApplication<Global>();

As long as the logic within it is available in ASP.NET Core, this approach can be used to incrementally migrate reliance on Global.asax to ASP.NET Core.

Authentication/Authorization events

In order for the authentication and authorization events to run at the desired time, the following pattern should be used:

app.UseAuthentication();
app.UseAuthenticationEvents();

app.UseAuthorization();
app.UseAuthorizationEvents();

If this is not done, the events will still run. However, it will be during the call of .UseSystemWebAdapters().

HTTP Module pooling

Because modules and applications in ASP.NET Framework were assigned to a request, a new instance is needed for each request. However, since they can be expensive to create, they are pooled using ObjectPool<T>. In order to customize the actual lifetime of the HttpApplication instances, a custom pool can be used:

builder.Services.TryAddSingleton<ObjectPool<HttpApplication>>(sp =>
{
    // Recommended to use the in-built policy as that will ensure everything is initialized correctly and is not intended to be replaced
    var policy = sp.GetRequiredService<IPooledObjectPolicy<HttpApplication>>();

    // Can use any provider needed
    var provider = new DefaultObjectPoolProvider();

    // Use the provider to create a custom pool that will then be used for the application.
    return provider.Create(policy);
});

Additional resources