مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت چهارم - پرهیز از الگوی Service Locator در برنامه‌های وب
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: سه دقیقه

الگوی Service locator را در قسمت دوم بررسی کردیم. همانطور که عنوان شد، بهتر است تا جائیکه امکان دارد از بکارگیری آن به علت ضدالگو بودن پرهیز کرد. در ادامه قسمت‌های مختلف یک برنامه‌ی ASP.NET Core را که می‌توان بدون نیاز به استفاده‌ی الگوی Service locator، تزریق وابستگی‌ها را در آن‌ها انجام داد، مرور می‌کنیم.


در کلاس آغازین برنامه

در اینجا در متد Configure آن تنها کافی است اینترفیس سرویس مدنظر خود را مانند IAmACustomService، به صورت یک پارامتر جدید اضافه کنید. کار وهله سازی آن توسط Service Provider برنامه به صورت خودکار صورت می‌گیرد:
public class Startup 
{ 
    public void ConfigureServices(IServiceCollection services) { } 
  
    public void Configure(IApplicationBuilder app, IAmACustomService customService) 
    { 
        // ....    
    }         
}

یک نکته‌ی مهم: اگر طول عمر IAmACustomService را Scoped تعریف کرده‌اید و این سرویس از نوع IDisposable نیز می‌باشد، این روش کارآیی نداشته و باید از نکته‌ی «روش صحیح Dispose اشیایی با طول عمر Scoped، در خارج از طول عمر یک درخواست ASP.NET Core» که در قسمت قبل معرفی شد استفاده کنید.


در میان افزارها

هم سازنده‌ی یک میان افزار و هم متد Invoke آن قابلیت تزریق وابستگی‌ها را دارند:
public class TestMiddleware 
{ 
    public TestMiddleware(RequestDelegate next, IAmACustomService service) 
    { 
        // ... 
    } 
 
    public async Task Invoke(HttpContext context, IAmACustomService service) 
    { 
        // ... 
    }     
}
از سازنده‌ی آن برای تزریق وابستگی سرویس‌هایی با طول عمر Singleton استفاده کنید. ServiceProvider به همراه ویژگی است به نام Scope Validation. در این حالت اگر طول عمر سرویسی Singleton باشد (مانند طول عمر یک میان‌افزار) و در سازنده‌ی آن یک سرویس با طول عمر Scoped تزریق شود، در زمان اجرا یک استثناء را صادر می‌کند؛ چون در این حالت رفتار این سرویس Scoped نیز Singleton می‌شود که احتمالا مدنظر شما نیست. در این حالت از پارامترهای اضافی متد Invoke می‌توان برای تزریق وابستگی‌هایی با طول عمر Transient و یا Scoped استفاده کرد.
البته می‌توان این Scope Validation را در فایل program.cs به نحو زیر غیرفعال کرد، ولی بهتر است اینکار را انجام ندهید و همان مقدار پیش‌فرض آن بسیار مناسب است:
public static IWebHostBuilder CreateDefaultBuilder(string[] args) 
{ 
            var builder = new WebHostBuilder() 
//...
                .UseDefaultServiceProvider((context, options) => 
                { 
                    options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); 
                }) 
//...

در کنترلرها

سازنده‌های کنترلرهای برنامه‌های ASP.NET Core قابلیت تزریق وابستگی‌ها را دارند:
public class HelloController : Controller 
{ 
    private readonly IAmACustomService _customService; 
 
    public HelloController(IAmACustomService customService) 
    { 
        _customService = customService; 
    } 
 
    public IActionResult Get() 
    { 
        // ... 
    } 
}
در اینجا حتی می‌توان با استفاده از ویژگی FromServices، یک سرویس را توسط پارامترهای یک اکشن متد نیز درخواست کرد:
[HttpGet("[action]")]
public IActionResult Index([FromServices] IAmACustomService service)
{  
   // ...
}
در این حالت بجای model binding، کار دریافت این سرویس درخواستی صورت می‌گیرد.


در مدل‌ها

ویژگی FromServices بر روی مدل‌ها نیز کار می‌کند.
public IActionResult Index(TestModel model)
{
  // ...
}
در اینجا نحوه‌ی تعریف TestModel را به همراه ویژگی FromServices مشاهده می‌کنید:
public class TestModel 
{        
    public string Name { get; set; } 
 
    [FromServices] 
    public IAmACustomService CustomService { get; set; } 
}
این حالت که property injection نیز نام دارد، نیاز به خاصیتی با یک public setter را دارد.


در Viewها

در Razor Views نیز می‌توان توسط inject directive@ کار تزریق وابستگی‌ها را انجام داد:
 @inject IAmACustomService CustomService
 

در ویژگی‌ها و فیلترها

