Udostępnij przez


Migrowanie modułów HTTP do oprogramowania pośredniczącego ASP.NET Core

W tym artykule pokazano, jak migrować istniejące moduły HTTP ASP.NET z pliku system.webserver do oprogramowania pośredniczącego ASP.NET Core.

Ponownie przeglądane moduły

Przed przejściem do oprogramowania pośredniczącego ASP.NET Core najpierw podsumujmy, jak działają moduły HTTP:

Menadżer modułów

Moduły to:

  • Klasy implementujące IHttpModule

  • Wywoływane dla każdego żądania

  • Możliwość przerwania (zatrzymania dalszego przetwarzania żądania)

  • Możliwość dodania czegoś do odpowiedzi HTTP lub utworzenia własnej odpowiedzi

  • Skonfigurowane w Web.config

Kolejność przetwarzania żądań przychodzących przez moduły jest określana przez:

  1. Seria zdarzeń wyzwalanych przez ASP.NET, takich jak BeginRequest i AuthenticateRequest. Aby uzyskać pełną listę, zobacz System.Web.HttpApplication. Każdy moduł może utworzyć procedurę obsługi dla co najmniej jednego zdarzenia.

  2. Dla tego samego zdarzenia kolejność, w jakiej są skonfigurowane w Web.config.

Oprócz modułów można dodawać programy obsługi zdarzeń cyklu życia do pliku Global.asax.cs . Te programy obsługi są uruchamiane po programach obsługi w skonfigurowanych modułach.

Z modułów do oprogramowania pośredniczącego

Oprogramowanie pośredniczące jest prostsze niż moduły HTTP:

  • Moduły, Global.asax.csWeb.config (z wyjątkiem konfiguracji usług IIS) i cykl życia aplikacji zniknęły

  • Role modułów zostały przejęte przez oprogramowanie pośredniczące

  • Oprogramowanie pośredniczące jest konfigurowane przy użyciu kodu, a nie w Web.config

  • Rozgałęzianie potoku umożliwia wysyłanie żądań do określonego oprogramowania pośredniczącego na podstawie nie tylko adresu URL, ale także nagłówków żądań, ciągów zapytania itp.
  • Rozgałęzianie potoku umożliwia wysyłanie żądań do określonego oprogramowania pośredniczącego na podstawie nie tylko adresu URL, ale także nagłówków żądań, ciągów zapytania itp.

Oprogramowanie pośredniczące jest bardzo podobne do modułów:

Oprogramowanie pośredniczące i moduły są przetwarzane w innej kolejności:

Oprogramowanie pośredniczące autoryzacji przerywa żądanie użytkownika, który nie jest autoryzowany. Żądanie strony Indeks jest dozwolone i przetwarzane przez warstwę pośrednią MVC. Żądanie raportu sprzedaży jest dozwolone i przetwarzane przez niestandardową warstwę pośrednią dla raportów.

Zwróć uwagę, jak na powyższym obrazie oprogramowanie pośredniczące uwierzytelniania przerwało żądanie.

Migrowanie kodu modułu do oprogramowania pośredniczącego

Istniejący moduł HTTP będzie wyglądać podobnie do następującego:

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

Jak pokazano na stronie Oprogramowanie pośredniczące, pośrednictwo ASP.NET Core to klasa, która udostępnia metodę przyjmującą Invoke i zwracającą HttpContext. Nowe oprogramowanie pośredniczące będzie wyglądać następująco:

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

Powyższy szablon oprogramowania pośredniczącego został pobrany z sekcji dotyczącej pisania oprogramowania pośredniczącego.

Klasa pomocnika MyMiddlewareExtensions ułatwia konfigurowanie middleware w klasie Startup. Metoda UseMyMiddleware dodaje klasę middleware do potoku żądania. Usługi wymagane przez oprogramowanie pośredniczące są wstrzykiwane w konstruktorze oprogramowania pośredniczącego.

Moduł może zakończyć żądanie, na przykład jeśli użytkownik nie jest autoryzowany:

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

