در مورد «
امکانات توکار تزریق وابستگیها در ASP.NET Core» پیشتر بحث شد. همچنین «
نحوهی تعریف Context، تزریق سرویسهای EF Core و تنظیمات رشتهی اتصالی آن» را نیز بررسی کردیم. به علاوه مباحث «
به روز رسانی ساختار بانک اطلاعاتی» و «
انتقال مهاجرتها به یک اسمبلی دیگر» نیز مرور شدند. بنابراین در این قسمت برای لایه بندی برنامههای EF Core، صرفا یک مثال را مرور خواهیم کرد که این قسمتها را در کنار هم قرار میدهد و عملا نکتهی اضافهتری را ندارد.
تزریق مستقیم کلاس Context برنامه، تزریق وابستگیها نام ندارد!
در همان قسمت اول سری شروع به کار با EF Core 1.0، مشاهده کردیم که پس از انجام تنظیمات اولیهی آن در کلاس آغازین برنامه:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped);
Context برنامه را در تمام قسمتهای آن میتوان تزریق کرد و کار میکند:
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 });
}
}
این روشی است که در بسیاری از مثالهای گوشه و کنار اینترنت قابل مشاهدهاست. یا کلاس Context را مستقیما در سازندهی کنترلرها تزریق میکنند و از آن استفاده میکنند (روش فوق) و یا لایهی سرویسی را ایجاد کرده و مجددا همین تزریق مستقیم را در آنجا انجام میدهند و سپس اینترفیسهای آن سرویس را در کنترلرهای برنامه تزریق کرده و استفاده میکنند. به این نوع تزریق وابستگیها، تزریق concrete types و یا concrete classes میگویند.
مشکلاتی را که تزریق مستقیم کلاسها و نوعها به همراه دارند به شرح زیر است:
- اگر نام این کلاس تغییر کند، باید این نام، در تمام کلاسهایی که به صورت مستقیم از آن استفاده میکنند نیز تغییر داده شود.
- اگر سازندهای به آن اضافه شد و یا امضای سازندهی موجود آن، تغییر کرد، باید نحوهی وهله سازی این کلاس را در تمام کلاسهای وابسته نیز اصلاح کرد.
- یکی از مهمترین دلایل استفادهی از تزریق وابستگیها، بالابردن قابلیت تست پذیری برنامه است. زمانیکه از اینترفیسها استفاده میشود، میتوان در مورد نحوهی تقلید (mocking) رفتار کلاسی خاص، مستقلا تصمیم گیری کرد. اما هنگامیکه یک کلاس را به همان شکل اولیهی آن تزریق میکنیم، به این معنا است که همواره دقیقا همین پیاده سازی خاص مدنظر ما است و این مساله، نوشتن آزمونهای واحد را با مشکل کردن mocking آنها، گاهی از اوقات غیرممکن میکند. هرچند تعدادی از فریم ورکهای پیشرفتهی mocking گاهی از اوقات امکان تقلید رفتار کلاسها و نوعها را نیز فراهم میکنند، اما با این شرط که تمام خواص و متدهای آنها را virtual تعریف کنید؛ تا بتوانند متدهای اصلی را با نمونههای مدنظر شما بازنویسی (override) کنند.
به همین جهت در ادامه، به همان طراحی
EF Code First #12 با نوشتن اینترفیس IUnitOfWork خواهیم رسید. یعنی کلاس Context برنامه را با این اینترفیس نشانه گذاری میکنیم (در انتهای لیست تمام اینترفیسهای دیگری که ممکن است در اینجا ذکر شده باشند):
public class ApplicationDbContext : IUnitOfWork
و سپس اینترفیس IUnitOfWork را به لایه سرویس برنامه و یا هر لایهی دیگری که به Context آن نیاز دارد، تزریق خواهیم کرد.
طراحی اینترفیس 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
علت اینکه در پروژههای بزرگی مانند EF، تمایل زیادی به استفادهی از کلاسهای abstract وجود دارد (بجای اینترفیسها) این است که اگر این نوع پرکاربرد را به صورت اینترفیس تعریف کنند، با تغییر متدی در آن، باید تمام کدهای خود را به اجبار بازنویسی کنید. اما در حالت استفادهی از کلاسهای abstract، میتوان پیاده سازی پیش فرضی را برای متدهایی که قرار است در آینده اضافه شوند، ارائه داد (یکی از تفاوتهای مهم آنها با اینترفیسها)، بدون اینکه تمام استفاده کنندگان از این کتابخانه، با ارتقاء نگارش EF خود، دیگر نتوانند برنامهی خود را کامپایل کنند.
- این اینترفیس به عمد به صورت 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();
}
}
در ابتدا اینترفیس IUnitOfWork به کلاس Context برنامه اعمال شدهاست:
public class ApplicationDbContext : DbContext, IUnitOfWork
و سپس متدهای آن منهای پیاده سازی اینترفیس IDisposable اعمالی به IUnitOfWork :
public interface IUnitOfWork : IDisposable
پیاده سازی شدهاند. علت اینجا است که چون کلاس پایه DbContext از همین اینترفیس مشتق میشود، دیگر نیاز به پیاده سازی اینترفیس 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>();
در اینجا هم ApplicationDbContext و هم IUnitOfWork با طول عمر Scoped به تنظیمات IoC Container مربوط به ASP.NET Core اضافه شدهاند. به این ترتیب هر زمانیکه وهلهای از نوع IUnitOfWork درخواست شود، تنها یک وهله از ApplicationDbContext در طول درخواست وب جاری، در اختیار مصرف کننده قرار میگیرد و همچنین مدیریت Dispose این وهلهها نیز خودکار است. به همین جهت اینترفیس IUnitOfWork را با IDisposable علامتگذاری کردیم.
استفاده از 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"
}
}
}
در اینجا ارجاعاتی را به اسمبلیهای موجودیتها و DataLayer برنامه مشاهده میکنید. در مورد این اسمبلیها در مطلب «
شروع به کار با EF Core 1.0 - قسمت 3 - انتقال مهاجرتها به یک اسمبلی دیگر» پیشتر بحث شد.
پس از تنظیم وابستگیهای این اسمبلی، اکنون یک کلاس نمونه از لایه سرویس برنامه، به شکل زیر خواهد بود:
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();
}
}
}
در اینجا اکنون میتوان IUnitOfWork را به سازندهی کلاس سرویس Blog تنظیم کرد و سپس به نحو متداولی از امکانات EF Core استفاده نمود.
استفاده از امکانات لایه سرویس برنامه، در دیگر لایههای آن
خروجی لایه سرویس، توسط اینترفیسهایی مانند 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;
}
در اینجا کنترلر برنامه تنها با اینترفیسهای IUnitOfWork و IBlogService کار میکند و دیگر ارجاع مستقیمی را به کلاس ApplicationDbContext ندارد.