Freigeben über


Migrieren von HTTP-Modulen zu ASP.NET Core Middleware

In diesem Artikel wird gezeigt, wie Vorhandene ASP.NET HTTP-Module von system.webserver zu ASP.NET Core Middleware migriert werden.

Module überarbeitet

Bevor Sie mit ASP.NET Core Middleware fortfahren, schauen wir uns zunächst an, wie HTTP-Module funktionieren:

Module-Handler

Module sind:

  • Klassen, die IHttpModule implementieren

  • Für jede Anforderung aufgerufen

  • Fähigkeit, die Weiterverarbeitung einer Anforderung durch Kurzschließen zu stoppen

  • Sie können zur HTTP-Antwort hinzufügen oder eigene Antworten erstellen.

  • Konfiguriert in Web.config

Die Reihenfolge, in der Module eingehende Anforderungen verarbeiten, wird durch:

  1. Eine Reihe von Ereignissen, die von ASP.NET ausgelöst werden, wie z. B. BeginRequest und AuthenticateRequest. Eine vollständige Liste finden Sie unter System.Web.HttpApplication. Jedes Modul kann einen Handler für ein oder mehrere Ereignisse erstellen.

  2. Für dasselbe Ereignis die Reihenfolge, in der sie in Web.configkonfiguriert sind.

Zusätzlich zu Modulen können Sie Ihrer Datei Global.asax.cs Handler für Lebenszyklusereignisse hinzufügen. Diese Handler werden nach den Handlern in den konfigurierten Modulen ausgeführt.

Von Modulen zu Middleware

Middleware ist einfacher als HTTP-Module:

  • Module, Global.asax.csWeb.config (mit Ausnahme der IIS-Konfiguration) und der Anwendungslebenszyklus sind nicht mehr vorhanden.

  • Die Rollen von Modulen wurden von Middleware übernommen.

  • Middleware wird mithilfe von Code und nicht in Web.config konfiguriert.

  • Mithilfe der Pipeline-Verzweigung können Sie Anforderungen an bestimmte Middleware senden, die nicht nur auf der URL, sondern auch auf Anforderungsheadern, Abfragezeichenfolgen usw. basieren.
  • Mithilfe der Pipeline-Verzweigung können Sie Anforderungen an bestimmte Middleware senden, die nicht nur auf der URL, sondern auch auf Anforderungsheadern, Abfragezeichenfolgen usw. basieren.

Middleware ist ähnlich wie Module:

  • Grundsätzlich für jede Anfrage aufgerufen

  • Kann eine Anforderung abfangen, indem die Anforderung nicht an die nächste Middleware übergeben wird.

  • In der Lage, eine eigene HTTP-Antwort zu erstellen

Middleware und Module werden in einer anderen Reihenfolge verarbeitet:

  • Die Reihenfolge der Middleware basiert auf der Reihenfolge, in der sie in die Anforderungspipeline eingefügt werden, während die Reihenfolge der Module hauptsächlich auf System.Web.HttpApplication Ereignissen basiert.

  • Die Reihenfolge der Middleware für Antworten ist die Umgekehrte von dieser für Anforderungen, während die Reihenfolge der Module für Anforderungen und Antworten identisch ist.

  • Siehe Erstellen einer Middlewarepipeline mit IApplicationBuilder

Die Autorisierungs-Middleware schaltet eine Anfrage für einen nicht autorisierten Benutzer ab. Eine Anfrage für die Indexseite ist zulässig und wird von der MVC-Middleware bearbeitet. Eine Anfrage für einen Verkaufsbericht ist zulässig und wird von einer benutzerdefinierten Berichts-Middleware bearbeitet.

Beachten Sie, wie in der obigen Abbildung die Authentifizierungs-Middleware die Abfrage kurzschließt.

Migrieren von Modulcode zu Middleware

Ein vorhandenes HTTP-Modul sieht ähnlich wie folgt aus:

// 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.
        }
    }
}

Wie auf der Middleware-Seite gezeigt, ist eine ASP.NET Core Middleware eine Klasse, die eine Invoke-Methode bereitstellt, die ein HttpContext-Objekt entgegennimmt und ein Task zurückgibt. Ihre neue Middleware sieht wie folgt aus:

// 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>();
        }
    }
}

Die vorangehende Middleware-Vorlage wurde aus dem Abschnitt zum Schreiben von Middleware übernommen.

Die MyMiddlewareExtensions-Hilfsklasse erleichtert die Konfiguration Ihrer Middleware in Ihrer Startup Klasse. Die UseMyMiddleware Methode fügt der Anforderungspipeline Ihre Middleware-Klasse hinzu. Dienste, die von der Middleware benötigt werden, werden in den Konstruktor der Middleware eingefügt.

