Repository Và Unit Of Work

ndtung449@gmail.com | 1173 day | 4907


Repository và Unit of Work là 2 design pattern thường đi chung với nhau để tách biệt giữa Business Logic Layer và Data Access Layer.

Nếu project nhỏ, đơn giản thì mình đừng áp dụng 2 pattern này. Sẽ tăng số lượng class, interface mà không giải quyết vấn đề gì. Tạo ra Service class chứa DbContext rồi xử lý business logic và access data trên đó là đủ rồi. Còn nếu “Keep It Stupid Simple” thì cứ code thẳng trên Controller, khỏi service.

Dưới đây là 2 hình so sánh giữa không apply và có apply Repository và UoW (tham khảo ở đây):

Bản chất của Repository là wrapper của DbSet. Còn UoW là để chứa tất cả Repository và đảm bảo các Repository đó xài chung 1 DbContext duy nhất, từ đó tạo nên Transaction – tất cả các hoạt động update database bởi các Repository trong cùng 1 business action sẽ hoặc là success hết hoặc là fail hết nhằm đảm bảo tính consistent của data. Như đoạn code dưới đây, add "order" sau đó add toàn bộ "ordertails" rồi mới save:

Không có Repository và UoW chúng ta vẫn đạt được tính Transaction bằng cách lấy những DbSet liên quan ra, thêm, xóa, sửa và cuối cùng gọi DbContext.SaveChange(). Kết thúc Transaction.

Nguyên nhân nào khiến 1 transaction bị failed? Data không thỏa mãn các constrains, db server bị vấn đề phần cứng (CPU, Ram, Disk, Network)

Khi nào thì sử dụng Repository và UoW?

  • Định ra những interface (repository interfaces) cho phép một số chỉnh sửa hay thay thế bằng ORM framework khác mà không ảnh hưởng tới Business logic layer. (Chém gió thôi, mình chưa thấy ai change mấy cái này hết).
  • Custom query
  • Điểm tập trung để add log trace (user nào create, update data nào)

Sắp tới có thể mình sẽ viết tiếp về Custom query và log trace. Sau đây là một ví dụ implement Repository và UoW, các bạn cũng có thể tham khảo cách của Microsoft. Cách của mình cũng là dựa theo Microsoft, áp dụng thêm DTO (Data Transfer Object), AutoMapper để map giữa Entity và Dto. Ngoài ra mình còn tạo thêm Service class để tách business logic ra khỏi controller. Hy vọng có thể giúp các bạn newbie hệ thống được những bước cần thực hiện.

Các class và interface cần implement:

  • Entities: Order, OrderDetails
  • Db context: ApplicationContext
  • Dto: OrderDto, OrderDetailsDto
  • Repository: IGenericRepository, GenericRepository
  • Unit of work: IUnitOfWork, UnitOfWork
  • Service: IBaseService, BaseService, IOrderDetailsService, OrderDetailsService
  • Api Controller: OrderDetailsController

Theo thứ tự như trên chúng ta add các class với content như sau:

    public class Order
    {
        public int Id { get; set; }
        public string Sku { get; set; }
        public string CreatedBy { get; set; }
        public DateTime CreatedAt { get; set; }
        public virtual ICollection<OrderDetails> OrderDetails { get; set; }
    }

[MaxLength(10)] dưới đây chỉ dùng để demo 1 trường hợp transaction bị failed

    public class OrderDetails
    {
        public int Id { get; set; }
        [MaxLength(10)] // just for demonstrating a case of transaction failure
        public string ProductName { get; set; }
        public decimal Price { get; set; }
        public int OrderId { get; set; }
        public virtual Order Order { get; set; }
        public DateTime CreatedAt { get; set; }
    }

ApplicationContext với DbSet cho 2 entity ở trên

    public class ApplicationContext : DbContext
    {
        public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options)
        {
        }
        
        public DbSet<Order> Order { get; set; }
        public DbSet<OrderDetails> OrderDetails { get; set; }
    }

Entity là để tương tác với Db, Dto để tương tác với user

    public class OrderDto
    {
        public int Id { get; set; }
        public string Sku { get; set; }
        public string CreatedBy { get; set; }
        public DateTime CreatedAt { get; set; }
        public virtual ICollection<OrderDetails> OrderDetails { get; set; }
    }
    public class OrderDetailsDto
    {
        public int Id { get; set; }
        [MaxLength(10)]
        public string ProductName { get; set; }
        public decimal Price { get; set; }
        public int OrderId { get; set; }
        public Order Order { get; set; }
        public DateTime CreatedAt { get; set; }
    }

IGenericRepository này define những CRUD cơ bản cho một Repository. Class GenericRepository implement interface này sẽ được sử dụng cho tất cả các repository. Phần implement hơi dài, các bạn tham khảo link github mình để cuối bài.

    public interface IGenericRepository<TEntity> where TEntity : class
    {
        IQueryable<TEntity> Find(Expression<Func<TEntity, bool>> filter = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            string includeProperties = "");
        Task<TEntity> FindByIdAsync(object id);
        void Add(TEntity entity);
        void Delete(object id);
        void Delete(TEntity entityToDelete);
        void Update(TEntity entityToUpdate);
    }