Oprogramowanie pośredniczące obsługuje to, nie wywołując Invoke następnego oprogramowania pośredniczącego w potoku. Należy pamiętać, że nie powoduje to całkowitego zakończenia żądania, ponieważ poprzednie oprogramowanie pośredniczące nadal będzie wywoływane, gdy odpowiedź wraca przez potok.

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

Podczas migracji funkcji modułu do nowego oprogramowania pośredniczącego może się okazać, że kod nie zostanie skompilowany, ponieważ HttpContext klasa uległa znacznej zmianie w ASP.NET Core. Zobacz Migrowanie z ASP.NET Framework HttpContext do ASP.NET Core, aby dowiedzieć się, jak przeprowadzić migrację do nowego ASP.NET Core HttpContext.

Migracja wstawiania modułu do rury żądań

Moduły HTTP są zwykle dodawane do potoku żądań przy użyciu 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>

Zmień to, dodając nowe oprogramowanie pośredniczące do potoku żądań w klasie Startup:

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

Dokładne miejsce w pipeline, w którym wstawiasz nowe middleware, zależy od zdarzenia obsługiwanego jako moduł (BeginRequest, EndRequest, itp.) oraz jego kolejności na liście modułów w Web.config.

Jak wspomniano wcześniej, w ASP.NET Core nie ma cyklu życia aplikacji i kolejność przetwarzania odpowiedzi przez oprogramowanie pośredniczące różni się od kolejności używanej przez moduły. Może to sprawić, że decyzja o zamówieniu będzie trudniejsza.

Jeśli kolejność stanie się problemem, moduł można podzielić na wiele komponentów pośredniczących, które mogą być ustawione niezależnie.

Ładowanie opcji oprogramowania pośredniczącego przy użyciu wzorca opcji

Niektóre moduły mają opcje konfiguracji przechowywane w Web.config. Jednak w ASP.NET Core nowy model konfiguracji jest używany zamiast Web.config.

Nowy system konfiguracji udostępnia następujące opcje, aby to rozwiązać.

  1. Utwórz klasę do przechowywania opcji oprogramowania pośredniczącego, na przykład:

    public class MyMiddlewareOptions
    {
        public string Param1 { get; set; }
        public string Param2 { get; set; }
    }
    
  2. Przechowywanie wartości opcji

    System konfiguracji umożliwia przechowywanie wartości opcji w dowolnym miejscu. Jednak większość witryn używa metody appsettings.json, więc zastosujemy takie podejście:

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

    MyMiddlewareOptionsSection jest nazwą sekcji. Nie musi być taka sama jak nazwa klasy opcji.

  3. Kojarzenie wartości opcji z klasą options

    Wzorzec opcji używa platformy iniekcji zależności ASP.NET Core do skojarzenia typu opcji (takiego jak MyMiddlewareOptions) z obiektem MyMiddlewareOptions , który ma rzeczywiste opcje.

    Zaktualizuj klasę Startup :

    1. Jeśli używasz appsettings.json, dodaj go do budowniczego konfiguracji w konstruktorze Startup.

      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. Skonfiguruj usługę opcji:

      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. Skojarz swoje opcje z twoją klasą opcji.

      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. Wstrzykiwanie opcji do konstruktora oprogramowania pośredniczącego. Jest to podobne do wstrzykiwania opcji do kontrolera.

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

    Metoda rozszerzenia UseMiddleware, która dodaje middleware do IApplicationBuilder, zajmuje się iniekcją zależności.

    Nie jest to ograniczone do IOptions obiektów. Każdy inny obiekt, którego wymaga oprogramowanie pośredniczące, można w ten sposób wstrzyknąć.

Ładowanie opcji oprogramowania pośredniczącego poprzez bezpośrednie wstrzyknięcie

Wzorzec opcji ma zaletę, że tworzy luźne sprzężenie między wartościami opcji a ich odbiorcami. Po skojarzeniu klasy options z rzeczywistymi wartościami opcji każda inna klasa może uzyskać dostęp do opcji za pośrednictwem struktury wstrzykiwania zależności. Nie ma potrzeby przekazywania wartości opcji.