Ihr Modul kann eine Anforderung beenden, z. B. wenn der Benutzer nicht autorisiert ist:

// 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;
    }
}

Eine Middleware behandelt dies, indem sie die nächste Middleware in der Pipeline nicht aufruft Invoke. Denken Sie daran, dass die Anforderung dadurch nicht vollständig beendet wird, da vorherige Middleware weiterhin aufgerufen wird, wenn die Antwort wieder durch die Pipeline führt.

// 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.
}

Wenn Sie die Funktionalität Ihres Moduls zu Ihrer neuen Middleware migrieren, stellen Sie möglicherweise fest, dass Ihr Code nicht kompiliert wird, da sich die HttpContext Klasse in ASP.NET Core erheblich geändert hat. Informationen zum Migrieren von ASP.NET Framework HttpContext zu ASP.NET Core finden Sie unter Migrieren zum neuen ASP.NET Core HttpContext.

Migrieren der Moduleinfügung in die Anforderungspipeline

HTTP-Module werden in der Regel mithilfe vonWeb.configzur Anforderungspipeline hinzugefügt:

<?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>

Konvertieren Sie dies, indem Sie Ihre neue Middleware zur Anforderungspipeline in Ihrer Startup Klasse hinzufügen:

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?}");
    });
}

Die genaue Stelle in der Pipeline, an der Sie Ihre neue Middleware einfügen, hängt von dem Ereignis ab, das als Modul (BeginRequest, EndRequestusw.) behandelt wird, und deren Reihenfolge in Ihrer Liste der Module in Web.config.

Wie bereits erwähnt, gibt es keinen Anwendungslebenszyklus in ASP.NET Core und die Reihenfolge, in der Antworten von Middleware verarbeitet werden, unterscheidet sich von der Reihenfolge, die von Modulen verwendet wird. Dies könnte Ihre Sortierentscheidung schwieriger machen.

Wenn die Sortierung zu einem Problem wird, könnten Sie Ihr Modul in mehrere Middleware-Komponenten aufteilen, die unabhängig voneinander sortiert werden können.

Laden von Middleware-Optionen mithilfe des Optionsmusters

Einige Module verfügen über Konfigurationsoptionen, die in Web.configgespeichert sind. In ASP.NET Core wird jedoch anstelle von Web.configein neues Konfigurationsmodell verwendet.

Das neue Konfigurationssystem bietet Ihnen die folgenden Optionen, um dies zu lösen:

  1. Erstellen Sie eine Klasse, um Ihre Middlewareoptionen zu speichern, z. B.:

    public class MyMiddlewareOptions
    {
        public string Param1 { get; set; }
        public string Param2 { get; set; }
    }
    
  2. Speichern der Optionswerte

    Mit dem Konfigurationssystem können Sie Optionswerte an beliebiger Stelle speichern. Jedoch verwenden die meisten Websites appsettings.json, daher werden wir diesen Ansatz verfolgen:

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

    MyMiddlewareOptionsSection hier ist ein Abschnittsname. Sie muss nicht mit dem Namen Ihrer Optionsklasse identisch sein.

  3. Zuordnen der Optionswerte zur Optionsklasse

    Das Optionsmuster verwendet das ASP.NET Core-Abhängigkeitsinjektions-Framework, um den Optionstyp (wie zum Beispiel MyMiddlewareOptions) einem MyMiddlewareOptions-Objekt zuzuordnen, das die tatsächlichen Optionen enthält.

    Aktualisieren Sie Ihre Startup Klasse:

    1. Wenn Sie appsettings.json verwenden, fügen Sie es dem Konfigurations-Builder im Startup Konstruktor hinzu.

      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. Konfigurieren Sie den Optionsdienst:

      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. Ordnen Sie Ihre Optionen Ihrer Optionsklasse zu:

      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. Injizieren Sie die Optionen in den Middleware-Konstruktor. Dies ähnelt dem Einfügen von Optionen in einen 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
        }
    }
    

    Die Erweiterungsmethode UseMiddleware, die Ihre Middleware hinzufügt, kümmert sich um die Abhängigkeitsinjektion in die IApplicationBuilder.

    Dies ist nicht auf Objekte beschränkt IOptions . Jedes andere Objekt, das Ihre Middleware benötigt, kann auf diese Weise eingefügt werden.

Laden von Middleware-Optionen durch direkte Injektion