در ASP.NET Core تزریق وابستگی‌های در سازنده‌های فیلترها نیز کار می‌کند:
public class ApiExceptionFilter : ExceptionFilterAttribute  
{  
    private ILogger<ApiExceptionFilter> _logger;  
    private IHostingEnvironment _environment;  
    private IConfiguration _configuration;  
  
    public ApiExceptionFilter(IHostingEnvironment environment, IConfiguration configuration, ILogger<ApiExceptionFilter> logger)  
    {  
        _environment = environment;  
         _configuration = configuration;  
         _logger = logger;  
    }
در این حالت چون سازنده‌ی این ویژگی، پارامتر دار شده‌است و این پارامترها نیز یک مقدار ثابت قابل کامپایل نیستند، برای معرفی یک چنین فیلتری باید از ServiceFilterها به صورت زیر استفاده کرد:
[Route("api/[controller]")]  
[ApiController]  
[ServiceFilter(typeof(ApiExceptionFilter))]  
public class ValuesController : ControllerBase  
{
  • #
    ‫۵ سال و ۸ ماه قبل، شنبه ۲۲ دی ۱۳۹۷، ساعت ۱۹:۲۰
    نکته تکمیلی :
    در مقاله گفته شده که میتوان از خاصیت [FromServices] را بر روی Property داخل Model استفاده کرد (property injection)
    طبق مستندات دات نت، خاصیت [FromServices] فقط بر روی Parameter قابل استفاده است و نه Property 
    در واقع این امکان برای نسخه beta بوده و خیلی وقت است که منسوخ شده   است. ^  ^
  • #
    ‫۵ سال و ۸ ماه قبل، سه‌شنبه ۲۵ دی ۱۳۹۷، ساعت ۱۲:۴۸
    در زمان استفاده از [FromServices] بر روی property‌ها چنین خطایی می‌ده.
    Attribute 'FromServices' is not valid on this declaration type. It is only valid on 'parameter' declarations 
    که فقط بر روی پارامتر کار می‌کنه .  
    به این صورت هم در سازنده‌ی کلاس استفاده می‌کنم اما باز هم نمیتونه inject کنه 
            readonly ITicketRepository _ticketRepository;
            public TicketCreatedEventHandler([FromServices]ITicketRepository ticketRepository)
            {
                _ticketRepository = ticketRepository;
            }
    و خطای زیرو می‌ده
    Unresolved dependency [Target Type: Application.EventHandlers.TicketCreatedEventHandler], [Parameter: ticketRepository(Domain.IRepositories.ITicketRepository)], [Requested dependency: ServiceType:Domain.IRepositories.ITicketRepository, ServiceName:]'

    این کلاس توسط NServiceBus فراخوانی میشه و هر زمان که تیکتی ساخته میشه، یک event پابلیش میشه که توسط این کلاس  Handle میشه
    اگر ServiceProvider رو static کنم که بتونم از injection استفاده کنم، مشکلی ایجاد نمی‌کنه؟  
    • #
      ‫۵ سال و ۸ ماه قبل، سه‌شنبه ۲۵ دی ۱۳۹۷، ساعت ۱۲:۵۸
      - از FromServices نباید در پارامترهای سازنده‌ی کلاس‌ها استفاده شود ( فقط در پارامترهای اکشن متدهای کنترلرها کاربرد دارد).
      - اگر IoC Container از پیش تنظیم شده‌ای وجود داشته باشد که کار وهله سازی TicketCreatedEventHandler شما را انجام می‌دهد، وهله سازی و مقدار دهی پارامترهای تزریق شده‌ی در سازنده‌ی کلاس را نیز به صورت خودکار انجام خواهد داد.
      - اگر IoC Container از پیش تنظیم شده‌ای وجود نداشته باشد، کار وهله سازی service provider را باید خودتان دقیقا معادل کاری که در برنامه‌های کنسول انجام می‌شود، انجام دهید و یا اگر می‌توانید به service provider موجودی دسترسی پیدا کنید که چه بهتر.
      - اگر IoC Container از پیش تنظیم شده‌ای وجود نداشته باشد و همچنین وهله سازی کلاس TicketCreatedEventHandler توسط شما قابل کنترل نیست، تنها راه حل ممکن، حذف پارامترهای سازنده‌ی کلاس و استفاده از الگوی Service locator برای دسترسی به سرویس‌های مدنظر است.
  • #
    ‫۵ سال و ۷ ماه قبل، یکشنبه ۳۰ دی ۱۳۹۷، ساعت ۱۵:۰۸
    یک نکته‌ی تکمیلی: امکان تزریق وابستگی‌های سرویس‌های سفارشی، در سازنده‌ی کلاس Startup برنامه‌های وب


