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

همانطور که در قسمت قبل نیز بررسی کردیم، ASP.NET Core امکان تزریق وابستگی‌های متداول را در اکثر قسمت‌های آن مانند کنترلرها، میان‌افزارها و غیره، میسر و پیش بینی کرده‌است؛ اما همیشه و در تمام مکان‌های یک برنامه‌ی وب، امکان تزریق وابستگی‌ها در سازنده‌ی کلاس‌ها وجود ندارد و مجبور به استفاده‌ی از الگوی Service Locator می‌باشیم. در این قسمت این مکان‌های ویژه را بررسی خواهیم کرد.


HttpContext و امکان دسترسی به Service Locatorها

در ASP.NET Core هر جائیکه دسترسی به HttpContext وجود داشته باشد، می‌توان از الگوی Service Locator نیز توسط خاصیت HttpContext.RequestServices آن استفاده کرد. این خاصیت از نوع IServiceProvider قرار گرفته‌ی در فضای نام System است که در قسمت دوم آن‌را بررسی کردیم. توسط این اینترفیس به متد object GetService(Type serviceType) دسترسی خواهیم یافت و برای کار با نگارش‌های جنریک آن نیاز است فضای نام Microsoft.Extensions.DependencyInjection را مورد استفاده قرار داد:
using Microsoft.Extensions.DependencyInjection;

namespace CoreIocSample02.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Privacy()
        {
            var myDisposableService = this.HttpContext.RequestServices.GetRequiredService<IMyDisposableService>();
            myDisposableService.Run();
            return View();
        }
    }
}
در اینجا یک نمونه مثال را از کار با HttpContext.RequestServices، در یک اکشن متد ملاحظه می‌کنید.


استفاده از Service Locatorها در فیلترها

هرچند استفاده‌ی از this.HttpContext.RequestServices در یک اکشن متد که کنترلر آن تزریق وابستگی‌های در سازنده‌ی کلاس را به صورت توکار پشتیبانی می‌کند، مزیت خاصی را به همراه ندارد و توصیه نمی‌شود، اما در انتهای قسمت قبل، امکان تزریق وابستگی‌های متداول در فیلترها را نیز بررسی کردیم. زمانیکه کار تزریق وابستگی‌ها در سازنده‌ی یک فیلتر صورت می‌گیرد، دیگر نمی‌توان ApiExceptionFilter را به نحو متداول [ApiExceptionFilter] فراخوانی کرد؛ چون پارامترهای سازنده‌ی آن جزو ثوابت قابل کامپایل نیستند و کامپایلر سی‌شارپ چنین اجازه‌ای را نمی‌دهد. به همین جهت مجبور به استفاده‌ی از [ServiceFilter(typeof(ApiExceptionFilter))] برای معرفی یک چنین فیلترهایی هستیم. اما می‌توان این وضعیت را با استفاده از الگوی Service Locator بهبود بخشید. اینبار بجای تعریف وابستگی‌ها در سازنده‌ی یک فیلتر:
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;  
    }
می‌توان آن‌ها را به صورت زیر نیز دریافت کرد:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

namespace Filters
{
    public class ApiExceptionFilter : ExceptionFilterAttribute
    {
        public override void OnException(ExceptionContext context)
        {
            var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ApiExceptionFilter>>();
            logger.LogError(context.Exception, context.Exception.Message);
            base.OnException(context);
        }
    }
}
در اینجا برای مثال سرویس ILogger توسط context.HttpContext.RequestServices قابل دسترسی شده‌است. به این ترتیب با حذف پارامترهای سازنده‌ی این کلاس فیلتر که به صورت ثوابت زمان کامپایل قابل تعریف نیستند، امکان استفاده‌ی از آن به صورت متداول [ApiExceptionFilter] میسر می‌شود.


استفاده از Service Locatorها در ValidationAttributes

روش تزریق وابستگی‌ها در سازنده‌ی کلاس‌های ValidationAttribute مهیا نیست و امکانی مانند ServiceFilterها در اینجا کار نمی‌کند. به همین جهت تنها روشی که برای دسترسی به سرویس‌ها باقی می‌ماند استفاده از الگوی Service Locator است که مثالی از آن‌را در کدهای زیر از طریق ValidationContext مشاهده می‌کنید:
using Microsoft.Extensions.DependencyInjection;
using System.ComponentModel.DataAnnotations;
using CoreIocServices;

