Sdílet prostřednictvím


Migrace modulů HTTP do middlewaru ASP.NET Core

Tento článek ukazuje, jak migrovat existující moduly ASP.NET HTTP ze system.webserveru do middlewaru ASP.NET Core.

Moduly znovu prozkoumány

Než se pustíme do ASP.NET základního middlewaru, pojďme nejprve zrekapitulovat, jak moduly HTTP fungují:

Řadič modulů

Moduly jsou:

  • Třídy, které implementují IHttpModule

  • Vyvoláno pro každý požadavek

  • Možnost zkratování (zastavení dalšího zpracování žádosti)

  • Možnost přidat k odpovědi HTTP nebo vytvořit vlastní

  • Nakonfigurováno v Web.config

Pořadí, ve kterém moduly zpracovávají příchozí požadavky, určuje:

  1. Řada událostí vygenerovaných ASP.NET, například BeginRequest a AuthenticateRequest. Úplný seznam viz System.Web.HttpApplication. Každý modul může vytvořit obslužnou rutinu pro jednu nebo více událostí.

  2. U stejné události je pořadí, ve kterém jsou nakonfigurované v Web.config.

Kromě modulů můžete do Global.asax.cs souboru přidat obslužné rutiny pro události životního cyklu. Tyto obslužné rutiny se spouští po obslužných rutinách v nakonfigurovaných modulech.

Z modulů do middlewaru

Middleware je jednodušší než moduly HTTP:

  • Moduly, Global.asax.csWeb.config (s výjimkou konfigurace služby IIS) a životní cyklus aplikace jsou pryč.

  • Role modulů převzal middleware.

  • Middleware se konfigurují pomocí kódu místo vWeb.config

  • Větvení kanálů umožňuje odesílat požadavky do konkrétního middlewaru, a to na základě adresy URL, ale také hlaviček požadavků, řetězců dotazů atd.
  • Větvení kanálů umožňuje odesílat požadavky do konkrétního middlewaru, a to na základě adresy URL, ale také hlaviček požadavků, řetězců dotazů atd.

Middleware je velmi podobný modulům:

Middleware a moduly se zpracovávají v jiném pořadí:

Autorizační Middleware přeruší požadavek na uživatele, který nemá oprávnění. Požadavek na úvodní stránku je povolen a zpracován middlewarem MVC. Požadavek na prodejní sestavu je povolen a zpracován vlastním middlewarem pro sestavy.

Všimněte si, jak na obrázku výše ověřovací middleware přerušil zpracování požadavku.

Migrace kódu modulu do middlewaru

Existující modul HTTP bude vypadat nějak takto:

// 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 je znázorněno na stránce Middleware, middleware v ASP.NET Core je třída, která zveřejňuje metodu Invoke, která přijímá HttpContext a vrací Task. Váš nový middleware bude vypadat takto:

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

Předchozí šablona middlewaru byla převzata z části věnované psaní middlewaru.

Pomocná třída MyMiddlewareExtensions usnadňuje konfiguraci middlewaru ve třídě Startup . Metoda UseMyMiddleware přidá třídu middlewaru do kanálu požadavku. Služby vyžadované middlewarem se vloží do konstruktoru middlewaru.

Váš modul může žádost ukončit, například pokud uživatel nemá oprávnění:

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

Middleware to zpracovává tak, že Invoke nevolá další middleware v potrubí. Mějte na paměti, že se tím požadavek úplně neukončí, protože předchozí middleware budou stále vyvolány, když odpověď prochází zpět v potrubí.

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

Když migrujete funkce modulu do nového middlewaru, můžete zjistit, že se váš kód nekompiluje, protože HttpContext se třída výrazně změnila v ASP.NET Core. Podívejte se na Migrace z ASP.NET Framework HttpContext na ASP.NET Core, kde se dozvíte, jak migrovat na nový ASP.NET Core HttpContext.

Migrace modulu vložení do kanálu žádosti

Moduly HTTP se obvykle přidávají do kanálu požadavku pomocí 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>

Konvertujte to přidáním vašeho nového middlewaru do potrubí požadavků ve třídě 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?}");
    });
}

Přesné místo v kanálu, kam vkládáte nový middleware, závisí na události, která je zpracována jako modul (BeginRequest, EndRequest, atd.) a na jeho pořadí v seznamu modulů v Web.config.

Jak jsme uvedli dříve, v ASP.NET Core neexistuje žádný životní cyklus aplikace a pořadí, ve kterém se odpovědi zpracovávají middlewarem, se liší od pořadí používaného moduly. To by mohlo znamenat, že vaše rozhodnutí o objednávání bude náročnější.

Pokud se řazení stane problémem, můžete modul rozdělit na několik middlewareových komponent, které lze řadit nezávisle.

Načítání možností middlewaru pomocí vzoru možností

Některé moduly mají možnosti konfigurace, které jsou uloženy v Web.config. V ASP.NET Core se však místo Web.configpoužívá nový konfigurační model .

