در برنامه هایی که business logic به داده ها مستقیما دسترسی دارد می توانید با هر یک از مشکلات زیر روبرو شوید.
الگوهای طراحی(Design patterns) برای حل مشکلات تکراری در برنامه های شما استفاده می شود و الگوی Repository یکی از پرکاربردترین الگوهای طراحی است.
الگوی Repository یک الگوی طراحی است که داده ها را "از" و "به" لایه های Domain و Data Access (مانند Entity Framework Core / Dapper) واسطه می کند. Repository کلاس هایی هستند که منطق مورد نیاز برای ذخیره یا بازیابی داده ها را پنهان می کنند. بنابراین، برنامه ما به اینکه از چه نوع ORMي استفاده می کنیم اهمیتی نمی دهد، زیرا همه چیز مربوط به ORM در یک لایه Repository مدیریت می شود. این به شما این امکان را می دهد که تفکیک بهتری از نگرانی ها(SoC) داشته باشید.الگوی Repository یکی از الگوهای طراحی به شدت مورد استفاده برای ساخت solution های تمیزتر(cleaner) است.
یکی از الگوهای ساختاری(structural patterns) اصلی که در DDD با آن مواجه می شود (و یکی از بحث برانگیزترین آنها) الگوی Repository است. شما persistent domain model را ایجاد کردهاید، اکنون باید بتوانید این اشیاء (objects) را از یک محل کپسولهشده(encapsulated store) بازیابی کنید.
مخازن(Repository) کلاس ها یا اجزایی هستند که منطق مورد نیاز برای دسترسی به منابع داده را در بر می گیرند. آنها عملکرد مشترک دسترسی به داده را متمرکز می کنند و قابلیت نگهداری بهتر را فراهم می کنند و زیرساخت یا فناوری مورد استفاده برای دسترسی به پایگاه های داده از لایه Domain Model را جدا می کنند.
مخازن به راحتی با الگوهای Factory اشتباه گرفته می شوند، در حالی که تفاوت اصلی این است که Factory Pattern لایه persistent را ارائه نمی دهد.
در عمل و در پیادهسازیهای DDD، استفاده از یک Repository فراتر از بازیابی است، و شامل سایر عمليات CRUD مي باشد.
معماری باید مستقل از Frameworks باشد.(Robert Cecil Martin)
مایکروسافت استفاده از الگوهای Repository را در سناریوهای پیچیده توصیه میکند تا کوپلینگ(coupling) را کاهش داده و تستپذیری بهتر را به ارمغان آورد. در مواردی که ساده ترین کد ممکن را می خواهید، می توانید از الگوی Repository اجتناب کنید.
هر الگویی که بر روی برنامه اعمال می شود دارای مزایا و معایب است که به بسیاری از جنبه های برنامه در حال توسعه بستگی دارد. قبل از تصمیم گیری در مورد استفاده یا عدم استفاده از آن الگو، همه جنبه ها مانند اندازه برنامه ها، ماهیت منطق دسترسی به داده ها، زبان برنامه نویسی مورد استفاده، مهارت های توسعه دهنده، زمان ورود به بازار و غیره باید در نظر گرفته شوند. در مورد الگوی Repository نیز همینطور است. اگر روی یک برنامه بسیار کوچک کار می کنید که کد یا عملکرد زیادی ندارد و همچنین اگر آن برنامه پس از استقرار دچار تغییرات زیادی نمی شود، در آن صورت برای ساده نگه داشتن کد می توانید از Repository خودداری کنید. همچنین اگر نمی خواهید یک Repository را پیاده سازی کنید، می توانید مستقیماً از کلاس DbContext در کنترلر یا سرویس استفاده کنید.
افزودن Repository مزایای خاص خود را دارد. اما من به شدت توصیه می کنم که از الگوهای طراحی در همه جا استفاده نکنید. سعی کنید تنها زمانی از آن استفاده کنید که سناریو به استفاده از این الگوی طراحی نیاز دارد. همانطور که گفته شد، الگوی Repository چیزی است که می تواند در دراز مدت برای شما مفید باشد.
ما پروژه ای را از ابتدا می سازیم که در آن یک معماری تمیز(clean architecture) برای دسترسی به داده ها را پیاده سازی می کنیم
بیایید الگوی Repository را در یک پروژه ASP.NET Core WebApi پیاده سازی کنیم.
بیایید با ایجاد یک Solution جدید شروع کنیم. در اینجا من Solution خود را RepositoryPattern.WebApi و اولین پروژه را WebApi نامگذاری می کنم.
در ادامه، بیایید 2 پروژه Class Library دیگر را در Solution اضافه کنیم. ما آن را DataAccess.EFCore و Domain می نامیم. در اینجا ویژگی ها و اهداف هر پروژه آورده شده است.
خوب الان Solution ما باید مشابه تصویر زیر باشد.
اکنون، اجازه دهید موجودیت های مورد نیاز را به پروژه Domain اضافه کنیم. یک پوشه جدید در پروژه دامنه با نام Entities ایجاد کنید.2 کلاس ساده : Developer و Project را در پوشه Entities ایجاد کنید.
public class Developer { public int Id { get; set; } public string Name { get; set; } public int Followers { get; set; } }
public class Project { public int Id { get; set; } public string Name { get; set; } }
در مرحله بعد، Entity Framework Core را راه اندازی و پیکربندی می کنیم. این بسته های مورد نیاز را در پروژه DataAccess.EFCore نصب کنید. اینجا جایی است که ما کلاس DbContect و پیاده سازی واقعی Repository را داریم.
یک reference به پروژه Domain (جایی که موجودیت های خود را تعریف کرده ایم) اضافه کنید و یک کلاس جدید در پروژه DataAccess.EFCore ایجاد کنید و نام آن را ApplicationDbContext.cs بگذارید.
public class ApplicationContext : DbContext { public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options) { } public DbSet<Developer> Developers { get; set; } public DbSet<Project> Projects { get; set; } }
خوب حالا اجازه دهید به پروژه WebApi برویم تا EFCore را در ASP.NET Core Application ثبت کنیم. ما همچنین پایگاه داده را در این مرحله به روز می کنیم تا جداول ما ایجاد شوند.
ابتدا این بسته را روی پروژه WebApi نصب کنید. این به شما امکان می دهد دستورات EF Core را روی CLI اجرا کنید.
سپس به Program.cs بروید :
builder.Services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)); });
توجه داشته باشید که باید یک reference از پروژه WebApiبه DataAccess.EFCore اضافه کنید.
پس از آن، فایل appsettings.json را در پروژه WebApi باز کنید و connection string را اضافه کنید.
"ConnectionStrings": { "DefaultConnection":"Server=.;Database=sampleRepository;Trusted_Connection=True;MultipleActiveResultSets=true" }
در نهایت، بیایید پایگاه داده را به روز کنیم. کنسول Package Manager خود را در ویژوال استودیو باز کنید و دستورات زیر را اجرا کنید.
مطمئن شوید که پروژه Startup خود را به عنوان WebApi و پروژه پیش فرض را به عنوان DataAccess.EFCore تنظیم کرده اید.
بیایید Repository را برای یک لحظه از دهن خود دور نگه داریم.
اکنون که لایه EFCore خود را پیکربندی کردیم، اجازه دهید کمی در مورد روش سنتی دریافت داده از این لایه صحبت کنیم. به طور سنتی، شما مستقیماً شی dbContext را برای خواندن و نوشتن داده ها فراخوانی می کنید. این خوبه. اما آیا واقعا برای بلند مدت ایده آل است؟ وقتی مستقیماً از dbContext استفاده می کنید، کاری که انجام می دهید این است که Entity Framework Core به شدت با برنامه شما همراه(در هم تنیده) شده است. بنابراین، در آینده وقتی چیزی جدیدتر و بهتر از EFCore عرضه/نیاز شد، پیاده سازی فناوری جدید و ایجاد تغییرات مربوطه برای شما بسیار آزاردهنده است. درسته؟
یکی دیگر از معایب استفاده مستقیم از dbContext این است که DbContext را آشکار می کنید، که کاملاً ناامن است.
این دلیل استفاده از الگوی Repository در برنامه های ASP.NET است.
در حین انجام عملیات CRUD با Entity Framework Core، ممکن است متوجه شده باشید که ماهیت اصلی کد یکسان است. با این حال ما آن را چندین بار می نویسیم. عملیات CRUD شامل ایجاد، خواندن، بهروزرسانی و حذف است. بنابراین، چرا یک راهانداز کلاس/اینترفیس (class/interface setup) نداشته باشید که بتوانید هر یک از این عملیات را تعمیم دهید.
ابتدا اجازه دهید یک پوشه جدید با نام Interfaces در پروژه Domain خود اضافه کنیم. چرا؟ زیرا، ما وابستگی ها را معکوس خواهیم کرد، به طوری که می توانید رابط/Interface را در پروژه Domain تعریف کنید، اما پیاده سازی می تواند خارج از پروژه Domain باشد. در این مورد، پیاده سازی ها به پروژه DataAccess.EFCore می روند. بنابراین، لایه Domain شما به هیچ چیز بستگی ندارد، بلکه سایر لایه ها به رابط/Interface لایه Domain وابسته هستند. این یک توضیح ساده از Dependency Inversion Principle است. خیلی باحاله، آره؟
یک اینترفیس جدید، Interfaces/IGenericRepository.cs اضافه کنید
public interface IGenericRepository<T> where T : class { T GetById(int id); IEnumerable<T> GetAll(); IEnumerable<T> Find(Expression<Func<T, bool>> expression); void Add(T entity); void AddRange(IEnumerable<T> entities); void Remove(T entity); void RemoveRange(IEnumerable<T> entities); }
این یک Generic Repository خواهد بود که می تواند برای کلاس های توسعه دهنده و پروژه استفاده شود. در اینجا T کلاس خاص است.
مجموعه متدها به ترجیح شما بستگی دارد. در حالت ایدهآل، ما به ۷ تابع یا متد نیاز داریم که بیشتر بخش مدیریت داده را پوشش دهد.
حالا بیایید این اینترفیس ها را پیاده سازی کنیم. در پروژه DataAccess.EFCore یک پوشه جدید با نام Repositories ایجاد کنید و داخل آن یک کلاس جدید ایجاد و نام آن را GenericRepository بگذارید.
public class GenericRepository<T> : IGenericRepository<T> where T : class { protected readonly ApplicationDbContext _context; public GenericRepository(ApplicationDbContext context) { _context = context; } public void Add(T entity) { _context.Set<T>().Add(entity); } public void AddRange(IEnumerable<T> entities) { _context.Set<T>().AddRange(entities); } public IEnumerable<T> Find(Expression<Func<T, bool>> expression) { return _context.Set<T>().Where(expression); } public IEnumerable<T> GetAll() { return _context.Set<T>().ToList(); } public T GetById(int id) { return _context.Set<T>().Find(id); } public void Remove(T entity) { _context.Set<T>().Remove(entity); } public void RemoveRange(IEnumerable<T> entities) { _context.Set<T>().RemoveRange(entities); } }
این کلاس اینترفیس IGenericRepository را پیاده سازی خواهد کرد. همچنین ApplicationDbContext را در اینجا تزریق خواهیم کرد. به این ترتیب ما تمام اقدامات مربوط به شی dbContext را در کلاس های Repository پنهان می کنیم. همچنین توجه داشته باشید که برای توابع ADD و Remove، ما فقط عملیات را روی شی dbContext انجام می دهیم. اما ما هنوز تغییرات را در پایگاه داده اعمال نمی کنیم/به روز نمی کنیم/ذخیره نمی کنیم. این کاری نیست که در یک کلاس Repository انجام شود. برای مواردی که دادهها را به پایگاه داده میدهید، به الگوی Unit of Work نیاز داریم. در بخش بعدی در مورد Unit of Work بحث خواهیم کرد.
فهمیدید چرا به جای IDevloperRepository از یک Generic Repository استفاده کردیم؟ هنگامی که تعداد زیادی از موجودیت ها در برنامه ما وجود دارد، ما به مخازن جداگانه برای هر موجودیت نیاز داریم. اما ما نمی خواهیم همه 7 تابع بالا را در هر کلاس Repository پیاده سازی کنیم، درست است؟ بنابراین ما یک مخزن عمومی ساختیم که متداول ترین پیاده سازی ها را در خود جای می دهد.
حال اگر به سوابق محبوب ترین توسعه دهندگان(Developers) از پایگاه داده خود نیاز داشته باشیم چه اتفاقی می افتد؟ ما عملکردی برای آن در کلاس عمومی خود نداریم، داریم؟ اینجاست که می توانیم مزیت ساخت یک Generic Repository را ببینیم.
در پروژه Domain، در پوشه Interfaces، یک اینترفیس جدید به نام IDveloperRepository اضافه کنید.
public interface IDeveloperRepository : IGenericRepository<Developer> { IEnumerable<Developer> GetPopularDevelopers(int count); }
در اینجا ما تمام عملکردهای مخزن عمومی/Generic Repository را به ارث می بریم و همچنین یک متدجدید "GetPopularDevelopers" را اضافه می کنیم.
بیایید IDveloperRepostory را پیاده سازی کنیم. به پروژه DataAccess بروید و در پوشه Repositories یک کلاس جدید به نام DeveloperRepository اضافه کنید.
public class DeveloperRepository : GenericRepository<Developer>, IDeveloperRepository { public DeveloperRepository(ApplicationDbContext context) : base(context) { } public IEnumerable<Developer> GetPopularDevelopers(int count) { return _context.Developers.OrderByDescending(d => d.Followers).Take(count).ToList(); } }
می توانید متوجه شوید که ما همه 7 تابع را در اینجا پیاده سازی نکرده ایم، زیرا قبلاً در مخزن عمومی ما پیاده سازی شده است. از نوشتن کد های تکراری جلوگیری شد درسته؟ همانطور که در ابتدای پست به آن اشاره شد از معایب دسترسی مستقیم به DbContext نوشتن کدهای تکراری بود.
به طور مشابه، اجازه دهید اینترفیس و پیاده سازی را برای ProjectRepository ایجاد کنیم.
public interface IProjectRepository : IGenericRepository<Project> { }
و در ادامه پیاده سازی آن
public class ProjectRepository : GenericRepository<Project>, IProjectRepository { public ProjectRepository(ApplicationDbContext context) : base(context) { } }
می بینید که رابط کاربری و پیاده سازی ها کاملا خالی هستند. پس چرا کلاس و رابط جدید برای Project ایجاد کنیم؟ این همچنین می تواند به یک عمل خوب در هنگام توسعه برنامه ها نسبت داده شود. ما همچنین پیشبینی میکنیم که در آینده، توابعی وجود داشته باشد که مختص موجودیت Project باشد.
در نهایت، اجازه دهید این اینترفیس ها را در پیاده سازی های مربوطه در Program.cs پروژه WebApi ثبت کنیم.
#region Repositories builder.Services.AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>)); builder.Services.AddTransient<IDeveloperRepository, DeveloperRepository>(); builder.Services.AddTransient<IProjectRepository, ProjectRepository>(); #endregion
الگوی Unit of Work Pattern یک الگوی طراحی است که با استفاده از آن می توانید Repositoryهای مختلفی را در برنامه در معرض دید قرار دهید. این ویژگی بسیار مشابه dbContext دارد، فقط Unit of Work مانند dbContext به Entity Framework Core کوپل نشده است.
نکته : DbContext یک کلاس مهم در Entity Framework API است. این یک پل بین کلاس های دامنه یا موجودیت شما و پایگاه داده است. DbContext کلاس اصلی است که وظیفه تعامل با پایگاه داده را بر عهده دارد.
الگوی Unit Of Work مفهومی است که به اجرای موثر الگوی Repository مربوط می شود.
تا به حال، ما چند Repositoryساختهایم. ما به راحتی می توانیم این Repositoryها را به سازنده کلاس های Services تزریق کنیم و به داده ها دسترسی داشته باشیم. زمانی که شما فقط 2 یا 3 شی Repository درگیر داشته باشید این کار بسیار آسان است. وقتی بیش از 3، Repository وجود دارد چه اتفاقی می افتد. افزودن تزریق های جدید هر چند وقت یکبار عملی نخواهد بود. برای اینکه همه Repositoryها را روی یک شیء قرار دهیم، از Unit Of Work استفاده می کنیم.
الگوی Unit of Work مسئول افشای مخازن موجود و ایجاد تغییرات در DataSource است تا از تراکنش کامل، بدون از دست رفتن داده اطمینان حاصل کند.
مزیت اصلی دیگر این است که چندین شی Repository نمونه های متفاوتی از dbcontext در درون خود دارند. این می تواند منجر به نشت داده ها در موارد پیچیده شود.
فرض کنید که شما باید یک توسعه دهنده جدید و یک پروژه جدید را در همان تراکنش وارد کنید. چه اتفاقی میافتد وقتی Developer جدید درج میشود اما Repository پروژه به دلایلی از کار میافتد. در سناریوهای دنیای واقعی، این کاملاً کشنده است. قبل از انجام هر گونه تغییر در پایگاه داده، باید اطمینان حاصل کنیم که هر دو مخزن به خوبی کار می کنند. دقیقاً به همین دلیل است که تصمیم گرفتیم SaveChanges را در هیچ یک از Repository ها قرار ندهیم.
در عوض، SaveChanges در کلاس UnitOfWork در دسترس خواهد بود.
کمی صبر کنید اگر متوجه تعاریف بالا از الگوی Unit Of Work نشده اید با مشاهده پیاده سازی آن درک بهتری خواهید داشت.
بیایید با IUnitOfWork شروع کنیم. یک اینترفیس جدید در دامنه Interfaces/IUnitOfWork ایجاد کنید
public interface IUnitOfWork : IDisposable { IDeveloperRepository Developers { get; } IProjectRepository Projects { get; } int Complete(); }
می بینید که ما اینترفیس های Repoitory مورد نیاز را در اینترفیس Unit Of Work لیست می کنیم. در نهایت ما یک متد "Complete" داریم که تغییرات را در پایگاه داده ذخیره می کند.
نکته : IDisposable یک اینترفیس است که شامل یک متد منفرد به نام ()Dispose برای آزاد کردن منابع مدیریت نشده مانند فایل ها، streams, database،connections و غیره است.
بیایید این اینترفیس را پیاده سازی کنیم. پیاده سازی را در پروژه DataAccess ایجاد کنید. یک کلاس جدید در UnitOfWork/UnitOfWork.cs اضافه کنید
public class UnitOfWork : IUnitOfWork { private readonly ApplicationDbContext _context; public UnitOfWork(ApplicationDbContext context) { _context = context; Developers = new DeveloperRepository(_context); Projects = new ProjectRepository(_context); } public IDeveloperRepository Developers { get; private set; } public IProjectRepository Projects { get; private set; } public int Complete() { return _context.SaveChanges(); } public void Dispose() { _context.Dispose(); } }
توجه داشته باشید در حالت ایده آل شما می خواهید یک لایه سرویس بین Repository و کنترلرها داشته باشید. اما برای اینکه همه چیز نسبتاً ساده باشد، اکنون از لایه سرویس اجتناب می کنیم.
قبل از آن، فراموش نکنیم که رابط IUnitofWork را در برنامه خود ثبت کنیم. به Program.cs بروید و این خط را اضافه کنید.
builder.Services.AddTransient<IUnitOfWork, UnitOfWork>();
خوب حالا یک Empty API Controller جدید در پروژه WebAPI در پوشه Controllers اضافه کنید.
[Route("api/[controller]")] [ApiController] public class DeveloperController : ControllerBase { private readonly IUnitOfWork _unitOfWork; public DeveloperController(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } }
در اینجا فقط شی IUnitOfWork تزریق می شود. به این ترتیب می توانید از نوشتن خطوط اضافی به کنترلرهای خود کاملاً خودداری کنید.
حال، فرض کنید برای این کنترلر به دو نقطه پایانی نیاز داریم. یک متد Post و یک متد GET.
خوب بیایید کار روی متد ها را شروع کنیم.
[HttpGet] public IActionResult GetPopularDevelopers([FromQuery] int count) { var popularDevelopers = _unitOfWork.Developers.GetPopularDevelopers(count); return Ok(popularDevelopers); }
با استفاده از شی _unitofWork، میتوانیم به متد سفارشی «GetPopularDeveloper» که ایجاد کردیم دسترسی پیدا کنیم. این مجموعه ای از توسعه دهندگان را برمی گرداند.
[HttpPost] public IActionResult AddDeveloperAndProject() { var developer = new Developer { Followers = 38, Name = "Farshid azizi" }; var project = new Project { Name = "Repository In Asp.Net Core" }; _unitOfWork.Developers.Add(developer); _unitOfWork.Projects.Add(project); _unitOfWork.Complete(); return Ok(); }
فرض کنید این خط از کد
_unitOfWork.Developers.Add(developer);
توسعه دهنده را در پایگاه داده ذخیره می کند، اما به دلایلی، خط این خط از کد
_unitOfWork.Projects.Add(project);
پروژه را ذخیره نمی کند. این می تواند برای برنامه ها به دلیل ناسازگاری داده ها بسیار کشنده باشد. با معرفی یک UnitOfWork، میتوانیم توسعهدهنده و پروژه را به صورت یکجا و در یک تراکنش ذخیره کنیم.
الگوی طراحی Unit of Work ، عملیات data persistence چندین شیء در business ما را به عنوان یک تراکنش atomic اعمال می کند، که تضمین میکند کل تراکنش committed یا rolled back میشود. الگوی طراحی Unit of Work چندین Repository را کپسوله می کند و database context واحدی را بین آنها به اشتراک می گذارد.
یکی از ویژگی های تراکنش ACID تراکنش atomic است، مجموعه ای تقسیم ناپذیر و تقلیل ناپذیر از عملیات پایگاه داده که یا همه اتفاق می افتد یا هیچ اتفاقی نمی افتد.
این الگو به شما کمک می کند تا به اصل خود را تکرار نکنید (DRY) پایبند باشید زیرا برای انجام عملیات CRUD نیازی به تکرار کد ندارید.
در اینجا همه چیز در مورد الگوی Repository در ASP.NET Core Application و Generic Repositories و Unit Of Work، روشی تمیزتر برای دسترسی به داده ها با پروژه های لایه ای و سایر سناریوهای موردی یاد گرفتیم. این تقریباً همه چیزهایی را که برای تبدیل شدن به یک حرفه ای در خصوص Repository Pattern در ASP.NET Core باید بدانید را پوشش می دهد.
بیشتر بخوانید : پیاده سازی Repositories (تکمیلی)
بيشتر بخوانيد : الگوی Specification در ASP.NET Core - بهبود Generic Repository Pattern
بیشتر بخوانید : نقشه راه توسعه دهندگان Asp.NET Core