namespace Test
{
    public class CustomValidationAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var service = validationContext.GetRequiredService<IMyDisposableService>();
            // use service
            // ... validation logic
        }
    }
}


استفاده از Service Locatorها در متد Main کلاس Program

فرض کنید سرویسی را در متد ConfigureServices کلاس Startup یک برنامه‌ی وب ثبت کرده‌اید:
namespace Test
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<ITokenGenerator, TokenGenerator>();
        }
برای استفاده‌ی از این سرویس در متد Main کلاس Program می‌توان به صورت زیر عمل کرد:
namespace Test
{
    public class Program
    {
        public static void Main(string[] args)
        {
            IWebHost webHost = CreateWebHostBuilder(args).Build();

            var tokenGenerator = webHost.Services.GetRequiredService<ITokenGenerator>();
            string token =  tokenGenerator.GetToken();
            System.Console.WriteLine(token);

            webHost.Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }
}
متد Build در اینجا، یک وهله‌ی از نوع IWebHost را بازگشت می‌دهد. یکی از خواص این اینترفیس نیز Services از نوع IServiceProvider است:
namespace Microsoft.AspNetCore.Hosting
{
    public interface IWebHost : IDisposable
    {
        IServiceProvider Services { get; }
    }
}
زمانیکه به IServiceProvider دسترسی داشته باشیم، می‌توان از متدهای GetRequiredService و یا GetService آن که در قسمت دوم، تفاوت‌های آن‌ها را بررسی کردیم، استفاده کرد و به وهله‌های سرویس‌های مدنظر دسترسی یافت.


استفاده از Service Locatorها در متد ConfigureServices کلاس Startup

برای دسترسی به سرویس‌های برنامه در متد ConfigureServices می‌توان متد BuildServiceProvider را بر روی پارامتر services فراخوانی کرد. خروجی آن از نوع کلاس ServiceProvider است که امکان دسترسی به متدهایی مانند GetRequiredService را میسر می‌کند:
namespace CoreIocSample02
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            var scopeFactory = services.BuildServiceProvider().GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var provider = scope.ServiceProvider;
                using (var dbContext = provider.GetRequiredService<ApplicationDbContext>())
                {
                    // ...
                }
            }
        }
در بسیاری از موارد، کار با GetRequiredService کافی است و مرحله‌ی بعدی هم ندارد. اما اگر سرویس شما دارای طول عمر از نوع Scoped و همچنین IDispoable نیز بود، همانطور که در نکته‌ی «روش صحیح Dispose اشیایی با طول عمر Scoped، در خارج از طول عمر یک درخواست ASP.NET Core» قسمت سوم عنوان شد، نیاز است یک Scope صریح را برای آن ایجاد و سپس آن‌را به نحو صحیحی Dispose کرد که روش آن‌را در مثال فوق ملاحظه می‌کنید.


استفاده از Service Locatorها در متد Configure کلاس Startup

در قسمت قبل عنوان شد که می‌توان سرویس‌های مدنظر خود را به صورت پارامترهایی جدید به متد Configure اضافه کرد و کار وهله سازی آن‌ها توسط Service Provider برنامه به صورت خودکار صورت می‌گیرد:
public class Startup 
{ 
    public void ConfigureServices(IServiceCollection services) { } 
  
    public void Configure(IApplicationBuilder app, IAmACustomService customService) 
    { 
        // ....    
    }         
}
در اینجا روش دومی نیز وجود دارد. می‌توان از پارامتر app نیز به صورت Service Locator استفاده کرد:
namespace CoreIocSample02
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var provider = scope.ServiceProvider;
                using (var dbContext = provider.GetRequiredService<ApplicationDbContext>())
                {
                    //...
                }
            }
