logo MSJO.kr

CQRS using C# and MediatR

2021-04-15
MsJ

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);
}
추천강좌
Reference
  1. docs.microsoft.com, “Command and Query Responsibility Segregation (CQRS) pattern”
  2. Martin Fowler, “BoundedContext”
  3. Jooho Son, “(번역)마틴 파울러 CQRS 포스팅”
  4. Jonathan Williams, “CQRS using C# and MediatR”
  5. jonathanjameswilliams26, “CQRSInDotnetCore”

Prεv  
Content
Search     RSS Feed     BY-NC-ND