Spowoduje to awarię, jeśli chcesz użyć tego samego oprogramowania pośredniczącego dwa razy, z różnymi opcjami. Na przykład middleware autoryzacyjne używane w różnych oddziałach, pozwalające na różne role. Nie można skojarzyć dwóch różnych obiektów opcji z jedną klasą opcji.

Rozwiązaniem jest pobranie obiektów opcji z rzeczywistymi wartościami w klasie Startup i przekazanie ich bezpośrednio do każdej instancji oprogramowania pośredniczącego.

  1. Dodaj drugi klucz do appsettings.json

    Aby dodać drugi zestaw opcji do appsettings.json pliku, użyj nowego klucza, aby go jednoznacznie zidentyfikować:

    {
      "MyMiddlewareOptionsSection2": {
        "Param1": "Param1Value2",
        "Param2": "Param2Value2"
      },
      "MyMiddlewareOptionsSection": {
        "Param1": "Param1Value",
        "Param2": "Param2Value"
      }
    }
    
  2. Pobierz wartości opcji i przekaż je do oprogramowania pośredniczącego. Use... Metoda rozszerzenia (która dodaje oprogramowanie pośredniczące do potoku) jest logicznym miejscem do przekazania wartości opcji:

    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. Włącz oprogramowanie pośredniczące, aby pobrać parametr opcji. Podaj przeciążenie metody rozszerzenia Use... (przyjmujące parametr options i przekazujące go do metody UseMiddleware). Gdy UseMiddleware jest wywoływany z parametrami, przekazuje je do konstruktora middleware podczas instancjowania obiektu middleware.

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

    Zwróć uwagę, jak to opakowuje obiekt options w OptionsWrapper obiekcie. Implementuje to IOptions, zgodnie z oczekiwaniami konstruktora middleware.

Przyrostowa migracja IHttpModule

Czasami konwertowanie modułów na oprogramowanie pośredniczące nie może być łatwe. Aby obsługiwać scenariusze migracji, w których moduły są wymagane i nie można ich przenieść do middleware, adaptery System.Web obsługują dodanie ich do ASP.NET Core.

Przykład IHttpModule

Aby obsługiwać moduły, wystąpienie HttpApplication musi być dostępne. Jeśli nie jest używany żaden niestandardowy HttpApplication , domyślny zostanie użyty do dodania modułów. Zdarzenia zadeklarowane w aplikacji niestandardowej (w tym Application_Start) zostaną zarejestrowane i odpowiednio uruchomione.

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()
    {
    }
}

Migracja Global.asax

Ta infrastruktura może służyć do migrowania użycia Global.asax jeśli zajdzie potrzeba. Plik można uwzględnić w aplikacji ASP.NET Core, ponieważ źródło z Global.asax jest niestandardowym HttpApplication. Ponieważ nosi nazwę Global, można użyć następującego kodu do zarejestrowania go:

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

Tak długo, jak logika jest dostępna w ASP.NET Core, to podejście może być używane do przyrostowej migracji polegania na Global.asax na rzecz ASP.NET Core.

Zdarzenia uwierzytelniania/autoryzacji

Aby zdarzenia uwierzytelniania i autoryzacji były uruchamiane w żądanym czasie, należy użyć następującego wzorca:

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

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

Jeśli nie zostanie to zrobione, zdarzenia będą działały nadal. Jednak będzie to w trakcie wywołania .UseSystemWebAdapters()metody .

Buforowanie modułów HTTP

Ponieważ moduły i aplikacje w programie ASP.NET Framework zostały przypisane do żądania, dla każdego żądania potrzebne jest nowe wystąpienie. Jednak ponieważ mogą być kosztowne do utworzenia, są łączone za pomocą ObjectPool<T>. Aby dostosować rzeczywisty okres istnienia HttpApplication wystąpień, można użyć puli niestandardowej:

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

Dodatkowe zasoby