Memigrasikan modul HTTP ke middleware ASP.NET Core

Artikel ini menunjukkan cara memigrasikan modul HTTP ASP.NET yang ada dari system.webserver ke middleware ASP.NET Core.

Modul yang ditinjau kembali

Sebelum melanjutkan ke middleware ASP.NET Core, mari kita rekap terlebih dahulu cara kerja modul HTTP:

Handler Modul

Modulnya adalah:

  • Kelas yang mengimplementasikan IHttpModule

  • Dipanggil untuk setiap permintaan

  • Dapat melakukan sirkuit pendek (hentikan pemrosesan permintaan lebih lanjut)

  • Dapat menambahkan ke respons HTTP, atau membuat respons HTTP mereka sendiri

  • Dikonfigurasi dalam Web.config

Urutan proses modul permintaan masuk ditentukan oleh:

  1. Serangkaian peristiwa yang dipicu oleh ASP.NET, seperti BeginRequest dan AuthenticateRequest. Untuk daftar lengkapnya, lihat System.Web.HttpApplication. Setiap modul dapat membuat handler untuk satu atau beberapa peristiwa.

  2. Untuk peristiwa yang sama, urutan konfigurasinya di Web.config.

Selain modul, Anda dapat menambahkan handler untuk peristiwa siklus hidup ke file Anda Global.asax.cs . Handler ini berjalan setelah handler dalam modul yang dikonfigurasi.

Dari modul ke middleware

Middleware lebih sederhana daripada modul HTTP:

  • Modul, Global.asax.cs, Web.config (kecuali untuk konfigurasi IIS) dan siklus hidup aplikasi hilang

  • Peran modul telah diambil alih oleh middleware

  • Middleware dikonfigurasi menggunakan kode daripada di Web.config

  • Pencabangan alur memungkinkan Anda mengirim permintaan ke middleware tertentu, berdasarkan tidak hanya URL tetapi juga pada header permintaan, string kueri, dll.
  • Pencabangan alur memungkinkan Anda mengirim permintaan ke middleware tertentu, berdasarkan tidak hanya URL tetapi juga pada header permintaan, string kueri, dll.

Middleware sangat mirip dengan modul:

Middleware dan modul diproses dalam urutan yang berbeda:

  • Urutan middleware didasarkan pada urutan di mana mereka dimasukkan ke dalam alur permintaan, sementara urutan modul terutama didasarkan pada System.Web.HttpApplication peristiwa.

  • Urutan middleware untuk respons adalah kebalikan dari urutan untuk permintaan, sedangkan urutan modul tetap sama baik untuk permintaan maupun respons.

  • Lihat Membuat alur middleware dengan IApplicationBuilder

Middleware otorisasi langsung mengakhiri permintaan untuk pengguna yang tidak berwenang. Permintaan untuk halaman Indeks diizinkan dan diproses oleh Middleware MVC. Permintaan untuk laporan penjualan diizinkan dan diproses oleh Middleware laporan khusus.

Perhatikan bagaimana pada gambar di atas, middleware autentikasi memutus permintaan.

Memigrasikan kode modul ke middleware

Modul HTTP yang ada akan terlihat mirip dengan ini:

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

Seperti yang ditunjukkan di halaman Middleware, middleware ASP.NET Core adalah kelas yang memperkenalkan metode yang menerima Invoke dan mengembalikan HttpContext. Middleware baru Anda akan terlihat seperti ini:

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

Templat middleware sebelumnya diambil dari bagian tentang menulis middleware.

Kelas pembantu MyMiddlewareExtensions memudahkan konfigurasi middleware Anda di kelas Anda Startup . Metode UseMyMiddleware ini menambahkan kelas middleware Anda ke alur permintaan. Layanan yang dibutuhkan oleh middleware disuntikkan ke dalam konstruktor middleware.

Modul Anda mungkin mengakhiri permintaan, misalnya jika pengguna tidak diotorisasi:

