Partager via


Migrer des modules HTTP vers ASP.NET intergiciel Core

Cet article explique comment migrer des modules HTTP ASP.NET existants de system.webserver vers ASP.NET intergiciel Core.

Modules revisités

Avant de passer à ASP.NET intergiciel Core, récapitule tout d’abord le fonctionnement des modules HTTP :

Gestionnaire de modules

Les modules sont les suivants :

  • Classes qui implémentent IHttpModule

  • Appelé pour chaque requête

  • Possibilité de court-circuit (arrêter le traitement ultérieur d’une demande)

  • Possibilité d’ajouter à la réponse HTTP ou de créer sa propre réponse

  • Configuré dans Web.config

L’ordre dans lequel les modules traitent les demandes entrantes est déterminé par :

  1. Événements de série déclenchés par ASP.NET, tels que BeginRequest et AuthenticateRequest. Pour obtenir une liste complète, consultez System.Web.HttpApplication. Chaque module peut créer un gestionnaire pour un ou plusieurs événements.

  2. Pour le même événement, l’ordre dans lequel ils sont configurés dans Web.config.

En plus des modules, vous pouvez ajouter des gestionnaires pour les événements de cycle de vie à votre Global.asax.cs fichier. Ces gestionnaires s’exécutent après les gestionnaires des modules configurés.

Des modules au middleware

Les intergiciels sont plus simples que les modules HTTP :

  • Modules, Global.asax.csWeb.config (à l’exception de la configuration IIS) et le cycle de vie de l’application sont disparus

  • Les rôles des modules ont été repris par l’intergiciel

  • Les intergiciels sont configurés à l’aide du code plutôt que dans Web.config

  • Le branchement de pipeline vous permet d’envoyer des requêtes à un intergiciel spécifique, en fonction non seulement de l’URL, mais également des en-têtes de requête, des chaînes de requête, etc.
  • Le branchement de pipeline vous permet d’envoyer des requêtes à un intergiciel spécifique, en fonction non seulement de l’URL, mais également des en-têtes de requête, des chaînes de requête, etc.

Les intergiciels sont très similaires aux modules :

Les intergiciels et les modules sont traités dans un ordre différent :

  • L’ordre des intergiciels est basé sur l’ordre dans lequel ils sont insérés dans le pipeline de requête, tandis que l’ordre des modules est principalement basé sur System.Web.HttpApplication les événements.

  • L’ordre des intergiciels pour les réponses est l’inverse de celui des demandes, tandis que l’ordre des modules est le même pour les demandes et les réponses

  • Voir Créer un pipeline d’intergiciels avec IApplicationBuilder

L’intergiciel d’autorisation court-circuite une demande d’utilisateur qui n’est pas autorisé. Une demande de la page Index est autorisée et traitée par l’intergiciel MVC. Une demande de rapport de vente est autorisée et traitée par un intergiciel de rapport personnalisé.

Notez comment, dans l’image ci-dessus, l’intergiciel d’authentification a court-circuité la requête.

Migration du code de module vers un intergiciel

Un module HTTP existant ressemble à ceci :

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

Comme indiqué dans la page Middleware, un intergiciel ASP.NET Core est une classe qui expose une Invoke méthode prenant un HttpContext et retournant un Task. Votre nouvel intergiciel ressemble à ceci :

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

Le modèle de middleware précédent a été extrait de la section sur l'écriture de middleware.

La classe d’assistance MyMiddlewareExtensions facilite la configuration de votre intergiciel dans votre Startup classe. La UseMyMiddleware méthode ajoute votre classe middleware au pipeline de requête. Les services requis par l’intergiciel sont injectés dans le constructeur du middleware.

Votre module peut mettre fin à une demande, par exemple si l’utilisateur n’est pas autorisé :

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

Un intergiciel gère cela en n’appelant Invoke pas le middleware suivant dans le pipeline. N’oubliez pas que cela n’arrête pas complètement la requête, car les intergiciels précédents seront toujours appelés lorsque la réponse revient dans le 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.
}

Lorsque vous migrez les fonctionnalités de votre module vers votre nouveau middleware, vous pouvez constater que votre code ne se compile pas, car la HttpContext classe a considérablement changé dans ASP.NET Core. Consultez Migrer de ASP.NET Framework HttpContext vers ASP.NET Core pour découvrir comment migrer vers la nouvelle ASP.NET Core HttpContext.

Migration de l’insertion de modules dans le pipeline des requêtes

Les modules HTTP sont généralement ajoutés au pipeline de requête à l’aide deWeb.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>

Convertissez-le en ajoutant votre nouveau middleware au pipeline de requête dans votre Startup classe :

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

L’emplacement exact dans le pipeline où vous insérez votre nouvel intergiciel dépend de l’événement qu’il a géré en tant que module (BeginRequest, EndRequestetc.) et de son ordre dans votre liste de modules dans Web.config.

Comme indiqué précédemment, il n’existe aucun cycle de vie d’application dans ASP.NET Core et l’ordre dans lequel les réponses sont traitées par middleware diffère de l’ordre utilisé par les modules. Cela pourrait rendre votre décision de commande plus difficile.

Si l’ordre devient un problème, vous pouvez fractionner votre module en plusieurs composants middleware qui peuvent être ordonnés indépendamment.

Chargement des options d’intergiciel à l’aide du modèle d’options

Certains modules ont des options de configuration stockées dans Web.config. Toutefois, dans ASP.NET Core, un nouveau modèle de configuration est utilisé à la place de Web.config.

