معرفی Middleware در ASP.NET Core

Middleware یک نرم افزار است که به یک اپلیکیشن pipeline برای مدیریت درخواست ها و پاسخ ها اسمبل شده است. بنابراین در این مقاله می خواهیم ASP.NET Core Middleware را با یکدیگر بطور کامل بررسی کنیم.

معرفی Middleware در ASP.NET Core

Middleware چیست؟

Middleware یک نرم افزار است که به یک اپلیکیشن pipeline برای مدیریت درخواست ها و پاسخ ها اسمبل شده است. هر کامپوننت به صورت زیر است:

انتخاب می کند که آیا درخواست ها را به کامپوننت بعدی در pipeline ارسال کند یا نه.

می تواند کار ها را قبل و بعد از اینکه کامپوننت بعدی در pipeline استفاده شود، انجام دهد.

request delegate ها برای ساخت pipeline درخواست ها استفاده می شوند. request delegate ها ، هر درخواست HTTP را مدیریت می کند.

Request delegate ها با استفاده از متد های Run ، Map و Use extension پیکربندی می شوند. یک request delegate شخصی می تواند به صورت in-line و به عنوان یک متد anonymous (in-line middleware نامیده می شود) و یا در یک کلاس با قابلیت استفاده ی مجدد تعریف شود. این کلاس های با قابلیت استفاده ی مجدد و متد های in-line anonymous ، middleware یا کامپوننت های middleware هستند. هر کامپوننت middleware در پایپ لاین request ها مسئول invoke کردن کامپوننت بعدی در pipeline یا قطع زنجیره در صورت لزوم است.

Migrate HTTP Modules to Middleware (مهاجرت ماژول های HTTP به Middleware) تفاوت بین پایپ لاین request ها در ASP.NET Core و ASP.NET 4.x را توضیح می دهد و نمونه های بیشتری از middleware را ارائه می دهد.

ساخت یک پایپ لاین middleware با IApplicationBuilder

پایپ لاین request ها در ASP.NET Core از ترتیب request delegate ها همانطور که در نمودار زیر نشان داده است یکی پس از دیگری فراخوانی می شوند، تشکیل شده است :

هر delegate می تواند عملیات ها را قبل و پس از delegate بعدی انجام دهد. یک delegate می تواند تصمیم بگیرد که یک request را به delegate بعدی ارسال نکند که به این کار قطع request های پایپ لاین می گویند. این قطع کردن، معمولا مطلوب است زیرا از کارهای غیرضروری جلوگیری می کند. برای مثال فایل استاتیک middleware می تواند یک request به یک فایل استاتیک برگرداند و باقی پایپ لاین را قطع کند. Delegate های مدیریت Exception باید درهمان ابتدا در پایپ لاین فراخوانی شوند تا بتوانند Exception هایی که بعد ها در مراحل مختلف پایپ لاین اتفاق می افتد را بگیرند.

ساده ترین اپلیکیشن ممکن ASP.NET Core یک delegate برای request تنظیم می کند که همه ی request ها را مدیریت می کند این مورد شامل یک پایپ لاین واقعی نیست در عوض یک تابع anonymous برای پاسخ به هر درخواست HTTP فراخوانی می شود.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello, World!");
        });
    }
}

اولین delegate در app.Run پایپ لاین را مشخص می کند.

شما می توانید delegate هایی از request های چندگانه را با استفاده از app.Use زنجیر کنید. پارامتر next، delegate بعدی در پایپ لاین را نشان می دهد. ( به خاطر داشته باشید که شما می توانید پایپ لاین را با فراخوانی نکردن پارامتر next قطع کنید.) همانطور که در این مثال نشان داده شده است شما معمولا می توانید عملیات ها را هم قبل و هم بعد از delegate بعدی انجام دهید:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            // Do work that doesn't write to the Response.
            await next.Invoke();
            // Do logging or other work that doesn't write to the Response.
        });

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from 2nd delegate.");
        });
    }
}

هشدار

next.Invoke را پس ازاینکه پاسخ به کلاینت ارسال شد فراخوانی نکنید. تغییرات در HttpResponse پس ازاینکه پاسخ شروع شد باعث exception می شود. برای مثال تغییراتی نظیر تنظیم header ها، کد وضعیت(status code) وغیره باعث یک exception می شود. نوشتن در بدنه ی پاسخ پس از فراخوانی next:

ممکن است باعث نقض پروتکل شود. برای مثال نوشتن بیشتر از طول محتوای تعیین شده باعث این مشکل می شود.

ممکن است فرمت body را خراب کند. برای مثل نوشتن یک footer با HTML در یک فایل CSS باعث ایجاد این مشکل می شود.

HttpResponse.HasStarted یک سرنخ کارا برای مشخص کردن این است که آیا header ها ارسال شده اند یا نه و/یا بدنه نوشته شده است یا نه.

ترتیب (Ordering)

ترتیبی که کامپوننت های middleware در متد Configure اضافه می شوند ترتیبی که آن ها روی request ها invoke می شوند را مشخص می کند و ترتیب عکس هم ترتیب response ها را مشخص می کند. این ترتیب برای امنیت، کارایی و عملکرد بسیار مهم است.