// 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 menangani ini dengan tidak memanggil Invoke pada middleware berikutnya di pipeline. Perlu diingat bahwa ini tidak sepenuhnya mengakhiri proses permintaan, karena middleware yang sebelumnya akan tetap dieksekusi ketika respons kembali melalui alur.

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

Saat memigrasikan fungsionalitas modul ke middleware baru, Anda mungkin menemukan bahwa kode Anda tidak dikompilasi karena HttpContext kelas telah berubah secara signifikan di ASP.NET Core. Lihat Migrasi dari ASP.NET Framework HttpContext ke ASP.NET Core untuk mempelajari cara bermigrasi ke ASP.NET Core HttpContext baru.

Memigrasikan penyisipan modul ke dalam alur permintaan

Modul HTTP biasanya ditambahkan ke alur permintaan menggunakan 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>

Konversikan ini dengan menambahkan middleware baru Anda ke alur permintaan di kelas Anda 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?}");
    });
}

Tempat yang tepat dalam alur tempat Anda memasukkan middleware baru tergantung pada peristiwa yang ditanganinya sebagai modul (BeginRequest, , EndRequestdll.) dan urutannya dalam daftar modul Anda di Web.config.

Seperti yang dinyatakan sebelumnya, tidak ada siklus hidup aplikasi di ASP.NET Core dan urutan di mana respons diproses oleh middleware berbeda dari urutan yang digunakan oleh modul. Ini bisa membuat keputusan pemesanan Anda lebih menantang.

Jika pemesanan menjadi masalah, Anda dapat membagi modul Anda menjadi beberapa komponen middleware yang dapat dipesan secara independen.

Memuat opsi middleware menggunakan pola opsi

Beberapa modul memiliki opsi konfigurasi yang disimpan di Web.config. Namun, di ASP.NET Core, model konfigurasi baru digunakan sebagai pengganti Web.config.

Sistem konfigurasi baru memberi Anda opsi ini untuk menyelesaikan ini:

  1. Buat kelas untuk menyimpan opsi middleware Anda, misalnya:

    public class MyMiddlewareOptions
    {
        public string Param1 { get; set; }
        public string Param2 { get; set; }
    }
    
  2. Menyimpan nilai opsi

    Sistem konfigurasi memungkinkan Anda menyimpan nilai opsi di mana pun Anda inginkan. Namun, sebagian besar situs menggunakan appsettings.json, jadi kita akan mengambil pendekatan itu:

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

    MyMiddlewareOptionsSection di sini adalah nama bagian. Tidak harus sama dengan nama kelas opsi Anda.

  3. Mengaitkan nilai opsi dengan kelas opsi

    Pola opsi menggunakan ASP.NET kerangka kerja injeksi dependensi Core untuk mengaitkan jenis opsi (seperti MyMiddlewareOptions) dengan MyMiddlewareOptions objek yang memiliki opsi aktual.

    Perbarui kelas Anda Startup :

    1. Jika Anda menggunakan appsettings.json, tambahkan ke penyusun konfigurasi di Startup konstruktor:

      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. Konfigurasikan layanan opsi:

      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. Kaitkan opsi Anda dengan kelas opsi Anda:

      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. Masukkan opsi ke konstruktor middleware Anda. Ini mirip dengan menyuntikkan opsi ke pengontrol.

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

    Metode ekstensi UseMiddleware yang menambahkan middleware Anda ke IApplicationBuilder mengurus injeksi dependensi.

    Ini tidak terbatas pada IOptions objek. Objek lain yang diperlukan middleware Anda dapat disuntikkan dengan cara ini.

Memuat opsi middleware melalui injeksi langsung

Pola opsi memiliki keuntungan karena menciptakan kopling longgar antara nilai opsi dan konsumennya. Setelah Anda mengaitkan kelas opsi dengan nilai opsi aktual, kelas lain apa pun bisa mendapatkan akses ke opsi melalui kerangka kerja injeksi dependensi. Tidak perlu meneruskan nilai opsi.