Unit of Work sẽ chứa 2 repository cho 2 entity và hàm SaveAsync

    public interface IUnitOfWork : IDisposable
    {
        IGenericRepository<Order> OrderRepository { get; }
        IGenericRepository<OrderDetails> OrderDetailsRepository { get; }
        Task SaveAsync();
    }
    public class UnitOfWork : IUnitOfWork
    {
        private ApplicationContext _context;

        public UnitOfWork(ApplicationContext context)
        {
            _context = context;
            InitRepositories();
        }
        
        public async Task SaveAsync()
        {
            await _context.SaveChangesAsync();
        }

        private bool _disposed = false;

        public IGenericRepository<Order> OrderRepository { get; private set; }

        public IGenericRepository<OrderDetails> OrderDetailsRepository { get; private set; }

        private void InitRepositories()
        {
            OrderRepository = new GenericRepository<Order>(_context);
            OrderDetailsRepository = new GenericRepository<OrderDetails>(_context);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    _context.Dispose();
                }
            }

            _disposed = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }

Base service chứa những CRUD dùng chung cho tất cả service. Mỗi service của các bạn cũng sẽ có 1 interface riêng, kế thừa interface này. Sau đó các bạn có thể thêm method hoặc override các CRUD này. Interface này yêu cầu truyền vào Entity và Dto tương ứng. Service là trung gian giữa Controller và các Repository, Controller chỉ quan tâm tới Dto, Repository chỉ biết Entity. Nhiệm vụ của Service là map từ Dto qua Entity và ngược lại.

    public interface IBaseService<TEntity, TDto>
        where TEntity : class
        where TDto : class
    {
        Task<TDto> CreateAsync(TDto dto);
        Task<TDto> UpdateAsync(TDto dto);
        Task DeleteAsync(object keyValues);
        Task<TDto> FindByIdAsync(object keyValues);
        Task<PaginatedList<TEntity, TDto>> FindAsync(Expression<Func<TEntity, bool>> filter = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, int page = 1);
    }

OrderDetailsService kế thừa IBaseService

    public interface IOrderDetailsService : IBaseService<OrderDetails, OrderDetailsDto>
    {
        Task SubmitAsync(string createBy, IEnumerable<OrderDetailsDto> orderDetails);
    }

và đây là cách implement:

    public class OrderDetailsService : BaseService<OrderDetails, OrderDetailsDto>, IOrderDetailsService
    {
        public OrderDetailsService(IUnitOfWork unitOfWork) : base(unitOfWork)
        {
        }

        protected override IGenericRepository<OrderDetails> _reponsitory => _unitOfWork.OrderDetailsRepository;

        public async Task SubmitAsync(string createBy, IEnumerable<OrderDetailsDto> orderDetails)
        {
            var orderDto = new OrderDto
            {
                CreatedAt = DateTime.Now,
                CreatedBy = createBy,
                Sku = Guid.NewGuid().ToString("n").Substring(0, 6)
            };

            var orderEntity = Mapper.Map<Order>(orderDto);

            _unitOfWork.OrderRepository.Add(orderEntity);

            foreach (var details in orderDetails)
            {
                details.Order = orderEntity;
                _unitOfWork.OrderDetailsRepository.Add(DtoToEntity(details));
            }

            await _unitOfWork.SaveAsync();
        }
    }

Sử dụng service trên controller:

    [Route("api/[controller]")]
    public class OrderDetaisController : Controller
    {
        private readonly IOrderDetailsService _orderDetailsService;

        public OrderDetaisController(IOrderDetailsService orderDetailsService)
        {
            _orderDetailsService = orderDetailsService;
        }

        [HttpPost]
        [Route("{createBy}")]
        public async Task Post(string createBy, [FromBody] IDictionary<string, IEnumerable<OrderDetailsDto>> dic)
        {
            var orderDetailsList = dic["orderDetails"];
            await _orderDetailsService.SubmitAsync(createBy, orderDetailsList);
        }
    }

Demo test

Dùng post man để post data:

Kiểm tra data đã được lưu xuống db:

Post product có tên dài hơn 10 ký tự. Để ý hàm SubmitAsync của OrderDetailsService, "order" được add trước rồi add từng "order details". Trong trường hợp này, khi exception xảy ra, chúng ta expect rằng không có order hay product nào được lưu xuống db.

Exception xảy ra và các bạn kiểm tra lại db xem có thêm order, hay product không nhé.

Full source code: download here

Cám ơn các bạn đã theo bài viết. Trên đây chỉ là ý kiến cá nhân của mình. Các bạn có ý kiến khác hay thắc mắc gì vui lòng log in và để lại comment nhé.

 


Top Articles

Bất Đầu Với WebApi Và Dot Net Core (.Net Core)

1254 day
Butter Ngo
Views 8638
Comments 0

Dot Net Core Bearer Token With (JWT) (.Net Core)

1187 day
Butter Ngo
Views 5332
Comments 0

Bắt Đầu Với Dot NET Core (.Net Core)

1266 day
Butter Ngo
Views 4329
Comments 0

Dapper Repository & Unit of Work (.Net)

1113 day
Tung Nguyen
Views 2527
Comments 0

Top Question

Bi lỗi Invalid Column Name khi sử dụng LinQ (.Net)

1114 day
Bảo Dương
Views 1281
Answers 2

.NET CORE API JWT (.Net Core)

407 day
huynhminhnhut97@gmail.com
Views 870
Answers 2

Làm thế nào để lấy information từ token (.Net Core)

503 day
ngovu.dl@gmail.com
Views 805
Answers 1