الگوی Repository چیست و چرا باید از آن استفاده کنیم؟
در یک اپلیکیشن، با الگوی Repository ما یک لایه abstraction بین لایه data access و لایه business logic ایجاد میکنیم. با استفاده از این الگو، ما رویکرد بیشتری از loosely coupled را برای دسترسی به داده هایمان از پایگاه داده ترویج میدهیم. همچنین این الگو باعث میشود که کد تمیزتری داشته باشیم و نگهداری و استفاده مجدد از آن راحت تر باشد. منطق Data access با مسئولیت پایداری مدل business اپلیکیشن، در یک کلاس جداگانه و یا در مجموعه ای از کلاسها به نام repository قرار دارد.
موضوع ما در این مقاله، پیاده سازی الگوی repository است. علاوه بر این، این مقاله، یک ارتباط قوی با EF Core دارد.
این مقاله، قسمتی از مجموعه آموزشی زیر میباشد:
- ایجاد یک دیتابیس برای پروژه
- پیاده سازی کدهای پایه
- log سفارشی در ASP.NET Core
- الگوی Repository با Entity Framework Core(همین مقاله)
- استفاده از Repository برای درخواستهای GET
- استفاده از Repository برای درخواستهای POST، PUT و DELETE
اگر می خواهید تمام آموزشهای لازم و پایه مربوط به این دوره آموزشی را ببینید ، لطفاً روی این لینک کلیک کنید: صفحه مقدمه برای این آموزش.
برای مطالعه قسمت قبلی، این لینک را بررسی کنید: ایجاد پروژه NET Core WebApi. – لاگ سفارشی در NET Core.
این مقاله به چند قسمت زیر تقسیم میشود:
- ایجاد Model ها
- کلاس Context و Database Connection
- منطق الگوی Repository
- کلاسهای Repository کاربر
- ایجاد یک Repository Wrapper
- نتیجه گیری
ایجاد Model ها
کارمان را با ایجاد یک پروژه Class Library جدید به نام Entities شروع میکنیم و داخل آن، یک پوشه جدید به نام Models ایجاد میکنیم که شامل تمام model classe ها میباشد. model classe ها ارائه دهنده جداول داخل پایگاه داده میباشند و برای ما، داده ها را از پایگاه داده به NET Core. مپ میکنند. بعد از این، باید این پروژه را به پروژه اصلی، رفرنس دهیم.
در پوشه Models، ما دو کلاس ایجاد میکنیم و به این صورت آنها را تغییر میدهیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Entities.Models { [Table("owner")] public class Owner { public Guid OwnerId { get; set; } [Required(ErrorMessage = "Name is required")] [StringLength(60, ErrorMessage = "Name can't be longer than 60 characters")] public string Name { get; set; } [Required(ErrorMessage = "Date of birth is required")] public DateTime DateOfBirth { get; set; } [Required(ErrorMessage = "Address is required")] [StringLength(100, ErrorMessage = "Address cannot be longer than 100 characters")] public string Address { get; set; } public ICollection<Account> Accounts { get; set; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Entities.Models { [Table("account")] public class Account { public Guid AccountId { get; set; } [Required(ErrorMessage = "Date created is required")] public DateTime DateCreated { get; set; } [Required(ErrorMessage = "Account type is required")] public string AccountType { get; set; } [ForeignKey(nameof(Owner))] public Guid OwnerId { get; set; } public Owner Owner { get; set; } } } |
همانطور که میبینید، دو مدل وجود دارد که با ویژگی Table(“tableName”) ست شده اند. این ویژگی نام جدول مربوطه را در پایگاه داده پیکربندی می کند. تمام فیلدهای اجباری، ویژگی [Required]
را بر روی خود دارند و اگر بخواهیم رشته ها را محدود کنیم میتوانیم از ویژگی [StringLength] استفاده کنیم. در کلاس Owner، خصوصیت Accounts را داریم که دلالت بر این دارد که یک Owner میتواند چندین Account داشته باشد. علاوه بر این، ما خصوصیات OwnerId و Owner را با ویژگی [ForeignKey] ست کردیم به این معنی که یک Account
فقط دارای یک Owner میباشد.
کلاس Context و Database Connection
حال اجازه بدید تا کلاس context را ایجاد کنیم که یک کامپوننت middleware برای ارتباط با پایگاه داده میباشد. این کلاس، خصوصیت DbSet دارد که شامل داده های جدول از پایگاه داده میباشد.
در root پروژه Entities، یک کلاس به نام RepositoryContext ایجاد میکنیم و کدهای زیر را در آن قرار میدهیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using Entities.Models; using Microsoft.EntityFrameworkCore; namespace Entities { public class RepositoryContext: DbContext { public RepositoryContext(DbContextOptions options) :base(options) { } public DbSet<Owner> Owners { get; set; } public DbSet<Account> Accounts { get; set; } } } |
توجه داشته باشید که باید پکیج Microsoft.EntityFrameworkCore را نصب نمایید.
برای برقراری ارتباط بین NET Core. و پایگاه داده MySQL، باید یک کتابخانه third-party به نام Pomelo.EntityFrameworkCore.MySql را نصب کنیم. در پروژه اصلی، ما میتوانیم آن را با NuGet package manager یا Package manager console نصب کنیم.
بعد از نصب، فایل appsettings.json را باز میکنیم و تنظیمات connection پایگاه داده را داخل آن اضافه میکنیم:
1 2 3 4 5 6 7 8 9 10 11 |
{ "Logging": { "LogLevel": { "Default": "Warning" } }, "mysqlconnection": { "connectionString": "server=localhost;userid=root;password=yourpass;database=accountowner;" }, "AllowedHosts": "*" } |
در کلاس ServiceExtensions، ما کدی را برای پیکربندی MySQL context مینویسیم.
اول، directive ها را با using اضافه میکنیم و سپس متد ConfigureMySqlContext را اضافه میکنیم:
1 2 3 4 5 6 7 8 |
using Microsoft.Extensions.Configuration; using Microsoft.EntityFrameworkCore; public static void ConfigureMySqlContext(this IServiceCollection services, IConfiguration config) { var connectionString = config["mysqlconnection:connectionString"]; services.AddDbContext<RepositoryContext>(o => o.UseMySql(connectionString)); } |
با کمک پارامتر IConfiguration config، میتوانیم به فابل appsettings.json و تمام داده هایی که از آن نیاز داریم دسترسی داشته باشیم.
اگر از پروژه NET 5. و از ورژن 5 کتابخانه Pomelo استفاده میکنید باید از کدی متفاوت در متد ConfigureMySqlContext استفاده کنید. متد UseMySql اینجا یک پارامتر دیگر نیز دارد:
1 2 |
services.AddDbContext<RepositoryContext> (o => o.UseMySql(connectionString, MySqlServerVersion.LatestSupportedServerVersion)); |
سپس در کلاس Startup، درون متد ConfigureServices، سرویس context را به IOC درست بالای ()services.AddControllers اضافه میکنیم:
1 |
services.ConfigureMySqlContext(Configuration); |
منطق الگوی Repository
بعد از ایجاد اتصال به پایگاه داده، باید یک generic repository ایجاد کنیم تا برای تمام متدهای CRUD بتوانیم از آن استفاده کنیم. به عبارتی، تمام متدها میتوانند تحت هر کلاس repository در پروژه فراخوانی شوند.
علاوه بر این، ایجاد generic repository و کلاسهای repository که از این generic repository استفاده میکنند مرحله نهایی نمیباشد. بلکه بعد از آن، قرار است یک مرحله جلوتر برویم و یک wrapper برای کلاسهای repository ایجاد کنیم و آن را به عنوان یک سرویس، inject کنیم. در نتیجه، در هر کنترلری که نیاز داریم میتوانیم این wrapper را فقط یکبار نمونه سازی کنیم و سپس هر کلاس repository ای را که بخواهیم توسط آن فراخوانی کنیم. زمانیکه ما از این wrapper در پروژه استفاده میکنیم به مزایای آن پی خواهید برد.
اول، یک اینترفیس برای repository داخل پروژه Contracts میسازیم:
1 2 3 4 5 6 7 8 9 10 11 |
namespace Contracts { public interface IRepositoryBase<T> { IQueryable<T> FindAll(); IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression); void Create(T entity); void Update(T entity); void Delete(T entity); } } |
بعد از ایجاد اینترفیس، یک پروژه جدید از نوع Class Library (.NET Core) به نام Repository ایجاد میکنیم (reference را از Contracts و Entities به این پروژه اضافه میکنیم) و داخل پروژه Repository، یک کلاس abstract به نام RepositoryBase که اینترفیس IRepositoryBase را پیاده سازی میکند را ایجاد میکنیم.
این پروژه را به پروژه اصلی نیز رفرنس دهید.
کد زیر را به کلاس RepositoryBase اضافه میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
using Contracts; using Entities; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace Repository { public abstract class RepositoryBase<T> : IRepositoryBase<T> where T : class { protected RepositoryContext RepositoryContext { get; set; } public RepositoryBase(RepositoryContext repositoryContext) { this.RepositoryContext = repositoryContext; } public IQueryable<T> FindAll() { return this.RepositoryContext.Set<T>().AsNoTracking(); } public IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression) { return this.RepositoryContext.Set<T>().Where(expression).AsNoTracking(); } public void Create(T entity) { this.RepositoryContext.Set<T>().Add(entity); } public void Update(T entity) { this.RepositoryContext.Set<T>().Update(entity); } public void Delete(T entity) { this.RepositoryContext.Set<T>().Remove(entity); } } } |
این کلاس abstract، همانند اینترفیس IRepositoryBase از نوع generic T برای کار با type های مختلف استفاده میکند. این نوع T امکان reusability بیشتری را به کلاس RepositoryBase میدهد. این به این معنی است که ما مجبور نیستیم که مدل مدنظرمان (class) را درست همین حالا برای RepositoryBase برای کار با آن تعیین کنیم، بلکه میخواهیم این کار را بعدا انجام دهیم.
کلاسهای Repository کاربر
حالا که ما کلاس RepositoryBase را داریم، classe های کاربری که از این کلاس abstract مشتق میشوند (ارث بری میکنند) را ایجاد میکنیم. هر class کاربر، اینترفیس مخصوص به خودش را برای متدهای اضافی ویژه مدل خودش خواهد داشت. علاوه بر این، با ارث بری کردن از کلاس RepositoryBase، آنها به تمام متدها از RepositoryBase دسترسی خواهند داشت. به این ترتیب ، ما logic را که برای همه کلاسهای repository کاربر مشترک است و از طرفی هم برای هر کلاس کاربر خاص است را جدا می کنیم.
اینترفیس ها را در پروژه Contracts برای کلاسهای Owner
و Account
ایجاد میکنیم.
فراموش نکنید که رفرنس را از پروژه Entities اضافه کنید. به محض اینکه این کار را انجام میدهیم، میتوانیم رفرنس Entities را از پروژه اصلی حذف کنیم. زیرا آن در حال حاضر از طریق پروژه Repository که از قبل شامل رفرنس پروژه Contracts میباشد به همراه رفرنس Entities
داخل آن، ارائه شده است.
1 2 3 4 5 6 7 8 |
using Entities.Models; namespace Contracts { public interface IOwnerRepository : IRepositoryBase<Owner> { } } |
1 2 3 4 5 6 7 8 |
using Entities.Models; namespace Contracts { public interface IAccountRepository : IRepositoryBase<Account> { } } |
حال کلاسهای repository کاربر را در پروژه Repository ایجاد میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Contracts; using Entities; using Entities.Models; namespace Repository { public class OwnerRepository : RepositoryBase<Owner>, IOwnerRepository { public OwnerRepository(RepositoryContext repositoryContext) :base(repositoryContext) { } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Contracts; using Entities; using Entities.Models; namespace Repository { public class AccountRepository : RepositoryBase<Account>, IAccountRepository { public AccountRepository(RepositoryContext repositoryContext) :base(repositoryContext) { } } } |
بعد از این مراحل، ما ایجاد repository و کلاسهای repository کاربر را تمام میکنیم. اما هنوز کارهای دیگری نیز هست که باید انجام دهیم.
ایجاد یک Repository Wrapper
بیایید تصور کنیم اگر داخل یک کنترلر نیاز به جمع آوری همه Owner ها و جمع آوری فقط برخی از Account های خاص داشته باشیم (برای مثال فقط اکانتهای Domestic)، باید کلاسهای OwnerRepository
و AccountRepository
را نمونه سازی کنیم و متدهای FindAll و FindByCondition را فراخوانی کنیم.
این شاید زمانیکه فقط دو کلاس داشته باشیم مسئله ای نباشد، اما اگر منطقی از 5 کلاس مختلف و یا بیشتر را نیاز داشته باشیم آنوقت تکلیف چه میشود. با توجه به این موضوع، یک wrapper برای کلاسهای repository کاربر ایجاد میکنیم. سپس آن را داخل IOC قرار میدهیم و در آخر آن را داخل سازنده کنترلر تزریق میکنیم. حالا با این نمونه wrapper ها، این امکان وجود دارد که هر کلاس repository که نیاز داشته باشیم را فراخوانی کنیم.
حال یک اینترفیس جدید در پروژه Contract ایجاد میکنیم:
1 2 3 4 5 6 7 8 9 |
namespace Contracts { public interface IRepositoryWrapper { IOwnerRepository Owner { get; } IAccountRepository Account { get; } void Save(); } } |
بعد از آن، یک کلاس جدید به پروژه Repository اضافه میکنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
using Contracts; using Entities; namespace Repository { public class RepositoryWrapper : IRepositoryWrapper { private RepositoryContext _repoContext; private IOwnerRepository _owner; private IAccountRepository _account; public IOwnerRepository Owner { get { if(_owner == null) { _owner = new OwnerRepository(_repoContext); } return _owner; } } public IAccountRepository Account { get { if(_account == null) { _account = new AccountRepository(_repoContext); } return _account; } } public RepositoryWrapper(RepositoryContext repositoryContext) { _repoContext = repositoryContext; } public void Save() { _repoContext.SaveChanges(); } } } |
همانطور که میبینید، ما خصوصیتهایی که repository های مربوطه را در اختیار ما قرار میدهند را ایجاد میکنیم و همچنین متد ()Save را داریم که بعد از تمام تغییراتی که بر روی یک آبجکت مشخص صورت میگیرد استفاده میکنیم. این یک شیوه مناسب است زیرا به فرض مثال، حالا میتوانیم دو owner اضافه کنیم، دو account را تغییر دهیم و یک owner را حذف کنیم و تمام اینها را در یک متد انجام دهیم و سپس در آخر فقط یکبار متد Save را فراخوانی کنیم. حال تمام تغییرات بر روی آبجکت اعمال خواهد شد و یا اینکه اگر چیزی به مشکل بخورد، تمام تغییرات برگردانده میشوند:
1 2 3 4 5 6 7 |
_repository.Owner.Create(owner); _repository.Owner.Create(anotheOwner); _repository.Account.Update(account); _repository.Account.Update(anotherAccount); _repository.Owner.Delete(oldOwner); _repository.Save(); |
در کلاس ServiceExtensions، ما این کد را اضافه میکنیم:
1 2 3 4 |
public static void ConfigureRepositoryWrapper(this IServiceCollection services) { services.AddScoped<IRepositoryWrapper, RepositoryWrapper>(); } |
و در کلاس Startup داخل متد ConfigureServices بالای خط ()services.AddControllers، این کد را اضافه میکنیم:
1 |
services.ConfigureRepositoryWrapper(); |
بسیار خب.
تست
حال تمام کاری که باید انجام دهیم این است که به همان شیوه ای که logger سفارشی که در قسمت 3 این سری آموزش تست کردیم را تست کنیم.
سرویس RepositoryWrapper را به داخل کنترلر WeatherForecast تزریق کنید و هر متدی از کلاس RepositoryBase را فراخوانی کنید:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
[ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private IRepositoryWrapper _repoWrapper; public WeatherForecastController(IRepositoryWrapper repoWrapper) { _repoWrapper = repoWrapper; } // GET api/values [HttpGet] public IEnumerable<string> Get() { var domesticAccounts = _repoWrapper.Account.FindByCondition(x => x.AccountType.Equals("Domestic")); var owners = _repoWrapper.Owner.FindAll(); return new string[] { "value1", "value2" }; } } |
داخل متد ()Get، یک breakpoint بگذارید و داده های برگشتی از پایگاه داده را ببینید.
در قسمت بعدی، به شما نشان میدهیم که اگر نمیخواهید که متدهای RepositoryBase اینجا در کنترلر در معرض نمایش قرار بگیرند را چطور محدود کنید.
نتیجه گیری
الگوی Repository، سطح انتزاع در کدتان را افزایش میدهد. این ممکن است که باعث شود که درک برای توسعه دهندگانی که با این الگو آشنا نیستند دشوارتر شود. اما زمانیکه شما با این الگو آشنایی دارید، این الگو مقدار کدهای زائد را کاهش میدهد و منطق را برای نگهداری راحت تر میسازد.
در این پست، شما یاد گرفتید که:
- الگوی repository چیست
- چطور مدل و ویژگی های مدل را اضافه کنیم
- چطور کلاس context و database connection را بسازیم
- راه صحیح ایجاد منطق repository چیست
- و راه ایجاد یک wrapper برای کلاسهای repository تان چیست
بابت مطالعه این مقاله از شما سپاسگذاریم. امیدوارم که اطلاعات مفیدی را در این مقاله دریافت کرده باشید.
به زودی شما را در مقاله بعدی میبینیم، جاییکه ما از منطق repository برای ایجاد درخواستهای HTTP استفاده میکنیم.