Nový konfigurační systém nabízí tyto možnosti řešení:

  1. Vytvořte třídu pro uložení možností middlewaru, například:

    public class MyMiddlewareOptions
    {
        public string Param1 { get; set; }
        public string Param2 { get; set; }
    }
    
  2. Uložení hodnot možností

    Konfigurační systém umožňuje ukládat hodnoty možností kdekoli, kde chcete. Většina webů však používá appsettings.json, a proto tento přístup zvolíme:

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

    MyMiddlewareOptionsSection je název oddílu. Nemusí se shodovat s názvem třídy možností.

  3. Přiřaďte hodnoty možností ke třídě možností

    Model možností používá architekturu injektáže závislostí ASP.NET Core k přidružení typu možností (například MyMiddlewareOptions) k MyMiddlewareOptions objektu, který má skutečné možnosti.

    Aktualizujte svoji Startup třídu:

    1. Pokud používáte appsettings.json, přidejte ho do tvůrce konfigurace v konstruktoru 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. Nakonfigurujte službu možností:

      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. Přidružte možnosti ke třídě možností:

      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. Vložte možnosti do konstruktoru middlewaru. Podobá se vkládání možností do kontroleru.

    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 rozšíření UseMiddleware, která přidává váš middleware do IApplicationBuilder, se postará o injektáž závislostí.

    To není omezeno na IOptions objekty. Jakýkoli jiný objekt, který middleware vyžaduje, je možné vložit tímto způsobem.

Načítání možností middleware prostřednictvím přímé injekce

Vzor možností má výhodu, že vytváří volné spojení mezi hodnotami možností a jejich spotřebiteli. Jakmile přidružíte třídu možností se skutečnými hodnotami možností, může každá jiná třída získat přístup k možnostem prostřednictvím architektury injektáže závislostí. Není potřeba předávat hodnoty možností.

Toto se však zkomplikuje, pokud chcete stejný middleware použít dvakrát s různými nastaveními. Například autorizační middleware používaný v různých větvích, který umožňuje různé role. Nelze přidružit dva různé objekty možností k jedné třídě možností.

Řešením je získat objekty možností se skutečnými hodnotami možností ve vaší Startup třídě a předat je přímo do každé instance middlewaru.

  1. Přidání druhého klíče do appsettings.json

    Pokud chcete do appsettings.json souboru přidat druhou sadu možností, použijte k jedinečné identifikaci nový klíč:

    {
      "MyMiddlewareOptionsSection2": {
        "Param1": "Param1Value2",
        "Param2": "Param2Value2"
      },
      "MyMiddlewareOptionsSection": {
        "Param1": "Param1Value",
        "Param2": "Param2Value"
      }
    }
    
  2. Načtěte hodnoty možností a předejte je middlewaru. Metoda Use... rozšíření (která přidává váš middleware do pipeline) je logické místo pro předávání hodnot parametrů:

    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. Povolte middleware, abyste mohli použít parametr možností. Zadejte přetížení Use... rozšiřující metody (která přebírá parametr možností a předává ho UseMiddleware). Při zavolání UseMiddleware s parametry jsou tyto parametry předány do konstruktoru middlewaru při instanciaci objektu middlewaru.

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

    Všimněte si, jak se tím zalomí objekt možností v objektu OptionsWrapper . Tím se implementuje IOptions, jak očekává konstruktor middlewaru.

Přírůstková migrace IHttpModule

Jsou chvíle, kdy převod modulů na middleware nelze snadno provést. Aby bylo možné podporovat scénáře migrace, ve kterých se vyžadují moduly a nelze je přesunout do middlewaru, podporují adaptéry System.Web jejich přidání do ASP.NET Core.

Příklad IHttpModule

Aby bylo možné podporovat moduly, musí být instance HttpApplication dostupná. Pokud se nepoužívá žádný vlastní HttpApplication, použije se výchozí k přidání modulů. Události deklarované ve vlastní aplikaci (včetně Application_Start) se zaregistrují a spustí odpovídajícím způsobem.

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

Migrace Global.asax

Tuto infrastrukturu lze v případě potřeby použít k migraci použití Global.asax. Zdroj z Global.asax je vlastní HttpApplication a soubor lze zahrnout do aplikace ASP.NET Core. Vzhledem k tomu, že je pojmenovaná Global, můžete k registraci použít následující kód:

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

Pokud je logika v ASP.NET Core dostupná, lze tento přístup použít k postupné migraci závislosti na Global.asax do ASP.NET Core.

Události ověřování/autorizace

Aby se události ověřování a autorizace spouštěly v požadované době, měli byste použít následující vzor:

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

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

Pokud to neuděláte, události se budou dál spouštět. Bude však během volání .UseSystemWebAdapters().

Sdružování modulů HTTP

Vzhledem k tomu, že se k požadavku přiřadily moduly a aplikace v ASP.NET Framework, je pro každý požadavek potřeba nová instance. Vzhledem k tomu, že jejich vytvoření může být nákladné, jsou sdružovány pomocí ObjectPool<T>. Pokud chcete přizpůsobit skutečnou dobu života HttpApplication instancí, můžete použít vlastní fond:

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

Dodatečné zdroje