    اگر به سازنده‌ی پیش‌فرض کلاس Startup یک برنامه‌ی وب دقت کنید، چنین تزریق وابستگی در قالب ابتدایی آن وجود دارد:
    public class Startup 
    { 
       public Startup(IConfiguration configuration) 
       { 
           Configuration = configuration; 
       }
    در اینجا ممکن است چند سؤال مطرح شوند:
    الف) چه سرویس‌های پیش‌فرض دیگری را نیز می‌توان در اینجا تزریق کرد؟
    ب) آیا می‌توان سرویس‌های سفارشی تهیه شده‌ی توسط خودمان را نیز در اینجا تزریق کرد؟

    الف) بر روی ابتدای متد ConfigureServices کلاس Startup یک break-point را قرار دهید. لیست پارامتر services آن، شامل سرویس‌های پیش‌فرضی است که قابلیت تزریق وابستگی‌ها را در سازنده‌ی این کلاس دارند و بیش از 40 کلاس هستند.

    ب) برای این منظور به فایل Program.cs مراجعه کرده و سرویس سفارشی خود را به صورت زیر، توسط متد ConfigureServices آن، اضافه کنید:
    using CoreIocServices;
    using Microsoft.AspNetCore;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.DependencyInjection;
    
    namespace CoreIocSample02
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                CreateWebHostBuilder(args).Build().Run();
            }
    
            public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
                WebHost.CreateDefaultBuilder(args)
                .ConfigureServices(serviceCollection =>
                {
                    serviceCollection.AddScoped<ISomeService, SomeService>();
                })
                .UseStartup<Startup>();
        }
    }
    اکنون ISomeService سفارشی ما قابلیت تزریق در سازنده‌ی کلاس Startup را نیز پیدا کرده‌است (علاوه بر سایر نقاط برنامه):
    namespace CoreIocSample02
    {
        public class Startup
        {
            private readonly ISomeService _someService;
    
            public Startup(IConfiguration configuration, ISomeService someService)
            {
                Configuration = configuration;
                _someService = someService;
            }
    
            public IConfiguration Configuration { get; }
    • #
      ‫۵ سال قبل، یکشنبه ۲۷ مرداد ۱۳۹۸، ساعت ۰۰:۲۸
      ارتقاء به ASP.NET Core 3.0: محدود شدن امکان تزریق وابستگی‌ها در سازنده‌ی کلاس آغازین برنامه

      یکی از تغییرات مهم ASP.NET Core 3.0 نسبت به نگارش‌های قبلی، جنریک شدن Host آن است (چون حالت‌های هاستینگ بیشتری را نسبت به حالت صرف MVC پشتیبانی می‌کند). به این ترتیب HostBuilder نگارش 2x:
      public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
                       WebHost.CreateDefaultBuilder(args)
                       .UseStartup<Startup>();
      اکنون در نگارش 3x به این صورت در آمده‌است:
      public static IHostBuilder CreateHostBuilder(string[] args) =>
                     Host.CreateDefaultBuilder(args)
                           .ConfigureWebHostDefaults(webBuilder =>
                           {
                              webBuilder.UseStartup<Startup>();
                           });
      این مورد، یک تغییر مهم را هم در وضعیت تزریق وابستگی‌های سفارشی در کلاس آغازین برنامه ایجاد کرده‌است: در نگارش 3x، فقط و فقط سرویس‌های IHostEnvironment ،IWebHostEnvironment و IConfiguration را می‌توانید به سازنده‌ی کلاس آغازین آن تزریق کنید.
      علت اینجا است که در ASP.NET Core 3x، یک باگ بسیار مهم سیستم تزریق وابستگی‌های ASP.NET Core برطرف شده‌است: اکنون فقط یک dependency injection container به ازای کل برنامه‌ی ASP.NET Core 3x ساخته می‌شود. در نگارش‌های قبلی، یک container برای برنامه و یک container مجزا برای host تولید می‌شدند. در این حالت اگر یک سرویس Singleton را در فایل program.cs معرفی می‌کردید:
      WebHost.CreateDefaultBuilder()
                   .UseStartup<Startup>()
                   .ConfigureServices(services => 
                           services.AddSingleton<MySingleton>())
                   .Build()
                   .Run();
      برخلاف تصور، این سرویس Singleton رفتار نمی‌کرد؛ چون همانطور که عنوان شد، دو container، برنامه را مدیریت می‌کردند (یعنی دوبار توسط دو ظرف متفاوت نگهدارنده‌ی اشیاء، وهله سازی می‌شد) که اکنون در نگارش 3x به یک مورد کاهش یافته‌است.
      در اینجا هرچند متد ConfigureServices وجود دارد، اما اگر از آن استفاده کنید، سرویس معرفی شده‌ی توسط آن، در سازنده‌ی کلاس Startup شناسایی نمی‌شود.