تزریق مستقیم کلاس Context برنامه، تزریق وابستگیها نام ندارد!
در همان قسمت اول سری شروع به کار با EF Core 1.0، مشاهده کردیم که پس از انجام تنظیمات اولیهی آن در کلاس آغازین برنامه:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped);
public class TestDBController : Controller { private readonly ApplicationDbContext _ctx; public TestDBController(ApplicationDbContext ctx) { _ctx = ctx; } public IActionResult Index() { var name = _ctx.Persons.First().FirstName; return Json(new { firstName = name }); } }
مشکلاتی را که تزریق مستقیم کلاسها و نوعها به همراه دارند به شرح زیر است:
- اگر نام این کلاس تغییر کند، باید این نام، در تمام کلاسهایی که به صورت مستقیم از آن استفاده میکنند نیز تغییر داده شود.
- اگر سازندهای به آن اضافه شد و یا امضای سازندهی موجود آن، تغییر کرد، باید نحوهی وهله سازی این کلاس را در تمام کلاسهای وابسته نیز اصلاح کرد.
- یکی از مهمترین دلایل استفادهی از تزریق وابستگیها، بالابردن قابلیت تست پذیری برنامه است. زمانیکه از اینترفیسها استفاده میشود، میتوان در مورد نحوهی تقلید (mocking) رفتار کلاسی خاص، مستقلا تصمیم گیری کرد. اما هنگامیکه یک کلاس را به همان شکل اولیهی آن تزریق میکنیم، به این معنا است که همواره دقیقا همین پیاده سازی خاص مدنظر ما است و این مساله، نوشتن آزمونهای واحد را با مشکل کردن mocking آنها، گاهی از اوقات غیرممکن میکند. هرچند تعدادی از فریم ورکهای پیشرفتهی mocking گاهی از اوقات امکان تقلید رفتار کلاسها و نوعها را نیز فراهم میکنند، اما با این شرط که تمام خواص و متدهای آنها را virtual تعریف کنید؛ تا بتوانند متدهای اصلی را با نمونههای مدنظر شما بازنویسی (override) کنند.
به همین جهت در ادامه، به همان طراحی EF Code First #12 با نوشتن اینترفیس IUnitOfWork خواهیم رسید. یعنی کلاس Context برنامه را با این اینترفیس نشانه گذاری میکنیم (در انتهای لیست تمام اینترفیسهای دیگری که ممکن است در اینجا ذکر شده باشند):
public class ApplicationDbContext : IUnitOfWork
طراحی اینترفیس IUnitOfWork
برای اینکه دیگر با کلاس ApplicationDbContext مستقیما کار نکرده و وابستگی به آنرا در تمام قسمتهای برنامه پخش نکنیم، اینترفیسی را ایجاد میکنیم که تنها قسمتهای مشخصی از DbContext را عمومی کند:
public interface IUnitOfWork : IDisposable { DbSet<TEntity> Set<TEntity>() where TEntity : class; void AddRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class; void RemoveRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class; EntityEntry<TEntity> Entry<TEntity>(TEntity entity) where TEntity : class; void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class; void ExecuteSqlCommand(string query); void ExecuteSqlCommand(string query, params object[] parameters); int SaveAllChanges(); Task<int> SaveAllChangesAsync(); }
- در این طراحی شاید عنوان کنید که DbSet، اینترفیس نیست. تعریف DbSet در EF Core به صورت زیر است و در حقیقت همانند اینترفیسها یک abstraction به حساب میآید:
public abstract class DbSet<TEntity> : IQueryable<TEntity>, IEnumerable<TEntity>, IEnumerable, IQueryable, IAsyncEnumerableAccessor<TEntity>, IInfrastructure<IServiceProvider> where TEntity : class
- این اینترفیس به عمد به صورت IDisposable تعریف شدهاست. این مساله به IoC Containers کمک خواهد کرد که بتوانند پاکسازی خودکار نوعهای IDisposable را در انتهای هر درخواست انجام دهند و برنامه مشکلی نشتی حافظه را پیدا نکند.
- اصل کار این اینترفیس، تعریف DbSet و متدهای SaveChanges است. سایر متدهایی را که مشاهده میکنید، صرفا جهت بیان اینکه چگونه میتوان قابلیتی از DbContext را بدون عمومی کردن خود کلاس DbContext، در کلاسهایی که از اینترفیس IUnitOfWork استفاده میکنند، میسر کرد.
پس از اینکه این اینترفیس تعریف شد، اعمال آن به کلاس Context برنامه به صورت ذیل خواهد بود:
public class ApplicationDbContext : DbContext, IUnitOfWork { private readonly IConfigurationRoot _configuration; public ApplicationDbContext(IConfigurationRoot configuration) { _configuration = configuration; } //public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) //{ //} public virtual DbSet<Blog> Blog { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer( _configuration["ConnectionStrings:ApplicationDbContextConnection"] , serverDbContextOptionsBuilder => { var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds; serverDbContextOptionsBuilder.CommandTimeout(minutes); } ); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } public void AddRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class { base.Set<TEntity>().AddRange(entities); } public void RemoveRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class { base.Set<TEntity>().RemoveRange(entities); } public void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class { base.Entry(entity).State = EntityState.Modified; // Or use ---> this.Update(entity); } public void ExecuteSqlCommand(string query) { base.Database.ExecuteSqlCommand(query); } public void ExecuteSqlCommand(string query, params object[] parameters) { base.Database.ExecuteSqlCommand(query, parameters); } public int SaveAllChanges() { return base.SaveChanges(); } public Task<int> SaveAllChangesAsync() { return base.SaveChangesAsync(); } }
public class ApplicationDbContext : DbContext, IUnitOfWork
public interface IUnitOfWork : IDisposable
در مورد تزریق IConfigurationRoot به سازندهی کلاس Context برنامه، در مطلب اول این سری در قسمت «یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه» پیشتر بحث شدهاست.
ثبت تنظیمات تزریق وابستگیهای IUnitOfWork
پس از تعریف و پیاده سازی اینترفیس IUnitOfWork، اکنون نوبت به معرفی آن به سیستم تزریق وابستگیهای ASP.NET Core است:
public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; }); services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped); services.AddScoped<IUnitOfWork, ApplicationDbContext>();
استفاده از IUnitOfWork در لایه سرویسهای برنامه
اکنون لایه سرویس برنامه و فایل project.json آن چنین شکلی را پیدا میکند:
{ "version": "1.0.0-*", "dependencies": { "Core1RtmEmptyTest.DataLayer": "1.0.0-*", "Core1RtmEmptyTest.Entities": "1.0.0-*", "Core1RtmEmptyTest.ViewModels": "1.0.0-*", "Microsoft.Extensions.Configuration.Abstractions": "1.0.0", "Microsoft.Extensions.Options": "1.0.0", "NETStandard.Library": "1.6.0" }, "frameworks": { "netstandard1.6": { "imports": "dnxcore50" } } }
پس از تنظیم وابستگیهای این اسمبلی، اکنون یک کلاس نمونه از لایه سرویس برنامه، به شکل زیر خواهد بود:
namespace Core1RtmEmptyTest.Services { public interface IBlogService { IReadOnlyList<Blog> GetPagedBlogsAsNoTracking(int pageNumber, int recordsPerPage); } public class BlogService : IBlogService { private readonly IUnitOfWork _uow; private readonly DbSet<Blog> _blogs; public BlogService(IUnitOfWork uow) { _uow = uow; _blogs = _uow.Set<Blog>(); } public IReadOnlyList<Blog> GetPagedBlogsAsNoTracking(int pageNumber, int recordsPerPage) { var skipRecords = pageNumber * recordsPerPage; return _blogs .AsNoTracking() .Skip(skipRecords) .Take(recordsPerPage) .ToList(); } } }
استفاده از امکانات لایه سرویس برنامه، در دیگر لایههای آن
خروجی لایه سرویس، توسط اینترفیسهایی مانند IBlogService در قسمتهای دیگر برنامه قابل استفاده و دسترسی میشود.
به همین جهت نیاز است مشخص کنیم، این اینترفیس را کدام کلاس ویژه قرار است پیاده سازی کند. برای این منظور همانند قبل در متد ConfigureServices کلاس آغازین برنامه این تنظیم را اضافه خواهیم کرد:
public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; }); services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped); services.AddScoped<IUnitOfWork, ApplicationDbContext>(); services.AddScoped<IBlogService, BlogService>();
public class TestDBController : Controller { private readonly IBlogService _blogService; private readonly IUnitOfWork _uow; public TestDBController(IBlogService blogService, IUnitOfWork uow) { _blogService = blogService; _uow = uow; }