次の方法で共有


HTTP モジュールを ASP.NET Core ミドルウェアに移行する

この記事では、既存の ASP.NET HTTP モジュールを system.webserver から ASP.NET Core ミドルウェアに移行する方法について説明します。

モジュールの再検討

ASP.NET Core ミドルウェアに進む前に、まず HTTP モジュールのしくみをまとめてみましょう。

モジュール ハンドラー

モジュールは次のとおりです。

  • IHttpModule を実装するクラス

  • すべての要求に対して呼び出されます

  • 短絡可能 (要求のさらなる処理を停止可能)

  • HTTP 応答に追加したり、独自の HTTP 応答を作成したりできる

  • 構成済みはWeb.configで設定されます

モジュールが受信要求を処理する順序は、次の方法で決まります。

  1. BeginRequestAuthenticateRequestなど、ASP.NET によって発生する一連のイベント。 完全な一覧については、 System.Web.HttpApplicationを参照してください。 各モジュールは、1 つ以上のイベントのハンドラーを作成できます。

  2. 同じイベントの場合、 Web.configで構成 される順序。

モジュールに加えて、ライフサイクル イベントのハンドラーを Global.asax.cs ファイルに追加できます。 これらのハンドラーは、構成されたモジュールのハンドラーの後に実行されます。

モジュールからミドルウェアへ

ミドルウェアは HTTP モジュールよりも簡単です。

  • モジュール、 Global.asax.csWeb.config (IIS 構成を除く) とアプリケーションのライフ サイクルがなくなった

  • モジュールのロールはミドルウェアによって引き継がれます

  • ミドルウェアは、Web.config ではなくコードを使用して構成されます

  • パイプライン分岐を 使用すると、URL だけでなく、要求ヘッダー、クエリ文字列などに基づいて、特定のミドルウェアに要求を送信できます。
  • パイプライン分岐を 使用すると、URL だけでなく、要求ヘッダー、クエリ文字列などに基づいて、特定のミドルウェアに要求を送信できます。

ミドルウェアはモジュールによく似ています。

ミドルウェアとモジュールは、異なる順序で処理されます。

承認ミドルウェアは、承認されていないユーザーの要求をショートサーキットします。[インデックス] ページの要求は、MVC ミドルウェアによって許可され、処理されます。販売レポートの要求は、カスタム レポート ミドルウェアによって許可および処理されます。

上の図では、認証ミドルウェアによって要求が中断された様子に注目してください。

モジュール コードをミドルウェアに移行する

既存の HTTP モジュールは次のようになります。

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

ミドルウェア ページに示すように、ASP.NET Core ミドルウェアは、Invokeを受け取ってHttpContextを返すTask メソッドを公開するクラスです。 新しいミドルウェアは次のようになります。

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

上記のミドルウェア テンプレートは、 ミドルウェアの記述に関するセクションから取得しました。

MyMiddlewareExtensions ヘルパー クラスを使用すると、Startup クラスでミドルウェアを簡単に構成できます。 UseMyMiddleware メソッドは、ミドルウェア クラスを要求パイプラインに追加します。 ミドルウェアに必要なサービスは、ミドルウェアのコンストラクターに挿入されます。

ユーザーが承認されていない場合など、モジュールによって要求が終了する場合があります。

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

ミドルウェアは、パイプライン内の次のミドルウェアで Invoke を呼び出さないでこれを処理します。 以前のミドルウェアは、応答がパイプラインを通って戻るときに引き続き呼び出されるため、要求は完全には終了しません。

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

モジュールの機能を新しいミドルウェアに移行すると、 HttpContext クラスが ASP.NET Core で大幅に変更されたため、コードがコンパイルされないことがあります。 新しい ASP.NET Core HttpContext に移行する方法については、「ASP.NET Framework HttpContext から ASP.NET Core への移行」を参照してください。

要求パイプラインへのモジュール挿入の移行

HTTP モジュールは、通常、次の 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>

これを変換するには、 クラスの要求パイプラインに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?}");
    });
}

新しいミドルウェアを挿入するパイプライン内の正確な場所は、モジュールとして処理されたイベント (BeginRequestEndRequestなど) と、 Web.config内のモジュールの一覧での順序によって異なります。

前述のように、ASP.NET Core にはアプリケーションのライフ サイクルがなく、ミドルウェアによって応答が処理される順序は、モジュールで使用される順序と異なります。 これにより、注文の決定がより困難になる可能性があります。

順序付けが問題になる場合は、モジュールを複数のミドルウェア コンポーネントに分割し、個別に並べ替えることができます。

オプション パターンを使用したミドルウェア オプションの読み込み

一部のモジュールには、 Web.configに格納されている構成オプションがあります。ただし、ASP.NET Core では、 Web.configの代わりに新しい構成モデルが使用されます。