Le nouveau système de configuration vous offre ces options pour résoudre ce problème :

  1. Créez une classe pour contenir vos options d’intergiciel, par exemple :

    public class MyMiddlewareOptions
    {
        public string Param1 { get; set; }
        public string Param2 { get; set; }
    }
    
  2. Stocker les valeurs d’option

    Le système de configuration vous permet de stocker les valeurs d’option n’importe où vous le souhaitez. Toutefois, la plupart des sites utilisent appsettings.json, nous allons donc adopter cette approche :

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

    MyMiddlewareOptionsSection voici un nom de section. Il n’est pas obligé d’être identique au nom de votre classe d’options.

  3. Associer les valeurs d’option à la classe options

    Le modèle d’options utilise le framework d’injection de dépendances d’ASP.NET Core pour associer le type d’options (par exemple MyMiddlewareOptions) à un objet MyMiddlewareOptions qui contient les options réelles.

    Mettez à jour votre Startup classe :

    1. Si vous utilisez appsettings.json, ajoutez-le au générateur de configuration dans le Startup constructeur :

      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. Configurez le service options :

      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. Associez vos options à votre classe d’options :

      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. Injectez les options dans votre constructeur d’intergiciel. Cela est similaire à l’injection d’options dans un contrôleur.

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

    La méthode d’extension UseMiddleware, qui ajoute votre intergiciel, prend en charge l’injection de dépendances pour le IApplicationBuilder.

    Cela n’est pas limité aux IOptions objets. Tout autre objet requis par votre intergiciel peut être injecté de cette façon.

Chargement des options d’intergiciel via l’injection directe

Le modèle d’options présente l’avantage qu’il crée un couplage libre entre les valeurs d’options et leurs consommateurs. Une fois que vous avez associé une classe d’options aux valeurs d’options réelles, toute autre classe peut accéder aux options via l’infrastructure d’injection de dépendances. Il n’est pas nécessaire de propager les valeurs des options.

Cela se décompose toutefois si vous souhaitez utiliser le même intergiciel deux fois, avec différentes options. Par exemple, un intergiciel d’autorisation utilisé dans différentes branches autorisant différents rôles. Vous ne pouvez pas associer deux objets d’options différents à la classe d’options unique.

La solution consiste à obtenir les objets d’options avec les valeurs d’options réelles de votre Startup classe et à les transmettre directement à chaque instance de votre intergiciel.

  1. Ajouter une deuxième clé à appsettings.json

    Pour ajouter un deuxième ensemble d’options au appsettings.json fichier, utilisez une nouvelle clé pour l’identifier de manière unique :

    {
      "MyMiddlewareOptionsSection2": {
        "Param1": "Param1Value2",
        "Param2": "Param2Value2"
      },
      "MyMiddlewareOptionsSection": {
        "Param1": "Param1Value",
        "Param2": "Param2Value"
      }
    }
    
  2. Récupérez les valeurs des options et passez-les au middleware. La méthode d’extension Use... (qui ajoute votre middleware au pipeline) est un endroit logique pour passer les valeurs d’option :

    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. Activez le middleware pour accepter un paramètre options. Fournissez une surcharge de la méthode d’extension Use... (qui prend le paramètre d’options et le transmet à UseMiddleware). Lorsqu’il UseMiddleware est appelé avec des paramètres, il transmet les paramètres à votre constructeur d’intergiciel lorsqu’il instancie l’objet 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));
        }
    }
    

    Notez comment cela encapsule l’objet options dans un OptionsWrapper objet. Cela implémente IOptions, comme prévu par le constructeur d’intergiciel.

Migration incrémentielle IHttpModule

Il y a des moments où il n'est pas facile de convertir des modules en intergiciels. Pour prendre en charge les scénarios de migration dans lesquels les modules sont requis et ne peuvent pas être déplacés vers l’intergiciel, les adaptateurs System.Web prennent en charge leur ajout à ASP.NET Core.

Exemple d'IHttpModule

Pour prendre en charge les modules, une instance de HttpApplication doit être disponible. Si aucune personnalisée HttpApplication n’est utilisée, une valeur par défaut est utilisée pour ajouter les modules. Les événements déclarés dans une application personnalisée (y compris Application_Start) sont enregistrés et exécutés en conséquence.

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

Migration de Global.asax

Cette infrastructure peut être utilisée pour migrer l’utilisation du Global.asax cas échéant. La source depuis Global.asax est un HttpApplication personnalisé, et le fichier peut être inclus dans une application ASP.NET Core. Étant donné qu’il est nommé Global, le code suivant peut être utilisé pour l’inscrire :

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

Tant que la logique qu’elle contient est disponible dans ASP.NET Core, cette approche peut être utilisée pour migrer progressivement la dépendance à Global.asax vers ASP.NET Core.

Événements d’authentification/autorisation

Pour que les événements d’authentification et d’autorisation s’exécutent au moment souhaité, le modèle suivant doit être utilisé :

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

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

Si ce n’est pas le cas, les événements continueront à s'exécuter. Toutefois, cela se produira au cours de l'appel de .UseSystemWebAdapters().

Regroupement de modules HTTP

Étant donné que les modules et les applications dans ASP.NET Framework ont été affectés à une demande, une nouvelle instance est nécessaire pour chaque requête. Toutefois, étant donné qu’ils peuvent être coûteux à créer, ils sont mis en commun à l’aide ObjectPool<T>. Pour personnaliser la durée de vie réelle des HttpApplication instances, un pool personnalisé peut être utilisé :

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

Ressources supplémentaires