© 2020, Developed by Hieu Dev

Triển khai CQRS Pattern với MediatR trong ASP.NET Core

Trong bài viết này, mình sẽ đề cập đến CQRS Pattern và hướng dẫn triển khai một project WebAPI với MediatR, Entity Framework Core một cách dễ dàng, dễ hiểu và nhanh chóng.

Triển khai CQRS Pattern với MediatR trong ASP.NET Core

CQRS là gì?

CQRS (hay Command Query Responsibility Segregation) là một design pattern phân tách các hoạt động read và write dữ liệu. Trong đó chia việc tương tác với dữ liệu thành 2 thành phần Command Query. Hai thành phần này tách biệt và độc lập với nhau.

Command được hiểu là Database Command, nó dùng để thay đổi trạng thái của hệ thống nhưng không trả về data, chảng hạn như bạn thực hiện Insert/Update hoặc Delete dữ liệu.

Query ở đây là khi bạn trả về data mà không thay đổi trạng thái của hệ thống.

Mediator Pattern 

Mediator Pattern là một design pattern giúp giảm đáng kể sự kết hợp giữa các thành phần khác nhau của ứng dụng bằng cách làm cho chúng giao tiếp gián tiếp. Là một behavioral pattern nên có rất nhiều thứ hay ho để đề cập, và mình sẽ đề cập ở các bài viết sau này. Và suy cho cùng, Mediator pattern rất phù hợp để triểm khai CQRS.

Để triển khai pattern này, ta sẽ sử dụng thư viện MediatR, và việc bây giờ cần làm là cài đặt thư viện này và triển khai.


Triển khai CQRS trong .NET Core

Để bắt đầu triển khai CQRS Pattern với MediatR trong ASP.NET Core, đầu tiên bạn cần mở Visual Studio lên và tạo mới project với ASP.NET Core Web API, và tuần tự thực hiện các bước sau:

Bước 1: Đầu tiên, bạn cần cài đặt các NuGet Package sau:
  • MediatR
  • MediatR.Extensions.Microsoft.DependencyInjection
  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

Bước 2: Trong thư mục Models, tạo mới class entity cho Product với code sau:

Models/Product.cs


public class Product
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public string Detail { get; set; }
        public decimal Price { get; set; }
        public int Quantity { get; set; }
    }


Bước 3: Tiếp theo, ta cần tạo Db Context, cụ thể bạn tạo thư mục Context và chứa 2 files lần lượt như sau:

Context/ApplicationContext.cs:


public class ApplicationContext : DbContext
    {
        public ApplicationContext(DbContextOptions options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
        }

        public DbSet<Product> Products { get; set; }
    }


Context/DataDbContextFactory.cs:

public class DataDbContextFactory : IDesignTimeDbContextFactory<ApplicationContext>
    {
        public ApplicationContext CreateDbContext(string[] args)
        {
            IConfigurationRoot configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .Build();

            var connectionString = configuration.GetConnectionString("ConnectionString");

            var optionsBuilder = new DbContextOptionsBuilder<ApplicationContext>();
            optionsBuilder.UseSqlServer(connectionString);

            return new ApplicationContext(optionsBuilder.Options);
        }
    }


Bước 4: Bây giờ bạn cần cấu hình chuỗi kết nối tới SQL Server trong appsetting.json:

"ConnectionStrings": {
    "ConnectionString": "Server=HIUPC;Database=DbCQRSSolution;Trusted_Connection=True;MultipleActiveResultSets=true"
  },


Và bạn cần thay đổi server name, database name cho phù hợp với cấu hình của bạn. 

Bước 5: Ta vào Program.cs và thêm đoạn code sau bên trong:

builder.Services.AddDbContext<DataDbContext>(options => options.UseSqlServer(
                            builder.Configuration.GetConnectionString("ConnectionString")));


Bước 6: Vào Tools > NuGet Package Manager > NuGet Package Console và thực thi lần lượt 2 command sau: 
  • Add-Migration InitialCreate
  • Update-database 