خاصیت app.ApplicationServices از نوع IServiceProvider است و مابقی نکات آن با توضیحات «استفاده از Service Locatorها در متد ConfigureServices کلاس Startup» مطلب جاری دقیقا یکی است.
مطالب مشابه
  • #
    ‫۵ سال و ۸ ماه قبل، جمعه ۱۴ دی ۱۳۹۷، ساعت ۰۶:۳۷
    در مقاله گفته شده "اما اگر سرویس شما دارای طول عمر از نوع Scoped و همچنین IDispoable نیز بود ..." 
    یعنی اگر یک Repository که اینترفیس IDisposable را پیاده سازی نمی‌کند داشته باشیم و آن را به صورت Scoped تعریف کرده باشیم، فرقی نمی‌کند که توسط  scopeFactory . CreateScope درخواست شود یا به صورت معمولی؟ ( دراینجا منظورم خود Repository هست و نه DbContext استفاده شده داخل آن)
    • #
      ‫۵ سال و ۸ ماه قبل، جمعه ۱۴ دی ۱۳۹۷، ساعت ۱۱:۱۴
      در نهایت Repository شما وابسته‌است به سرویس DbContext از نوع IDispoable (برای تشکیل یک سرویس، چندین سرویس به صورت خودکار توسط IoC Container وهله سازی می‌شوند). بنابراین برای Dispose صحیح وابستگی‌های تو در توی آن‌ها حتما نیاز است که یک Scope را ایجاد کنید؛ وگرنه این سرویس‌ها و منابع آن‌ها (مانند اتصال گشوده شده‌ی به بانک اطلاعاتی) تا آخر طول عمر برنامه در حافظه باقی خواهند ماند.
  • #
    ‫۵ سال و ۸ ماه قبل، جمعه ۱۴ دی ۱۳۹۷، ساعت ۰۶:۴۱
    تفاوت ApplicationServices با RequestServices در چیست؟ آیا هر دو به یک رفرنس اشاره می‌کنند یا RequestServices به Scoped Container و ApplicationServices به Root Container اشاره می‌کند؟
    • #
      ‫۵ سال و ۸ ماه قبل، جمعه ۱۴ دی ۱۳۹۷، ساعت ۱۱:۱۱
      در کل برنامه، یک IoC Container‌ بیشتر وجود ندارد. تفاوت ApplicationServices با RequestServices در اصل به وجود یا نبود Scope بر می‌گردد. زمانیکه سرویسی را از ApplicationServices درخواست می‌کنید، مطلقا Scope ای برای آن ایجاد نمی‌شود و اگر مجددا درخواست شود، طول عمر Singleton را مشاهده می‌کنید (مباحث قسمت سوم)، صرف نظر از طول عمر تعریف شده‌ی برای آن سرویس؛ مگر اینکه خودتان به نحوی که توضیح داده شد، یک Scope سفارشی را برای آن ایجاد کنید. اما چون HttpContext.RequestServices داخل یک Scope پیش‌فرض درخواست جاری وب قرار دارد، نیازی به ایجاد صریح Scope را ندارد. البته Scope Validation توضیح داده شده‌ی در مطلب جاری که به ASP.NET Core 2.0 اضافه شده‌است، جلوی اینگونه اشتباهات را با صدور یک استثناء در زمان اجرا می‌گیرد.
  • #
    ‫۵ سال و ۸ ماه قبل، شنبه ۱۵ دی ۱۳۹۷، ساعت ۱۲:۰۷
    آیا در هنگام استفاده از الگوی Service Locator در مکان‌های ویژه فوق امکان استفاده از lazy  به صورت زیر وجود دارد؟ 
    private readonly Lazy<ITestService> _testService;
    
    public TestController(Lazy<ITestService> testService)
    {
        this._testService= testService;
    }

  • #
    ‫۵ سال و ۳ ماه قبل، یکشنبه ۲۹ اردیبهشت ۱۳۹۸، ساعت ۱۹:۳۵
    یک نکته تکمیلی:
    در کلاس‌هایی که اینترفیس <IClassFixture<TestStartup را پیاده سازی می‌کنند، نمی‌توان بیش از یک کلاس TestStartup را به آن‌ها تزریق کرد. در اینجا هم به عنوان مثال اگر بخواهیم سرویس seed را به کلاس تست زیر تزریق نماییم، باید از Service Locatorها  استفاده نماییم:
    public class MemberTest : IClassFixture<TestStartup>
        {
            private readonly TestStartup testStartup;
            private readonly DatabaseSeedService seedService;
            public MemberTest(TestStartup testStartup)
            {
                this.testStartup = testStartup;
                this.seedService = this.testStartup.GetService<DatabaseSeedService>();
            }
    
            [Fact]
            public async Task Add_InsertMember_ReturnOKResponse()
            {
                // ...
            }
        }