- Domain, Todo.cs
- Database, Repository.cs
- Queries, GetTodoByID.cs
- Commands, AddTodo.cs
- TodoController.cs
- Startup.cs
- 추천강좌
- Reference
CQRS(Command and Query Responsibility Segregation, 명령과 쿼리의 역할 분리) 패턴은 데이터 저장소에 대한 읽기 및 업데이트 작업을 분리하여 구현하는 것으로 이렇게 하면 성능, 확장성 및 보안을 최대화할 수 있는 장점이 있다1.
CQRS는 정보를 업데이트할 때와 조회할 때 다른 모델을 사용하는 것이 핵심이다. 다만, 일부 경우에는 이점이 있지만, 대부분의 경우에는 CQRS를 적용하면 복잡성이 높아지는 위험성이 있다. CQRS는 시스템의 Bounded Context2에서만 사용돼야 하고, 시스템 전체에서 사용해서는 안 된다. 이러한 사고방식은 각 Bounded Context는 개별적으로 모델링을 해야 한다는 의미다3.
아래의 예제는 닷넷 API 프로젝트에 MediatR 패키지를 사용하여 CQRS를 구현한 간단한 예제이다. ‘Jonathan Williams’의 강좌4 를 참고하였으며 자세한 전체 예제는 GitHub(CQRSInDotnetCore)5에서 볼 수 있다.
Domain, Todo.cs
namespace CQRSExam.Domain
{
public class Todo
{
public int Id { get; init; }
public string Name { get; init; }
public bool Completed { get; init; }
}
}
Database, Repository.cs
using CQRSExam.Domain;
using System.Collections.Generic;
namespace CQRSExam.Database
{
public class Repository
{
public List<Todo> Todos { get; } = new()
{
new Todo{Id = 1, Name = "Todo List 1", Completed = false },
new Todo{Id = 2, Name = "Todo List 2", Completed = true },
new Todo{Id = 3, Name = "Todo List 3", Completed = false },
new Todo{Id = 4, Name = "Todo List 4", Completed = true },
new Todo{Id = 5, Name = "Todo List 5", Completed = false },
};
}
}
Queries, GetTodoByID.cs
using CQRSExam.Database;
using MediatR;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace CQRSExam.Queries
{
public static class GetTodoByID
{
// Query, Command : Execute
public record Query(int Id) : IRequest<Response>;
// Handler : Logic
public class Handler : IRequestHandler<Query, Response>
{
private readonly Repository _repository;
public Handler(Repository repository)
{
_repository = repository;
}
public async Task<Response> Handle(Query request, CancellationToken cancellationToken)
{
var todo = _repository.Todos.FirstOrDefault(x => x.Id == request.Id);
return await Task.FromResult(todo == null ? null : new Response(todo.Id, todo.Name, todo.Completed));
}
}
// Response : Return
public record Response(int Id, string Name, bool Completed);
}
}
Commands, AddTodo.cs
using System.Threading;
using System.Threading.Tasks;
using CQRSExam.Database;
using CQRSExam.Domain;
using MediatR;
namespace CQRSExam.Commands
{
public static class AddTodo
{
// Command
public record Command(string Name) : IRequest<int>;
// Handler
public class Handler : IRequestHandler<Command, int>
{
private readonly Repository _repository;
public Handler(Repository repository)
{
_repository = repository;
}
public async Task<int> Handle(Command request, CancellationToken cancellationToken)
{
_repository.Todos.Add(new Todo {Id = 10, Name = request.Name});
return await Task.FromResult(10);
}
}
}
}
TodoController.cs
using CQRSExam.Commands;
using CQRSExam.Queries;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
namespace CQRSExam.Controllers
{
[ApiController]
public class TodoController : ControllerBase
{
private readonly IMediator _mediator;
public TodoController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet("/{id:int}")]
public async Task<IActionResult> GetTodoById(int id)
{
var response = await _mediator.Send(new GetTodoByID.Query(id));
return response == null ? NotFound() : Ok(response);
}
[HttpPost("")]
public async Task<IActionResult> AddTodo(AddTodo.Command command) => Ok(await _mediator.Send(command));
}
}
Startup.cs
public static void ConfigureServices(IServiceCollection services)
{
// 추가
services.AddSingleton<Repository>();
services.AddMediatR(typeof(Startup).Assembly);
}
추천강좌
- Kilt and Code, “Using MediatR Request Handlers in ASP.NET Core to Decouple Code”
- Intro to MediatR - Implementing CQRS and Mediator Patterns
- asp.net core - MediatR (CQRS) Tutorial & Tips