Giới thiệu
CQRS được lấy cảm hứng từ mô hình Command Query Separation (CQS) do Bertrand Meyer đề xuất trong cuốn sách "Object Oriented Software Construction". Triết lý chính của CQS là
A method should either change state of an object, or return a result, but not both. In other words, asking the question should not change the answer. More formally, methods should return a value only if they are referentially transparent and hence possess no side effects.” (Wikipedia)
Dựa trên nguyên tắc này, CQS phân chia các phương thức thành hai nhóm:
- Command: Thay đổi trạng thái của đối tượng hoặc toàn bộ hệ thống (còn gọi là modifiers hoặc mutators).
- Query: Trả về kết quả và không thay đổi trạng thái của đối tượng.
Trong thực tế, việc phân biệt ra Query và Command khá đơn giản. Bạn có thể tìm hiểu thêm bài viết tại đây
CQRS: Bản nâng cấp của CQS
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 và Query. Hai thành phần này tách biệt và độc lập với nhau.
Để giao tiếp với database, chúng ta thường dùng mô hình thông dụng là Repository với 4 phương thức cơ bản Insert, Get, Update, Delete (CRUD). Data được lưu trữ một chỗ và ứng dụng tương tác với chỗ đó để cả tạo, đọc, sửa, xóa. CQRS thì khác, CQRS tách thành hai mô hình Command và Query (tương tự như CQS) nhưng model Read và Write độc lập với nhau.
CQRS đang cố gắng giải quyết vấn đề gì?
Khi thiết kế hệ thống, chúng ta thường bắt đầu với việc thiết kế lưu trữ dữ liệu. Đầu tiên là chuẩn hóa dữ liệu (database normalization), thêm primary key và foreign keys để thực thi tính toàn vẹn tham chiếu, thêm index, để đảm bảo tối ưu hóa cho việc read-write. Hoặc chúng ta suy nghĩ về case read, thêm data vào database trước...
Không có cách nào là sai. Vấn đề là sự cân bằng giữa read và write không được đảm bảo. CQRS giải quyết vấn đề này bằng cách tách biệt mô hình Read và Write
Mediator Pattern
Mediator Pattern là một mô hình thiết kế hành vi giúp giảm thiểu sự phụ thuộc trực tiếp giữa các đối tượng trong hệ thống. Nó hoạt động như một người trung gian để truyền tải thông tin và điều phối các tương tác giữa các đối tượng.
Tháp điều khiển tại sân bay có kiểm soát là một ví dụ về hoạt động của Mediator pattern. Các phi công của các máy bay đang cất cánh hoặc hạ cánh kết nối với tháp chứ không phải giao tiếp rõ ràng với nhau. Những khó khăn về việc ai có thể cất hoặc hạ cánh được thi hành bởi tháp điều khiển. Điều quan trọng cần lưu ý là tháp không kiểm soát toàn bộ chuyến bay. Nó tồn tại chỉ để thực thi các quy định an toàn trong lúc cất và hạ cánh.
Ở hình trên, Some Service sẽ gởi message tới Mediator, Mediator sẽ invoke service để handle message. Không có dependency trực tiếp giữa các blue component.
MediatR
Cách sử dụng MediatR
Cài đặt
dotnet add package MediatR
Đăng ký các dịch vụ MediatR:
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
Trường hợp các Query, Command, và Handler ở project khác, bạn tạo method và gọi hàm đăng ký như sau:
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
Setup ASP.NET Core với MediatR
Để bắt đầu đơn giản nhất, chúng ta sẽ lần lượt thực hiện các bước sau:
- Tạo Model class: Author và Book
- 2 model này đại diện cho 2 entity mà chúng ta sẽ khai báo trong Entity Framework
- Cài đặt package EF Core InMemory NuGet.
- Tạo DbContext class
- Khai báo Repository class: IAuthorRepository và AuthorRepository
- Khai báo Dependency Injection
- Cài đặt và khai báo Swagger
- Khai báo Controller
Model Author và Book
public class Author
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<Book> Books { get; set; }
}
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public Author Author { get; set; }
}
Install package
Install-Package Microsoft.EntityFrameworkCore.InMemory
Khai báo BookContext
public class BookContext : DbContext
{
public DbSet<Author> Authors { get; set; }
public DbSet<Book> Books { get; set; }
public BookContext(DbContextOptions<BookContext> options) : base(options)
{
}
}
Khai báo Model trả về
public class AuthorModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Khai báo Repository
public interface IAuthorRepository
{
public List<AuthorModel> GetAuthors(int pageSize, int page);
}
public class AuthorRepository: IAuthorRepository
{
private readonly BookContext _dbContext;
public AuthorRepository(BookContext dbContext)
{
_dbContext = dbContext;
}
public List<AuthorModel> GetAuthors(int pageSize, int page)
{
var list = _dbContext.Authors.Skip(pageSize * page).Take(pageSize).Select(t => new AuthorModel
{
FirstName = t.FirstName,
LastName = t.LastName,
Id = t.Id,
}).ToList();
return list;
}
}
Mình sẽ lược qua đoạn đăng ký Dependency Injection và Swagger
Ở controller, bạn viết như sau:[Route("api/[controller]")]
[ApiController]
public class AuthorsController : ControllerBase
{
private readonly IAuthorRepository _authorRepository;
public AuthorsController(IAuthorRepository authorRepository)
{
_authorRepository = authorRepository;
}
[HttpGet]
public ActionResult<List<Author>> Get(int pageSize, int pageIndex)
{
return Ok(_authorRepository.GetAuthors(pageSize, pageIndex));
}
}
Controller vẫn có dependency vào Repository. Chúng ta sẽ thêm MediatR vào và gọi Query thay vì gọi trực tiếp authorRepository
Install Package cho CqrsSample và CqrsSample.Application
dotnet add package MediatR
Thêm đoạn đăng ký sau vào Program.cs
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
Ở CqrsSample.Application, thêm class sau:
public class ApplicationServiceRegistration
{
public void Register(IServiceCollection services)
{
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
}
}
Gọi hàm đăng ký ở Program.cs
var applicationModule = new ApplicationServiceRegistration();
applicationModule.Register(builder.Services);
Ở Project CqrsSample.Application, tạo file GetAuthorQuery và GetAuthorQueryHandler
using MediatR;
namespace CqrsSample.Application.Queries;
public class GetAuthorQuery : IRequest<List<AuthorModel>>
{
public int PageSize { get; set; }
public int PageIndex { get; set; } = 10;
}
using MediatR;
namespace CqrsSample.Application.Queries.Handlers
{
public class GetAuthorQueryHandler : IRequestHandler<GetAuthorQuery, List<AuthorModel>>
{
private readonly IAuthorRepository _authorRepository;
public GetAuthorQueryHandler(IAuthorRepository authorRepository)
{
_authorRepository = authorRepository;
}
public async Task<List<AuthorModel>> Handle(GetAuthorQuery request, CancellationToken cancellationToken)
{
return _authorRepository.GetAuthors(request.PageSize, request.PageIndex);
}
}
}
Sửa file AuthorController lại như sau
[Route("api/[controller]")]
[ApiController]
public class AuthorsController : ControllerBase
{
private readonly IMediator _mediator;
public AuthorsController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<ActionResult<List<Author>>> Get(int pageSize, int pageIndex)
{
var authorRequest = new GetAuthorQuery
{
PageIndex = pageIndex,
PageSize = pageSize
};
var result = await _mediator.Send(authorRequest);
return Ok(result);
}
}
Kiểm tra kết quả ở trang Swagger. Bạn có thể làm tương tự với Command.
System.InvalidOperationException: No service for type ‘MediatR.IRequestHandler`[nameofyourclass]’ has been registered.
Trả lờiXóaSolution
This could mean that your DI container still cannot find the handler. For instance because it is in another project.
The trick here is simply to add ‘Containing’ to RegisterServicesFromAssembly
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining(typeof(MoneyTransferHandler)));