新しい 構成システム では、これを解決するための次のオプションが提供されます。

  1. ミドルウェア オプションを保持するクラスを作成します。次に例を示します。

    public class MyMiddlewareOptions
    {
        public string Param1 { get; set; }
        public string Param2 { get; set; }
    }
    
  2. オプション値を格納する

    構成システムを使用すると、任意の場所にオプション値を格納できます。 ただし、ほとんどのサイトでは appsettings.jsonを使用するため、次の方法を使用します。

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

    ここでの MyMiddlewareOptionsSection はセクション名です。 options クラスの名前と同じである必要はありません。

  3. オプション値を options クラスに関連付ける

    オプション パターンでは、ASP.NET Core の依存関係挿入フレームワークを使用して、オプションの種類 ( MyMiddlewareOptions など) を、実際のオプションを持つ MyMiddlewareOptions オブジェクトに関連付けます。

    Startup クラスを更新します。

    1. appsettings.jsonを使用している場合は、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. オプション サービスを構成します。

      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. オプションを 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. ミドルウェア コンストラクターにオプションを挿入します。 これは、コントローラーにオプションを挿入するのと似ています。

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

    ミドルウェアをに追加する IApplicationBuilder 拡張メソッドは、依存関係の挿入を処理します。

    これは、 IOptions オブジェクトに限定されるわけではありません。 ミドルウェアに必要なその他のオブジェクトは、この方法で挿入できます。

直接挿入によるミドルウェア オプションの読み込み

オプション パターンには、オプション値とそのコンシューマーの間に疎結合が作成されるという利点があります。 options クラスを実際のオプション値に関連付けてから、他のクラスは依存関係挿入フレームワークを使用してオプションにアクセスできます。 オプション値を渡す必要はありません。

ただし、同じミドルウェアを異なるオプションで 2 回使用する場合は、この処理が中断されます。 たとえば、異なるブランチで使用される承認ミドルウェアでは、異なるロールが許可されます。 2 つの異なるオプション オブジェクトを 1 つのオプション クラスに関連付けることはできません。

解決策は、 Startup クラスの実際のオプション値を持つ options オブジェクトを取得し、ミドルウェアの各インスタンスに直接渡すことです。

  1. appsettings.json に2番目のキーを追加する

    appsettings.json ファイルに 2 つ目のオプション セットを追加するには、新しいキーを使用して一意に識別します。

    {
      "MyMiddlewareOptionsSection2": {
        "Param1": "Param1Value2",
        "Param2": "Param2Value2"
      },
      "MyMiddlewareOptionsSection": {
        "Param1": "Param1Value",
        "Param2": "Param2Value"
      }
    }
    
  2. オプション値を取得し、ミドルウェアに渡します。 (パイプラインにミドルウェアを追加する) Use... 拡張メソッドは、オプション値を渡す論理的な場所です。

    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. ミドルウェアがオプション パラメーターを受け取るようにします。 Use...拡張メソッドのオーバーロードを指定します (options パラメーターを受け取り、UseMiddlewareに渡します)。 UseMiddleware がパラメーターとともに呼び出されると、パラメーターはミドルウェア オブジェクトをインスタンス化する際にミドルウェア コンストラクターに渡されます。

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

    これにより、 OptionsWrapper オブジェクト内の options オブジェクトがどのようにラップされるかに注意してください。 これにより、ミドルウェア コンストラクターの想定どおりに、 IOptionsが実装されます。

IHttpModule の段階的移行

モジュールからミドルウェアへの変換が簡単にできない場合があります。 モジュールが必要であり、ミドルウェアに移動できない移行シナリオをサポートするために、System.Web アダプターではそれらを ASP.NET Core に追加できます。

IHttpModule の例

モジュールをサポートするには、 HttpApplication のインスタンスを使用できる必要があります。 カスタム HttpApplication が使用されていない場合は、モジュールの追加に既定のが使用されます。 カスタム アプリケーションで宣言されたイベント ( Application_Startを含む) が登録され、それに応じて実行されます。

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 の移行

このインフラストラクチャは、必要に応じて Global.asax の使用を移行するために使用できます。 Global.asaxからのソースはカスタム HttpApplicationであり、ファイルは ASP.NET Core アプリケーションに含めることができます。 Globalという名前なので、次のコードを使用して登録できます。

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

ASP.NET Core でロジックを使用できる限り、このアプローチを使用して、 Global.asax への依存を ASP.NET Core に段階的に移行できます。

認証/承認イベント

必要なタイミングで認証イベントと承認イベントを実行するには、次のパターンを使用する必要があります。

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

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

これが行われなければ、イベントは引き続き実行されます。 ただし、 .UseSystemWebAdapters()の呼び出し中になります。

HTTP モジュール プール

ASP.NET Framework のモジュールとアプリケーションは要求に割り当てられたため、要求ごとに新しいインスタンスが必要です。 ただし、作成にはコストがかかる可能性があるため、 ObjectPool<T>を使用してプールされます。 HttpApplication インスタンスの実際の有効期間をカスタマイズするために、カスタム プールを使用できます。

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

その他のリソース