خودکارسازی، در قسمتهای مختلف یک پروژه میتواند انجام شود. نمونههای مختلف این خودکارسازیها که اکثرا توسط رفلکشن انجام میشوند شامل نگاشت خودکار Dto به Entity و بالعکس (توسط AutoMapper)، ثبت خودکار تمام Entityها در DbContext بدون نیاز به ثبت تک تک آنها به صورت public DbSet<Person> People { get; set; } (که در این روش خودکار، اسم جداول میتواند به صورت جمع ثبت شود)، ثبت خودکار EntityTypeConfigurationها، ثبت خودکار کلیهی کلاسهای Profile برای کانفیگ AutoMapper و رجیستر خودکار DI سرویسها، تا نیازی به نوشتن کدهای تکراری و مشابه ;()<services.AddTransient<IUserService, UserService نداشته باشیم.
برای مشاهدهی عملی پیادهسازی این نمونهها میتوانید به پروژهی ASP.NET Core WebAPI مراجعه کنید. در این مقاله میخواهیم همین سناریو را برای ثبت سرویسهایمان در متد ConfigureServices انجام دهیم، تا نیازی به نوشتن هیچ کدی برای آنها نداشته باشیم.
و سپس در متد ConfigureServices میتوان آن را به صورت زیر استفاده کرد:
ولی اگه پروژهی ما متوسط به بالا باشد، کمکم تعداد سرویسهای ما زیاد میشود (برای مثال چند نمونه از سرویسهای رایج مورد استفاده، شامل سرویسهای لاگ خطاها مثل Elmah و سرویس HttpClientFactory و AutoRegisterDi (توضیح در ادامه مقاله) و AutoMapper و Cache و EFSecondLevelCache و Hangfire و ....) میبینیم که تعداد این سرویسها هم زیاد است و حتی به صورت اکستنشن هم به مرور زمان باعث شلوغ شدن استارتاپ میشوند. ضمن اینکه یک کار تکراری است که باید هر بار انجام شود.
کار تمام شد. حالا تمام سرویسهای ما با ایجاد کلاس مرتبط و implement شدن از اینترفیس IServiceInstaller، به طور خودکار در استارتاپ و متد ConfigureServies ثبت خواهند شد.
ثبت سرویسهای مختلف، به همراه تنظیمات آنها (مانند Authentication، Swagger، DbContext، ApiVersioning و ...) در استارتاپ میتواند به چندین صورت انجام شود.
روش اول اینکه به صورت دستی تمام کدهای مربوط به رجیستر کردن سرویسها و تنظیمات آنها، در متد ConfigureServices نوشته شود که خیلی جالب نیست و موجب شلوغ شدن سریع این متد میشود. نمونهی این شیوه را برای ثبت سرویس مربوط به DbContext میبینیم:
راه دوم روش استفاده از متدهای الحاقی است؛ طوریکه برای هر سرویس، یک متد الحاقی را تعریف کنیم و از آن، در این متد استفاده کنیم که حجم کدها را تا حد زیادی کم میکند. برای مثال ثبت سرویس بالا را میتوانیم در کلاس دیگری با نام DbContextServiceCollectionExtensions.cs ثبت کنیم:
public void ConfigureServices(IServiceCollection services) { // DbContext Service services.AddDbContext<AppDbContext>(options => { options .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder => { sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds sqlServerOptionsBuilder.EnableRetryOnFailure(); sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName); }) //Tips .ConfigureWarnings(warning => warning.Throw(RelationalEventId .QueryPossibleExceptionWithAggregateOperatorWarning)); // Activate EF Second Level Cache options.AddInterceptors(new SecondLevelCacheInterceptor()); }); // register other services .... }
راه دوم روش استفاده از متدهای الحاقی است؛ طوریکه برای هر سرویس، یک متد الحاقی را تعریف کنیم و از آن، در این متد استفاده کنیم که حجم کدها را تا حد زیادی کم میکند. برای مثال ثبت سرویس بالا را میتوانیم در کلاس دیگری با نام DbContextServiceCollectionExtensions.cs ثبت کنیم:
public static class DbContextServiceCollectionExtensions { public static void AddDbContext(this IServiceCollection services) { services.AddDbContext<AppDbContext>(options => { options .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder => { sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds sqlServerOptionsBuilder.EnableRetryOnFailure(); sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName); }) //Tips .ConfigureWarnings(warning => warning.Throw(RelationalEventId .QueryPossibleExceptionWithAggregateOperatorWarning)); // Activate EF Second Level Cache options.AddInterceptors(new SecondLevelCacheInterceptor()); }); } }
public void ConfigureServices(IServiceCollection services) { // Add DbContext services.AddDbContext(); //.... Register other services }
راه سوم ثبت سرویس، استفاده از یک اینترفیس به نام IServiceInstaller و استفاده از آن در کلاسهای مختلف مربوط به ثبت سرویس و بعد خواندن خودکار این تنظیمات با یک خط کد سادهی رفلکشن است که در ادامه میبینیم:
اینترفیس IServiceInstaller را تعریف میکنیم:
توضیح: پارامتر appSettings کلاسی شامل کلیهی مقادیر فایل appsettings.json است. شما میتوانید بجای آن از IConfiguration استفاده کنید و مقدار آن را در Startup پاس دهید. پارامتر آخر برای حالتی است که این فایلها را در لایهی دیگری به غیر از لایهی اصلی Api (مثل لایهی WebFamewrk) پیاده سازی میکنید.
public interface IServiceInstaller { void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly); }
سپس کلاسهای ثبت سرویسهایمان را با ارث بری از این اینترفیس میسازیم. برای نمونه رجیستر DbContext را با ایجاد کلاسی به نام DbContextInstaller انجام میدهیم:
public class DbContextInstaller : IServiceInstaller { public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly) { services.AddDbContext<AppDbContext>(options => { options .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder => { sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds sqlServerOptionsBuilder.EnableRetryOnFailure(); sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName); }) //Tips .ConfigureWarnings(warning => warning.Throw(RelationalEventId .QueryPossibleExceptionWithAggregateOperatorWarning)); // Activate EF Second Level Cache options.AddInterceptors(new SecondLevelCacheInterceptor()); }); } }
حالا برای ثبت این کلاس و کلاسهای مشابه Installer، میآییم یک متد الحاقی را برای متد ConfigureServices مینویسیم که در آن از رفلکشن استفاده میکنیم:
public static class ServiceInstallerExtensions { public static void InstallServicesInAssemblies(this IServiceCollection services, AppSettings appSettings) { var startupProjectAssembly = Assembly.GetCallingAssembly(); var assemblies = new[] { startupProjectAssembly, Assembly.GetExecutingAssembly() }; var installers = assemblies.SelectMany(a => a.GetExportedTypes()) .Where(c => c.IsClass && !c.IsAbstract && c.IsPublic && typeof(IServiceInstaller).IsAssignableFrom(c)) .Select(Activator.CreateInstance).Cast<IServiceInstaller>().ToList(); installers.ForEach(i => i.InstallServices(services, appSettings, startupProjectAssembly)); } }
در نهایت متد ConfigureServices ما به صورت زیر خواهد بود (بعد از اضافه کردن تمام سرویسها!):
public void ConfigureServices(IServiceCollection services) { //* HttpContextAccessor // services.AddHttpContextAccessor(); //* Controllers services.AddControllers(options => { options.Filters.Add(new AuthorizeFilter()); }) .AddNewtonsoftJson(); //* Installers services.InstallServicesInAssemblies(_appSettings); }
فقط یک نکته آخر اینکه برای رجیستر خودکار DI سرویسها (و ننوشتن کدهایی مانند ()<services.AddTransient<IUserService, UserService برای رجیستر هر سرویس) میتوانیم از Autofac استفاده کنیم (در پروژهی بالا آمده است) و یا از پکیج AutoRegisterDi استفاده کنیم (متعلق به Jon P Smith) که از خود Container داخلی Core استفاده میکند و از Autofac سبکتر است. کلاسی میسازیم به نام RegisterServicesUsingAutoRegisterDiInstaller:
public class RegisterServicesUsingAutoRegisterDiInstaller : IServiceInstaller { public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly) { var dataAssembly = typeof(SomeRepository).Assembly; var serviceAssembly = typeof(SomeService).Assembly; var webFrameworkAssembly = Assembly.GetExecutingAssembly(); var startupAssembly = startupProjectAssembly; var assembliesToScan = new[] { dataAssembly, serviceAssembly, webFrameworkAssembly, startupAssembly }; #region Generic Type Dependencies services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); #endregion #region Scoped Dependency Interface services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan) .Where(c => c.GetInterfaces().Contains(typeof(IScopedDependency))) .AsPublicImplementedInterfaces(ServiceLifetime.Scoped); #endregion #region Singleton Dependency Interface services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan) .Where(c => c.GetInterfaces().Contains(typeof(ISingletonDependency))) .AsPublicImplementedInterfaces(ServiceLifetime.Singleton); #endregion #region Transient Dependency Interface services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan) .Where(c => c.GetInterfaces().Contains(typeof(ITransientDependency))) .AsPublicImplementedInterfaces(); // Default is Transient #endregion #region Register DIs By Name services.RegisterAssemblyPublicNonGenericClasses(dataAssembly) .Where(c => c.Name.EndsWith("Repository") && !c.GetInterfaces().Contains(typeof(ITransientDependency)) && !c.GetInterfaces().Contains(typeof(IScopedDependency)) && !c.GetInterfaces().Contains(typeof(ISingletonDependency))) .AsPublicImplementedInterfaces(ServiceLifetime.Scoped); services.RegisterAssemblyPublicNonGenericClasses(serviceAssembly) .Where(c => c.Name.EndsWith("Service") && !c.GetInterfaces().Contains(typeof(ITransientDependency)) && !c.GetInterfaces().Contains(typeof(IScopedDependency)) && !c.GetInterfaces().Contains(typeof(ISingletonDependency))) .AsPublicImplementedInterfaces(); #endregion } }
(رجیستر در اینجا با اولویت اینترفیسهای ITransiantDependency، IScopedDependency، ISingletonDependency و سپس اتمام نام سرویس با کلمههای Repository و Service انجام میشود که شما میتوانید با منطق و نیاز خودتان آنها را تغییر دهید)