Bước 7: Sau khi đã có database, bây giờ ta sẽ thực hiện CRUD theo CQRS. Như hình dưới đây, bạn sẽ thấy được cách bố trí code, ta sẽ đặt thư mục Features làm thư mục root và bên trong sẽ chứa thư mục Commands Queries, đảm nhận các nhiệm vụ khác nhau như chúng ta đã đề cập ở đầu bài viết.


Trước tiên, ta sẽ tạo các Query trước, và cũng như định nghĩa, bạn lưu ý rằng chúng ta sẽ viết các query trong thư mục này. Trong thư mục Queries, lần lượt tạo các file với code sau:

Queries/GetAllProductsQuery.cs:

public class GetAllProductsQuery : IRequest<IEnumerable<Product>>
    {

        public class GetAllProductsQueryHandler : IRequestHandler<GetAllProductsQuery, IEnumerable<Product>>
        {
            private readonly ApplicationContext _context;
            public GetAllProductsQueryHandler(ApplicationContext context)
            {
                _context = context;
            }
            public async Task<IEnumerable<Product>> Handle(GetAllProductsQuery query, CancellationToken cancellationToken)
            {
                var productList = await _context.Products.ToListAsync();
                if (productList == null)
                {
                    return null;
                }
                return productList.AsReadOnly();
            }
        }
    }


Queries/GetProductByIdQuery.cs:
public class GetProductByIdQuery : IRequest<Product>
    {
        public Guid Id { get; set; }
        public class GetProductByIdQueryHandler : IRequestHandler<GetProductByIdQuery, Product>
        {
            private readonly ApplicationContext _context;
            public GetProductByIdQueryHandler(ApplicationContext context)
            {
                _context = context;
            }
            public async Task<Product> Handle(GetProductByIdQuery query, CancellationToken cancellationToken)
            {
                var product = _context.Products.Where(a => a.Id == query.Id).FirstOrDefault();
                if (product == null) return null;
                return product;
            }
        }
    }


Như code trên, ta thấy được các query sẽ implements interface IRequest để thư viện MediatR biết là class này là một query hay command. Sau đó, tùy thuộc vào query bạn muốn trả về, bạn sẽ truyển một IEnumerable<Product> cho query get list hay một entuty Product chi việc query theo id.

Mỗi request (Query/Command) đều sẽ cần một handler cho chính nó. Handler sẽ xác định các việc cần làm khi nhận yêu cầu từ client. Vì vậy dưới mỗi 2 queries trên, mình cũng đã thực hiện viết một handle và lần lượt truyển các tham số ứng với quest sẽ xử lý, và kết quả trả về của request.

Bước 8: Bây giờ, chúng ta sẽ tiến hành tạo các Commands, cụ thể là các commands để insert, update, delete dữ liệu.

Trong thư mục Commands, lần lượt tạo các file với code tương ứng như sau:

Commands/CreateProductCommand.cs:

public class CreateProductCommand : IRequest<Guid>
    {
        public string Name { get; set; }
        public string Detail { get; set; }
        public decimal Price { get; set; }
        public int Quantity { get; set; }
        public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Guid>
        {
            private readonly ApplicationContext _context;
            public CreateProductCommandHandler(ApplicationContext context)
            {
                _context = context;
            }
            public async Task<Guid> Handle(CreateProductCommand command, CancellationToken cancellationToken)
            {
                var product = new Product();
                product.Name = command.Name;
                product.Detail = command.Detail;
                product.Price = command.Price;
                product.Quantity = command.Quantity;
                _context.Products.Add(product);
                await _context.SaveChangesAsync();
                return product.Id;
            }
        }
    }


Commands/UpdateProductCommand.cs:

public class UpdateProductCommand : IRequest<Guid>
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public string Detail { get; set; }
        public decimal Price { get; set; }
        public int Quantity { get; set; }
        public class UpdateProductCommandHandler : IRequestHandler<UpdateProductCommand, Guid>
        {
            private readonly ApplicationContext _context;
            public UpdateProductCommandHandler(ApplicationContext context)
            {
                _context = context;
            }
            public async Task<Guid> Handle(UpdateProductCommand command, CancellationToken cancellationToken)
            {
                var product = _context.Products.Where(a => a.Id == command.Id).FirstOrDefault();

                if (product == null)
                {
                    return default;
                }
                else
                {
                    product.Name = command.Name;
                    product.Detail = command.Detail;
                    product.Price = command.Price;
                    product.Quantity = command.Quantity;
                    await _context.SaveChangesAsync();
                    return product.Id;
                }
            }
        }
    }


Commands/DeleteProductByIdCommand.cs:

 
public class DeleteProductByIdCommand : IRequest<Guid>
    {
        public Guid Id { get; set; }
        public class DeleteProductByIdCommandHandler : IRequestHandler<DeleteProductByIdCommand, Guid>
        {
            private readonly ApplicationContext _context;
            public DeleteProductByIdCommandHandler(ApplicationContext context)
            {
                _context = context;
            }
            public async Task<Guid> Handle(DeleteProductByIdCommand command, CancellationToken cancellationToken)
            {
                var product = await _context.Products.Where(a => a.Id == command.Id).FirstOrDefaultAsync();
                if (product == null) return default;
                _context.Products.Remove(product);
                await _context.SaveChangesAsync();
                return product.Id;
            }
        }
    }


Việc thực hiện các Commands cũng giống như tạo các queries ở trên, ở đây bạn cũng viết các Handler tướng ứng trong mỗi file tương ứng.

Bước 9: Bây giờ ta sẽ gọi các commands và queries ra controller đơn giản như sau:

Controller/ProductsController.cs:


[Route("api/product")]
    [ApiController]
    public class ProductsController : ControllerBase
    {
        private readonly IMediator _mediator;

        public ProductsController(IMediator mediator)
        {
            _mediator = mediator;
        }

        [HttpGet]
        public async Task<IActionResult> Get()
        {
            return Ok(await _mediator.Send(new GetAllProductsQuery()));
        }

        [HttpGet("{id}")]
        public async Task<IActionResult> GetById(Guid id)
        {
            return Ok(await _mediator.Send(new GetProductByIdQuery { Id = id }));
        }

        [HttpPost]
        public async Task<IActionResult> Create(CreateProductCommand command)
        {
            return Ok(await _mediator.Send(command));
        }

        [HttpPut("{id}")]
        public async Task<IActionResult> Update(Guid id, UpdateProductCommand command)
        {
            if (id != command.Id)
            {
                return BadRequest();
            }
            return Ok(await _mediator.Send(command));
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(Guid id)
        {
            return Ok(await _mediator.Send(new DeleteProductByIdCommand { Id = id }));
        }

    }


Khi gọi phương thức _mediator.Send() và truyền vào tên request, mediator sẽ gọi tới handler tương ứng của request. Về cơ bản, controller sẽ không cần biết bất kỳ implement, điều đó giúp controller sẽ clean hơn, mọi xử lý phức tạp đẵ được Mediator quản lý.

Bước 10: Bây giờ, ta cần cấu hình dependency injection. Ở các phiện bản trước .NET 6, bạn sẽ thêm trong ConfigureServices(), và ở .NET 6 thì bạn cấu hình như sau trong Program.cs:

builder.Services.AddMediatR(typeof(Program).Assembly);

Các bước thực hiện đã hoàn tất, bây giờ bạn chỉ cần run project và tận hưởng kết quả.

Lời kết

Trước khi tiếp cận một pattern mới như này, bạn nên tìm hiểu kĩ và làm được CRUD, như vậy giúp cho việc tiếp cận trở nên dễ dàng hơn. Source code mình cũng đã publish lên github, các bạn có thể tham khảo tại link dưới đây:


Vâng, cũng còn những cái hẹn cho những pattern khác trong ASP.NET nữa, mong sẽ được các bạn quan tâm và theo dõi.

Mong bài viết hữu ích đến các bạn, chúc các bạn thành công,

Hieu Ho.

2 Nhận xét

Mới hơn Cũ hơn