متد Configure (در ادامه نشان داده است) کامپوننت های middleware زیر را اضافه می کند:

مدیریت Exception/خطا

فایل سرور استاتیک

اهرازهویت

MVC

ASP.NET Core 2.x

public void Configure(IApplicationBuilder app)
{
    app.UseExceptionHandler("/Home/Error"); // Call first to catch exceptions
                                            // thrown in the following middleware.

    app.UseStaticFiles();                   // Return static files and end pipeline.

    app.UseAuthentication();               // Authenticate before you access
                                           // secure resources.

    app.UseMvcWithDefaultRoute();          // Add MVC to the request pipeline.
}

ASP.NET Core 1.x

public void Configure(IApplicationBuilder app)
{
    app.UseExceptionHandler("/Home/Error"); // Call first to catch exceptions
                                            // thrown in the following middleware.

    app.UseStaticFiles();                   // Return static files and end pipeline.

    app.UseIdentity();                     // Authenticate before you access
                                           // secure resources.

    app.UseMvcWithDefaultRoute();          // Add MVC to the request pipeline.
}

در کد بالا UseExceptionHandler اولین کامپوننت middleware اضافه شده به پایپ لاین است بنابراین تمام exception هایی که در فراخوانی های بعدی اتفاق می افتد را می گیرد.

فایل استاتیک middleware در همان ابتدای پایپ لاین فراخوانی شده است بنابراین می تواند request ها و قطع کردن ها را بدون رفتن به کامپوننت های بعدی مدیریت کند. فایل استاتیک middleware هیچ امکان بررسی را ارائه نمی دهد. هر فایلی که توسط این موارد ارائه می شود از جمله مواردی که در wwwroot هستند بطور عمومی دردسترس هستند. فایل های استاتیک را به عنوان روشی برای امنیت فایل های استاتیک بررسی کنید.

ASP.NET Core 2.x

اگر request به وسیله ی فایل استاتیک middleware مدیریت نشده بود به Identity middleware (app.UseAuthentication) که اهراز هویت را انجام می دهد، ارسال می شود. در اینجا request های اهرازهویت نشده قطع نمی شوند و اگرچه Identity ، request ها را اعتبارسنجی می کند اما مجوزدهی (و حذف کردن ها) تنها پس از اینکه MVC یک Razor Page خاص یا controller و اکشن را انتخاب می کند، اتفاق می افتد.

ASP.NET Core 1.x

اگر request به وسیله ی فایل استاتیک middleware مدیریت نشده بود به Identity middleware (app.UseIdentity) که اهراز هویت را انجام می دهد، ارسال می شود. در اینجا request های اهرازهویت نشده قطع نمی شوند و اگرچه Identity ، request ها را اعتبارسنجی می کند اما مجوزدهی (و حذف کردن ها) تنها پس از اینکه MVC یک Razor Page خاص یا controller و اکشن را انتخاب می کند، اتفاق می افتد.

مثال زیر یک ترتیب middleware که request ها برای فایل های استاتیک قبل از فشرده سازی پاسخ middleware توسط فایل استاتیک middleware مدیریت می شوند را نشان می دهد. فایل های استاتیک با این ترتیب از middleware ها فشرده سازی نمی شود. پاسخ های MVC از UseMvcWithDefaultRoute می توانند فشرده شوند.

public void Configure(IApplicationBuilder app)
{
    app.UseStaticFiles();         // Static files not compressed
                                  // by middleware.
    app.UseResponseCompression();
    app.UseMvcWithDefaultRoute();
}

استفاده از Run و Map

شما پایپ لاین HTTP را با استفاده از Use ، Run و Map پیکربندی می کنید. متد Use می تواند پایپ لاین را قطع کند ( یعنی اگر next برای request delegate فراخوانی نشود.) Run یک قرارداد است و برخی از کامپوننت های middleware ممکن است از متد های Run [Middleware] که در انتهای پایپ لاین اجرا می شوند، استفاده نکنند.

افزونه های Map* به عنوان یک قرارداد برای شاخه بندی(branching) پاپ لاین استفاده می شود. Map پایپ لاین request را برمبنای منطبق شدن با مسیر داده شده منشعب می کند. اگر مسیر request با همان مسیر داده شده شروع شود، آن شاخه اجرا می شود.

public class Startup
{
    private static void HandleMapTest1(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 1");
        });
    }

    private static void HandleMapTest2(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Map Test 2");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Map("/map1", HandleMapTest1);

        app.Map("/map2", HandleMapTest2);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

جدول زیر request ها و response ها از http://localhost:1234 را با استفاده از کد قبلی نشان می دهد:

زمانی که Map استفاده می شود سگمنت های مسیر منطبق شده از HttpRequest.Path حذف می شود و برای هر Request به HttpRequest.PathBase ملحق می شود.

 MapWhenپایپ لاین request را برمبنای پیش فرض داده شده منشعب می کند. هر پیش فرض از نوع Func<HttpContext, bool> می تواند برای نگاشت request ها به یک شاخه ی جدید از پایپ لاین استفاده شود. در مثال زیر یک پیش فرض برای مشخص کردن وجود یک شاخه ی متغیر رشته ای کوئری وجود دارد:

public class Startup
{
    private static void HandleBranch(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            var branchVer = context.Request.Query["branch"];
            await context.Response.WriteAsync($"Branch used = {branchVer}");
        });
    }

    public void Configure(IApplicationBuilder app)
    {
        app.MapWhen(context => context.Request.Query.ContainsKey("branch"),
                               HandleBranch);

        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello from non-Map delegate. <p>");
        });
    }
}