Das Optionsmuster hat den Vorteil, dass es eine lose Kopplung zwischen Optionswerten und ihren Verbrauchern schafft. Nachdem Sie eine Optionsklasse den tatsächlichen Optionswerten zugeordnet haben, kann jede andere Klasse über das Abhängigkeitseinfügungsframework Zugriff auf die Optionen erhalten. Es ist nicht erforderlich, Optionswerte weiterzugeben.

Dies bricht jedoch auf, wenn Sie dieselbe Middleware zweimal mit unterschiedlichen Optionen verwenden möchten. Beispielsweise eine Autorisierungs-Middleware, die in verschiedenen Zweigen verwendet wird und unterschiedliche Rollen zulässt. Sie können zwei verschiedene Optionsobjekte nicht der 1 Optionsklasse zuordnen.

Die Lösung besteht darin, die Optionsobjekte mit den tatsächlichen Optionswerten in Ihrer Startup Klasse abzurufen und diese direkt an jede Instanz Ihrer Middleware zu übergeben.

  1. Hinzufügen eines zweiten Schlüssels zu appsettings.json

    Um der appsettings.json Datei einen zweiten Satz von Optionen hinzuzufügen, verwenden Sie einen neuen Schlüssel, um ihn eindeutig zu identifizieren:

    {
      "MyMiddlewareOptionsSection2": {
        "Param1": "Param1Value2",
        "Param2": "Param2Value2"
      },
      "MyMiddlewareOptionsSection": {
        "Param1": "Param1Value",
        "Param2": "Param2Value"
      }
    }
    
  2. Rufen Sie Optionswerte ab, und übergeben Sie sie an Middleware. Die Use...-Erweiterungsmethode (die Ihre Middleware zur Pipeline hinzufügt) ist ein logischer Ort, an den die Optionswerte übergeben werden können.

    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. Aktivieren Sie Middleware, um einen Optionsparameter anzunehmen. Stellen Sie eine Überladung der Use... Erweiterungsmethode bereit (die den Optionsparameter verwendet und an UseMiddlewarediese übergibt). Beim UseMiddleware Aufruf mit Parametern werden die Parameter an den Middleware-Konstruktor übergeben, wenn es das Middleware-Objekt instanziiert.

    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));
        }
    }
    

    Beachten Sie, wie das Optionsobjekt als ein OptionsWrapper Objekt eingeschlossen wird. Dies implementiert IOptions, wie vom Middleware-Konstruktor erwartet.

Inkrementelle IHttpModule-Migration

Es gibt Zeiten, in denen das Konvertieren von Modulen in Middleware nicht einfach erfolgen kann. Um Migrationsszenarien zu unterstützen, in denen Module erforderlich sind und nicht in Middleware verschoben werden können, unterstützen System.Web-Adapter das Hinzufügen zu ASP.NET Core.

Beispiel für IHttpModule

Zur Unterstützung von HttpApplication Modulen muss eine Instanz verfügbar sein. Wenn keine benutzerdefinierten HttpApplication verwendet werden, wird ein Standardwert verwendet, um die Module hinzuzufügen. Ereignisse, die in einer benutzerdefinierten Anwendung (einschließlich Application_Start) deklariert sind, werden registriert und entsprechend ausgeführt.

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

Diese Infrastruktur kann verwendet werden, um die Nutzung von Global.asax zu migrieren, falls erforderlich. Die Quelle aus Global.asax ist eine benutzerdefinierte HttpApplication Und die Datei kann in einer ASP.NET Core-Anwendung enthalten sein. Da er benannt Globalist, kann der folgende Code verwendet werden, um ihn zu registrieren:

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

Solange die darin enthaltene Logik in ASP.NET Core verfügbar ist, kann dieser Ansatz verwendet werden, um die Abhängigkeit von Global.asax schrittweise auf ASP.NET Core zu migrieren.

Authentifizierungs-/Autorisierungsereignisse

Damit die Authentifizierungs- und Autorisierungsereignisse zur gewünschten Zeit ausgeführt werden können, sollte das folgende Muster verwendet werden:

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

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

Wenn dies nicht gemacht wird, laufen die Ereignisse weiter. Während des Anrufs von .UseSystemWebAdapters() wird es jedoch geschehen.

HTTP-Modulpooling

Da Module und Anwendungen in ASP.NET Framework einer Anforderung zugewiesen wurden, wird für jede Anforderung eine neue Instanz benötigt. Da sie jedoch teuer zu erstellen sind, werden sie mithilfe von ObjectPool<T> zusammengefasst. Um die tatsächliche Lebensdauer der HttpApplication Instanzen anzupassen, kann ein benutzerdefinierter Pool verwendet werden:

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);
});

Weitere Ressourcen