نوشتن Middleware سفارشی در ASP.NET Core
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: پنج دقیقه

در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 3 - Middleware چیست؟» با اصول مقدماتی Middlewareها آشنا شدیم. همچنین در مطلب «آشنایی با OWIN و بررسی نقش آن در ASP.NET Core» یک مثال سفارشی از آن‌ها، بررسی شد. در اینجا می‌خواهیم نکات بیشتری را در مورد تهیه‌ی Middlewareهای سفارشی بررسی کنیم.


تفاوت بین متدهای app.Use  و  app.Run در چیست؟

Middlewareها به همان ترتیبی که در متد Configure کلاس آغازین برنامه معرفی می‌شوند، اجرا خواهند شد؛ اما نکته‌ی مهم اینجا است که middleware ایی که توسط متد app.Use تعریف می‌شود، می‌تواند middleware بعدی ثبت شده‌را، فراخوانی کند؛ اما app.Run خیر. برای درک بهتر این مفهوم، به مثال زیر دقت کنید:
using Microsoft.AspNetCore.Http;

public class Startup
{
    public void Configure(IApplicationBuilder app)
    {
        app.Use(async (context, next) =>
        {
            await context.Response.WriteAsync("<div>from middleware-1, inside app.Use, before next()</div>");
 
            await next();
 
            await context.Response.WriteAsync("<div>from middleware-1, inside app.Use, after next()</div>");
        });
 
        app.Run(async context =>
        {
            await context.Response.WriteAsync("<div>Inside middleware-2 defined using app.Run</div>");
        });
 
        app.Use(async (context, next) =>
        {
            await context.Response.WriteAsync("<div>from middleware-3, inside app.Use, before next()</div>");
 
            await next();
 
            await context.Response.WriteAsync("<div>from middleware-3, inside app.Use, after next()</div>");
        });
اگر در این حالت برنامه را اجرا کنیم، چنین خروجی را مشاهده خواهیم کرد:


همانطور که در تصویر نیز مشخص است، ابتدا کدهای پیش از فراخوانی دلیگیت next میان‌افزار اول اجرا شده‌است. سپس باتوجه به فراخوانی دلیگیت next، کدهای دومین میان‌افزار ثبت شده، فراخوانی گردیده‌است و سپس کدهای پس از فراخوانی دلیگیت next میان‌افزار اول، اجرا شده‌اند.
این دلیگیت در اصل یک چنین امضایی را دارد:
 public delegate Task RequestDelegate(HttpContext context);
در اینجا چون میان‌افزار دوم از نوع app.Run است و قابلیت فراخوانی دلیگیت next را ندارد، از نوع terminal یا خاتمه دهنده به‌شمار آمده و دیگر میان افزار بعدی ثبت شده، یعنی میان‌افزار سوم، اجرا نخواهد شد و کار پردازش برنامه در همین مرحله به پایان می‌رسد.

باید دقت داشت که فراخوانی دلیگیت next در میان‌افزارهای از نوع app.Use الزامی نبوده و اگر اینکار انجام نشود، بین app.Run و app.Use تفاوتی نخواهد بود و هر دو terminal به حساب می‌آیند.


تفاوت بین متدهایapp.Map  و  app.MapWhen در چیست؟

متد app.Map در صورت برآورده شدن شرطی، سبب اجرای میان‌افزاری مشخص می‌شود (امکان اجرای غیر خطی میان‌افزارها).
فرض کنید قطعه کد زیر را پس از اولین app.Use مثال فوق قرار داده‌ایم:
app.Map("/dnt", appBuilder =>
{
    appBuilder.Run(async context =>
    {
        await context.Response.WriteAsync(@"<div>Inside Map(/dnt) --> Run</div>");
    });
});
در این حالت اگر برنامه را اجرا کنیم، خروجی جدیدی را مشاهده نخواهیم کرد و خروجی حاصل دقیقا مانند تصویر مثال فوق است. اما اگر آدرس ویژه‌ی dnt/ درخواست شود (الگوی تطابق اولین پارامتر متد Map)، آنگاه میان افزار ثبت شده‌ی app.Run ویژه‌ی این حالت خاص، اجرا می‌شود:


در اینجا چون app.Run داخلی فراخوانی شده، از نوع terminal است، دیگر میان افزارهای پس از آن اجرا نشده‌اند. بدیهی است در اینجا نیز می‌توان به هر تعدادی که نیاز است میان افزارهای جدیدی را به appBuilder متد app.Map اضافه کرد.

پارامتر اول متد Map برای تطابق با الگوهایی خاص و مشخص، مناسب است. اما در اگر در اینجا نیاز به اطلاعات بیشتری از HttpContext جاری داشته باشیم، می‌توانیم از متد app.MapWhen استفاده کنیم که اولین پارامتر آن یک دلیگیت است که HttpContext را در اختیار استفاده کننده قرار می‌دهد و اگر در نهایت true را دریافت کند، سبب اجرای میان افزارهای قسمت appBuilder آن خواهد شد:
app.MapWhen(context =>
{
    return context.Request.Query.ContainsKey("dnt");
},
appBuilder =>
{
    appBuilder.Run(async context =>
    {
        await context.Response.WriteAsync(@"<div>Inside MapWhen(?dnt) --> Run</div>");
    });
});
در این مثال، شرط ارائه شده‌ی در پارامتر اول، اندکی پیچید‌ه‌تر است از حالت app.Map. در اینجا مشخص شده‌است که اگر آدرس دریافتی از کاربر، دارای کوئری استرینگی به نام dnt بود، آنگاه میان افزار(های) ارائه شده‌ی در قسمت appBuilder، اجرا شود. برای نمونه درخواست آدرس ذیل، سبب فراخوانی appBuilder.Run ذکر شده می‌شود:
 http://localhost:7742/?dnt=true



نظم بخشیدن به تعاریف میان‌افزارها

متدهای app.Run و app.Use و امثال آن‌ها برای تعریف سریع میان افزارها مناسب هستند. اما اگر بخواهیم کدهای کلاس آغازین برنامه را اندکی خلوت کرده و به تعاریف میان‌افزارها نظم ببخشیم، می‌توان کدهای آن‌ها را به کلاس‌هایی با امضایی خاص منتقل کرد:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
 
namespace Core1RtmEmptyTest.StartupCustomizations
{
    public class MyMiddleware1
    {
        private readonly RequestDelegate _next;
 
        public MyMiddleware1(RequestDelegate next)
        {
            _next = next;
        }
 
        public async Task Invoke(HttpContext context)
        {
                    context.Response.ContentType = "text/html";
                    context.Response.StatusCode = 200;
            await context.Response.WriteAsync("<div>Hello from MyMiddleware1.</div>");
            await _next.Invoke(context);
            await context.Response.WriteAsync("<div>End of action.</div>");
        }
    } 
}
در اینجا نحوه‌ی تعریف یک کلاس میان‌افزار سفارشی را مشاهده می‌کنید. دو پارامتر context و next ایی را که در متد app.Use مشاهده کردید، دراینجا به نحو واضح‌تری مشخص شده‌اند. دلیگیت next اشاره کننده‌ی به اجرای میان افزار بعدی، در سازنده‌ی کلاس این میان افزار تزریق شده‌است. همچنین در اینجا می‌توان سرویس‌هایی را که به IoC Container توکار ASP.NET Core معرفی کرده‌ایم نیز تزریق کنیم و از این لحاظ محدودیتی ندارد (و همچنین امضای آن می‌تواند کاملا متغیر باشد). قسمت اصلی اجرایی این میان افزار، متد Invoke آن است که اطلاعات HttpContext جاری را در اختیار مصرف کننده قرار می‌دهد.
در اینجا نیز اگر دلیگیت next_ فراخوانی نشود، این میان‌افزار سبب خاتمه‌ی اجرای پردازش درخواست جاری می‌گردد.

 مرحله‌ی بعد، روش معرفی این میان‌افزار تعریف شده، به لیست میان‌افزارهای موجود است. برای این منظور می‌توان متد app.UseMiddleware را به صورت مستقیم در کلاس آغازین برنامه فراخوانی کرد و یا مرسوم است اگر کتابخانه‌ای را طراحی کرده‌اید، به نحو ذیل متد الحاقی خاصی را برای آن تدارک دید:
using Microsoft.AspNetCore.Builder;

public static class MyMiddlewareExtensions
{
    public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder app)
    {
        app.UseMiddleware<MyMiddleware1>();
        return app;
    }
}
سپس مصرف کننده تنها باید این متد را در کلاس آغازین برنامه فراخوانی کند:
public void Configure(IApplicationBuilder app)
{
    app.UseMyMiddleware();
  • #
    ‫۷ سال و ۶ ماه قبل، دوشنبه ۷ فروردین ۱۳۹۶، ساعت ۱۴:۴۳
    یک نکته‌ی تکمیلی: انتقال کدهای IHttpModule‌ها به میان افزارها

    معادل BeginRequest و EndRequest در ماژول‌های نگارش‌های پیشین ASP.NET:
    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.
            }
        }
    }
    دقیقا مکان‌های پیش و پس از فراخوانی await _next در میان افزارها هستند:
    public class EndRequestMiddleware
    {
        private readonly RequestDelegate _next;
    
        public EndRequestMiddleware(RequestDelegate next)
        {
            _next = next;
        }
    
        public async Task Invoke(HttpContext context)
        {
            // Do tasks before other middleware here, aka 'BeginRequest'
            // ...
    
            // Let the middleware pipeline run
            await _next(context);
    
            // Do tasks after middleware here, aka 'EndRequest'
            // ...
        }
    }
  • #
    ‫۶ سال و ۶ ماه قبل، سه‌شنبه ۲۲ اسفند ۱۳۹۶، ساعت ۲۰:۳۲
    سلام وقت بخیر 
    چطور میشه به Url و Body , Header  درخواست‌ها و همچنین Body  پاسخ سرور  در میان افزار‌ها دسترسی پیدا کرد ؟ 
    من با استفاده از کد زیر نتونستم body  پاسخ رو بخونم 
    string responseBody = new StreamReader(context.Response.Body).ReadToEnd();
    بر روی همین خط ، خطای زیر نمایش داده میشه
    ArgumentException: Stream was not readable.

  • #
    ‫۶ سال و ۶ ماه قبل، شنبه ۲۶ اسفند ۱۳۹۶، ساعت ۱۵:۰۶
    سلام وقت بخیر 
    من یک میان افزار دارم با سازنده  زیر :
     public IPCheckMiddleware(  RequestDelegate next ,IIP iP)
            {
                _next = next;
                _IP = iP;
            }
    و سرویس IIP  که در سازنده صدا زده شده  بدین صورت است :
     public class IPService:IIP
        {
            private readonly IHttpContextAccessor _Http;
            private readonly IUnitOfWork _uow;
            private readonly DbSet<AccessIp> _IP;
    
            public IPService(IHttpContextAccessor http , IUnitOfWork unitOfWork)
            {
                _Http = http;
                _uow = unitOfWork;
                _IP = _uow.Set<AccessIp>();
            }
    }
      و همچنین سرویس‌های بنده بصورت زیر اضافه شده اند :
                services.AddScoped<IUnitOfWork, ApplicationDbContext>();
                services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
                services.AddScoped<IIP, IPService>();
     متن خطایی که با آن مواجه میشوم :
    An error occurred while starting the application.
    InvalidOperationException: Cannot resolve scoped service 'MohasebKhodro.Services.Shared.IIP' from root provider.
    
    Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.ValidateResolution(Type serviceType, ServiceProvider serviceProvider)
    
        InvalidOperationException: Cannot resolve scoped service 'MohasebKhodro.Services.Shared.IIP' from root provider.
            Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteValidator.ValidateResolution(Type serviceType, ServiceProvider serviceProvider)
            Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
            Microsoft.Extensions.Internal.ActivatorUtilities+ConstructorMatcher.CreateInstance(IServiceProvider provider)
            Microsoft.Extensions.Internal.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters)
            Microsoft.AspNetCore.Builder.UseMiddlewareExtensions+<>c__DisplayClass4_0.<UseMiddleware>b__0(RequestDelegate next)
            Microsoft.AspNetCore.Builder.Internal.ApplicationBuilder.Build()
            Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication()
    همچنین با تغییر روش اضافه کردن سرویس IIP  به AddTransient باز هم مشکل وجود دارد .
    چه راه حلی وجود دارد ؟
    • #
      ‫۶ سال و ۶ ماه قبل، شنبه ۲۶ اسفند ۱۳۹۶، ساعت ۱۵:۴۸
      طول عمر میان افزارها به صورت singleton تعریف می‌شود. به همین جهت فقط وابستگی‌هایی با طول عمر singleton را هم می‌توانید در سازنده‌ی آن‌ها تزریق کنید؛ مانند IHttpContextAccessor در مثال شما. سایر مواردی که این طول عمر را ندارند، باید به این صورت تزریق شوند:
      public async Task Invoke(HttpContext context, IIP iip)
      {
      
      }
      در این حالت هست که طول عمرهایی مانند scoped یا transient معنا پیدا می‌کنند. اگر سرویس‌هایی با این طول عمرها در سازنده‌ی یک کلاس singleton تزریق شوند، دیگر هیچگاه وهله سازی مجدد نخواهند شد و دقیقا به صورت singleton رفتار می‌کنند؛ چون سازنده‌ی کلاس singleton فقط یکبار در طول عمر برنامه فراخوانی می‌شود.
  • #
    ‫۵ سال و ۶ ماه قبل، یکشنبه ۱۹ اسفند ۱۳۹۷، ساعت ۱۴:۲۱
    چطور می‌توان به کمک Middleware‌ها درخواست‌های سمت کلاینت را به ترتیب اجرا کرد؟ یعنی اگر به صورت همزمان چندین درخواست به سرور ارسال شده باشد بتوان به ترتیب درخواست‌ها را اجرا کرد به عبارت دیگر تا پاسخ درخواست اول به کلاینت برگشت داده نشده باشد درخواست دوم اجرا نشود؟
    • #
      ‫۵ سال و ۶ ماه قبل، یکشنبه ۱۹ اسفند ۱۳۹۷، ساعت ۱۴:۳۴
      « CorrelationId   An ASP.NET Core middleware component which synchronises a correlation ID for cross API request logging»
      همچنین sp_getapplock را هم مدنظر داشته باشید.
    • #
      ‫۵ سال و ۶ ماه قبل، دوشنبه ۲۰ اسفند ۱۳۹۷، ساعت ۱۴:۱۱
      یکی از روش‌های مقابله با مشکل فوق استفاده از کلاس SemaphoreSlim می باشد که در NET Framework 4.0 معرفی شده و در فضای نام  System.Threading در دسترس می‌باشد.
      اگر اکشن متد‌های شما به صورت async await ایجاد کرده اید بهتر هست  ابتدا کلاس زیر را ایجاد نمایید:
      using System;
      using System.Threading;
      using System.Threading.Tasks;
       
      namespace MyApp
      {
          public class AsyncLock : IDisposable
          {
              private SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
       
              public async Task<AsyncLock> LockAsync()
              {
                  await _semaphoreSlim.WaitAsync();
                  return this;
              }
       
              public void Dispose()
              {
                  _semaphoreSlim.Release();
              }
          }
      }
      سپس به صورت زیر از آن استفاده کنید:
      private static readonly AsyncLock _mutex = new AsyncLock();
       
      using(await _mutex.LockAsync())
      {
          // Critical section... You can await here!
      }
      در این صورت تمامی درخواست‌های به سمت سرور به ترتیب اجرا خواهند شد و دیگر مشکل فوق را نخواهیم داشت.
      • #
        ‫۵ سال و ۶ ماه قبل، دوشنبه ۲۰ اسفند ۱۳۹۷، ساعت ۱۴:۵۹
        برای پیاده سازی قفل گذاری در سطح برنامه:
        - سمت دیتابیس آن استفاده از تراکنش‌ها و isolation level است و یا مواردی مانند sp_getapplock .
        - بهتر است برای پیاده سازی AsyncLock از کتابخانه‌ی « AsyncEx » استفاده کنید، چون نکات زیادی را به همراه دارد. 
        - همچنین برای کپسوله سازی آن بهتر است از فیلترها استفاده شود (مثلا یک فیلتر [DisableConcurrentExecution] را بر این اساس ایجاد کنید) تا صرفا یک اکشن متد خاص را تحت کنترل قرار دهند و نه مانند میان‌افزارها که تمام سیستم را داخل lock قرار می‌دهند.
        • #
          ‫۵ سال و ۶ ماه قبل، سه‌شنبه ۲۱ اسفند ۱۳۹۷، ساعت ۱۲:۵۰
          آیا راهی وجود دارد که از طریق فیلترها بتوان کتابخونه فوق رو شبیه به OutputCache  به صورت زیر کپسوله و طراحی کرد:
           [OutputCache(Duration = 60, VaryByParam = "none")]
          یعنی 
          [ServiceFilter(typeof(LockFilter(Duration = 60, VaryByParam = "none" )))]  
  • #
    ‫۵ سال و ۳ ماه قبل، چهارشنبه ۸ خرداد ۱۳۹۸، ساعت ۱۵:۳۰
    چطور می‌توانیم در Middleware‌های سفارشی شده به ModelState دسترسی داشته باشیم؟
    • #
      ‫۵ سال و ۳ ماه قبل، چهارشنبه ۸ خرداد ۱۳۹۸، ساعت ۱۵:۴۰
      - انتخاب Middleware برای دسترسی به ModelState کار اشتباهی است؛ چون پس از فراخوانی میان‌افزار MVC و پردازش‌های model binding و validation آن است که کار مقدار دهی ModelStateDictionary صورت می‌گیرد. در حالیکه یک میان‌افزار را در هر قسمتی از pipeline می‌توان قرار داد و الزامی ندارد که پس از میان‌افزار MVC باشد.
      - اگر نیاز به ModelState است، استفاده از اکشن‌فیلترها برای آن توصیه می‌شود که قابلیت تعریف سراسری هم دارند:
      public class ModelStateFeatureFilter : IAsyncActionFilter
      {
          public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
          {
              var state = context.ModelState;
              // store state ...
              await next();
          }
      }
      یا می‌توان این context.ModelState را در جائی ذخیره کرد و سپس در یک میان‌افزار که پس از میان‌افزار MVC قرار می‌گیرد، آن‌را خواند.
  • #
    ‫۱ ماه قبل، سه‌شنبه ۲۳ مرداد ۱۴۰۳، ساعت ۱۸:۵۲

    یک نکته‌ی تکمیلی: کار با اینترفیس IMiddleware جهت تعریف میان‌افزارهای سفارشی

    اگر امروز قصد تعریف میان‌افزارهای سفارشی را دارید، بهتر است از روش باز public async Task Invoke(HttpContext context) که در این مطلب معرفی شد، دیگر استفاده نکنید؛ چون مهم‌ترین محدودیت‌های آن، داشتن طول عمر Singleton غیرقابل تغییر و همچنین عدم امکان پیاده سازی اینترفیس IDisposable در آن جهت پاکسازی خودکار منابع است. امروز روش توصیه شده‌، استفاده از اینترفیس IMiddleware است. در این حالت متد Task Invoke فوق، به متد مشخص و ثابت Task InvokeAsync(HttpContext context, RequestDelegate next) تغییر می‌کند. چون در این حالت دیگر نمی‌توان پارامترهای این متد مشخص را مانند قبل که اینترفیسی را پیاده سازی نمی‌کرد، به صورت پویا کم و زیاد کرد، می‌توان سرویس‌های مدنظر را به سازنده‌ی کلاس، تزریق کرد. به همین جهت نیاز است، آن‌را به نحو زیر به سیستم تزریق وابستگی‌ها معرفی کرد:

    builder.Services.AddTransient<MyNewMiddleware>();

    این الزام به تعریف آن به صورت یک سرویس رسمی، مزیت‌های زیر را به همراه دارد:

    الف) می‌توان طول عمری، غیر از Singleton را هم در صورت نیاز، تعریف کرد (و مشکل کار با سرویس‌هایی با طول عمرهای غیر از Singleton کمتر می‌شود).

    ب) چون طول عمر این میان‌افزار اکنون توسط سیستم تزریق وابستگی‌ها مدیریت می‌شود، اگر این میان‌افزار اینترفیس IDisposable را پیاده سازی کند، کار پاکسازی منابع آن خودکار خواهد شد.

    نکته 1: روش معرفی آن به سیستم تزریق وابستگی‌ها، به صورت Concrete type است؛ یعنی اصل کلاس باید معرفی شود (مانند سطر فوق) و نه اینکه به صورت متداول زیر به همراه ذکر اینترفیس IMiddleware باشد:

    builder.Services.AddTransient<IMiddleware, MyNewMiddleware>();

    مابقی کار با آن، با میان‌افزارهای متداول، تفاوتی ندارد. یعنی قسمت UseMiddleware آن یکی است:

    app.UseMiddleware<MyNewMiddleware>();

    بنابراین این روش نسبت به روش متداول قبلی، دو تفاوت پیاده سازی اینترفیس مشخص IMiddleware و ثبت کلاس آن به صورت یک سرویس رسمی را دارد؛ مابقی نکات آن، مانند قبل است.

    نکته 2: اگر از Scrutor برای ثبت خودکار سرویس‌های برنامه استفاده می‌کنید، روش ثبت خودکار اینگونه سرویس‌ها به صورت زیر و با استفاده از متد ()AsSelf است:

    services.Scan(scan => scan.FromAssembliesOf(typeof(IDataSeedersRunner))
                .AddClasses(classes => classes.Where(type =>
                {
                    var allInterfaces = type.GetInterfaces();
    
                    return allInterfaces.Contains(typeof(IMiddleware)) && 
                           allInterfaces.Contains(typeof(ISingletonService));
                }))
                .AsSelf()
                .WithSingletonLifetime());

    در این مثال، تمام IMiddleware هایی که با نشانگر ISingletonService هم مزین شده‌اند، یافت شده و به صورت Concrete type هایی، با طول عمر Singleton، به سیستم تزریق وابستگی‌ها اضافه می‌شوند.