جدول زیر request ها و response ها از http://localhost:1234 با استفاده از کد قبلی را نشان می دهد:

Map از موارد تودرتو نیز پشتیبانی می کند. برای مثال:

app.Map("/level1", level1App => {
       level1App.Map("/level2a", level2AApp => {
           // "/level1/level2a"
           //...
       });
       level1App.Map("/level2b", level2BApp => {
           // "/level1/level2b"
           //...
       });
   });

Map همچنین می تواند سگمنت های چندگانه را به یکباره با یکدیگر منطبق کند. برای مثال:

app.Map("/level1/level2", HandleMultiSeg);

Middleware های داخلی

ASP.NET Core کامپوننت های middleware زیر را دارد و همچنین توصیفی از ترتیبی که باید اضافه شوند نیز وجود دارد:

 نوشتن middleware

دراصل middleware در یک کلاس کپسوله سازی می شود و با یک متد افزونه ای نمایش داده می شود. Middleware زیر را درنظر بگیرید که culture را برای request جاری از رشته ی کوئری تنظیم می کند.

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use((context, next) =>
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;
            }

            // Call the next delegate/middleware in the pipeline
            return next();
        });

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });

    }
}

توجه: کد نمونه ی بالا برای ساخت یک کامپوننت middleware نوشته شده است. سراسری سازی و محلی سازی برای پشتیبانی از محلی سازی های داخلی  ASP.NET Core را بررسی کنید.

شما می توانید middleware را با ارسال در culture آزمایش کنید. برای مثال: http://localhost:7997/?culture=no

کد زیر delegate مربوط به یک middleware را به یک کلاس انتقال می دهد:

using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;

namespace Culture
{
    public class RequestCultureMiddleware
    {
        private readonly RequestDelegate _next;

        public RequestCultureMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            var cultureQuery = context.Request.Query["culture"];
            if (!string.IsNullOrWhiteSpace(cultureQuery))
            {
                var culture = new CultureInfo(cultureQuery);

                CultureInfo.CurrentCulture = culture;
                CultureInfo.CurrentUICulture = culture;

            }

            // Call the next delegate/middleware in the pipeline
            await _next(context);
        }
    }
}

توجه

در ASP.NET Core 1.x نام متد های Task در middleware باید Invoke باشد. در ASP.NET Core 2.0 یا به بعد نام می تواند Invoke یا InvokeAsync باشد.

متد افزونه ای زیر middleware را از طریق IApplicationBuilder به نمایش می گذارد:

using Microsoft.AspNetCore.Builder;

namespace Culture
{
    public static class RequestCultureMiddlewareExtensions
    {
        public static IApplicationBuilder UseRequestCulture(
            this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<RequestCultureMiddleware>();
        }
    }
}

کد زیر middleware را از Configure فراخوانی می کند:

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseRequestCulture();

        app.Run(async (context) =>
        {
            await context.Response.WriteAsync(
                $"Hello {CultureInfo.CurrentCulture.DisplayName}");
        });

    }
}

Middleware باید اصول وابستگی صریح را به وسیله ی به نمایش گذاشتن وابستگی های خود در constructor، دنبال کند. Middleware در طول حیات هر اپلیکیشن یک بار ساخته می شود. اگر می خواهید سرویس ها را با middleware در یک request به اشتراک بگذارید، در ادامه وابستگی های Per-request را مطالعه کنید.

کامپوننت های Middleware می توانند وابستگی های خود را از طریق تزریق وابستگی ها از طریق پارامتر های constructor تامین کنند. همچنین UseMiddleware<T> می تواند پارامتر های اضافی را مستقیما بپذیرد.

وابستگی های Per-request

 چون middleware در تنظیمات اپلیکیشن ساخته می شود ونه در هر request ،بنابراین محدوده ی طول عمر سرویس های استفاده شده توسط سازنده های Middleware با سایر انواع وابستگی های تزریق شده در هر request به اشتراک گذاشته نمی شود. اگر شما باید یک محدوده ی سرویس بین middleware خودتان وسایر انواع دیگر به اشتراک بگذارید این سرویس ها را به متد های Invoke اضافه کنید. متد Invoke می تواند پارامتر های اضافی که توسط تزریق وابستگی ها جمع شده اند را بپذیرد. برای مثال:

public class MyMiddleware
{
    private readonly RequestDelegate _next;

    public MyMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext, IMyScopedService svc)
    {
        svc.MyProperty = 1000;
        await _next(httpContext);
    }
}