Namun, ini tidak berfungsi jika Anda ingin menggunakan middleware yang sama dua kali dengan opsi yang berbeda. Misalnya middleware otorisasi yang digunakan di cabang yang berbeda yang memungkinkan peran yang berbeda. Anda tidak dapat mengaitkan dua objek opsi yang berbeda dengan satu kelas opsi.

Solusinya adalah mendapatkan objek opsi dengan nilai opsi aktual di kelas Anda Startup dan meneruskan objek tersebut langsung ke setiap instans middleware Anda.

  1. Menambahkan kunci kedua ke appsettings.json

    Untuk menambahkan sekumpulan opsi kedua ke appsettings.json file, gunakan kunci baru untuk mengidentifikasinya secara unik:

    {
      "MyMiddlewareOptionsSection2": {
        "Param1": "Param1Value2",
        "Param2": "Param2Value2"
      },
      "MyMiddlewareOptionsSection": {
        "Param1": "Param1Value",
        "Param2": "Param2Value"
      }
    }
    
  2. Ambil nilai opsi dan teruskan ke middleware. Metode ekstensi Use... (yang menambahkan middleware Anda ke pipeline) adalah tempat yang logis untuk meneruskan nilai opsi.

    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. Aktifkan middleware untuk mengambil parameter opsi. Berikan overload dari metode ekstensi Use... (yang menerima parameter opsi dan meneruskannya ke UseMiddleware). Ketika UseMiddleware dipanggil dengan parameter, ia meneruskan parameter ke konstruktor middleware Anda saat membuat instans objek 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));
        }
    }
    

    Perhatikan bagaimana ini membungkus objek opsi dalam OptionsWrapper objek. Ini mengimplementasikan IOptions, seperti yang diharapkan oleh konstruktor middleware.

Migrasi IHttpModule bertahap

Ada kalanya mengonversi modul ke middleware tidak dapat dengan mudah dilakukan. Untuk mendukung skenario migrasi di mana modul diperlukan dan tidak dapat dipindahkan ke middleware, adaptor System.Web mendukung penambahan modul ke ASP.NET Core.

Contoh IHttpModule

Untuk mendukung modul, instans HttpApplication harus tersedia. Jika tidak ada HttpApplication khusus yang digunakan, satu default akan digunakan untuk menambahkan modul. Peristiwa yang dideklarasikan dalam aplikasi kustom (termasuk Application_Start) akan didaftarkan dan dijalankan sesuai.

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

Migrasi Global.asax

Infrastruktur ini dapat digunakan untuk memigrasikan penggunaan Global.asax jika diperlukan. Sumber dari Global.asax adalah kustom HttpApplication dan file dapat disertakan dalam aplikasi ASP.NET Core. Karena diberi nama Global, kode berikut dapat digunakan untuk mendaftarkannya:

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

Selama logika di dalamnya tersedia di ASP.NET Core, pendekatan ini dapat digunakan untuk memigrasikan keandalan Global.asax secara bertahap ke ASP.NET Core.

Peristiwa autentikasi/Otorisasi

Agar peristiwa autentikasi dan otorisasi berjalan pada waktu yang diinginkan, pola berikut harus digunakan:

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

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

Jika ini tidak dilakukan, peristiwa akan tetap berjalan. Namun, hal itu akan terjadi selama panggilan .UseSystemWebAdapters().

Pengumpulan Modul HTTP

Karena modul dan aplikasi dalam ASP.NET Framework ditetapkan ke permintaan, instans baru diperlukan untuk setiap permintaan. Namun, karena mereka bisa mahal untuk dibuat, mereka dikumpulkan menggunakan ObjectPool<T>. Untuk menyesuaikan masa pakai yang sebenarnya dari instans HttpApplication, kumpulan kustom dapat digunakan:

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

Sumber daya tambahan