심재운 블로그

반응형

 

 

오늘날 개발자는 프로젝트의 아키텍처를 선택할 때 많은 선택권을 가지고 있습니다. 옵션 중 하나는 개발자가 유지 관리 및 확장이 용이 한 깨끗한 아키텍처를 구축하는 데 도움이되는 CQRS라는 패턴을 선택하는 것입니다. 이 자습서에서는 CQRS 패턴에 대한 개요를 제공하고 ASP.NET Core 프로젝트에서 사용하는 방법을 이해하는 데 도움이됩니다.

 

 

CQRS 패턴이란 무엇입니까?

CQRS (Command Query Responsibility Segregation)는  Bertrand Meyer의 Object-Oriented Software Construction 책에 처음 설명되어  있습니다. 애플리케이션의 읽기 및 쓰기 작업을 구분하는 아키텍처 패턴입니다. 

읽기 작업을 Queries 라고 하고 쓰기 작업을 Commands 이라고 합니다. 

 

Queries  – 데이터를 반환하지만 응용 프로그램 상태를 변경하지 않는 작업입니다.

Commands – 응용 프로그램 상태를 변경하고 데이터를 반환하지 않는 작업입니다. 애플리케이션 내에서 부작용이있는 방법입니다. 

 

 

CQRS 패턴은 단일 책임 원칙(single responsibility principle)의 훌륭한 expression 입니다. 실제 응용 프로그램에서는 읽기 작업에 대한 요구 사항이 일반적으로 쓰기 작업과 다르기 때문에 Queries 및 Commands 에 대한 별도의 모델이 있어야한다고 명시합니다. 별도의 모델을 사용하는 경우 다른 작업을 방해 할 염려없이 복잡한 시나리오를 처리 할 수 ​​있습니다. 이러한 분리 없이는 state, Commands 및 Queries 로 가득 차 있고 시간이 지남에 따라 유지 관리가 더 어려운 domain models 로 쉽게 끝날 수 있습니다.

 

CQRS 패턴의 장점

CQRS 패턴은 다음과 같은 이점을 제공합니다.

  • Separation of Concern(우려 사항 분리) – 읽기 및 쓰기 작업을위한 별도의 모델이있어 유연성을 제공 할뿐만 아니라 모델을 단순하고 유지 관리하기 쉽습니다. 일반적으로 쓰기 모델에는 대부분의 복잡한 비즈니스 로직이 있지만 읽기 모델은 일반적으로 간단합니다.
  • Better Scalability(확장성 향상) – 읽기 작업은 쓰기보다 자주 발생하므로 쿼리를 명령과 분리하여 유지하면 응용 프로그램의 확장 성이 높아집니다. 읽기 및 쓰기 모델은 두 개의 서로 다른 개발자 또는 팀이 어떤 것도 망칠 염려없이 독립적으로 확장 할 수 있습니다.
  • Better performance (더 나은 성능) – 읽기 작업을 위해 별도의 데이터베이스 또는 빠른 캐시 (예 : Redis)를 사용하여 애플리케이션 성능을 크게 향상시킬 수 있습니다.
  • Optimized Data Models(최적화 된 데이터 모델) – 읽기 모델은 queries 에 최적화 된 스키마 또는 미리 계산 된 데이터 소스를 사용할 수 있습니다. 마찬가지로 쓰기 모델은 데이터 업데이트에 최적화 된 스키마를 사용할 수 있습니다.

 

CQRS 패턴의 단점

다음과 같은 CQRS 패턴을 구현하는 데 몇 가지 문제가 있습니다.

 

Added Complexity(복잡성 추가) – CQRS의 기본 아이디어는 간단하지만 더 큰 시스템에서는 읽기 작업도 동시에 데이터를 업데이트해야하는 시나리오에서 복잡성이 증가하는 경우가 있습니다. 이벤트 소싱과 같은 개념도 도입하면 복잡성도 증가했습니다.

Eventual consistency(최종 일관성) – 읽기 용으로 별도의 데이터베이스를 사용하고 쓰기 용으로 별도의 데이터베이스를 사용하는 경우 읽기 데이터 동기화가 문제가 됩니다. 우리는 오래된 데이터를 읽지 않도록 해야합니다.

 

ASP.NET Core 5에서 CQRS 패턴 시작하기

이제 CQRS 패턴에 대한 기본적인 이해를 마쳤으므로 실제 ASP.NET Core 5 애플리케이션을 빌드하여이 패턴의 실제 구현에 대해 자세히 알아 보겠습니다. 간단하게하기 위해 애플리케이션의 다른 레이어를 분리하기 위해 여러 프로젝트를 만들지 않을 것입니다. 곧 ASP.NET Core 프로젝트에서 Clean (Onion) 아키텍처를 구현하는 방법에 대한 전체 기사를 작성할 것입니다.

 

 

프로젝트 설정

Visual Studio 2019에서 새 ASP.NET Core 5 MVC 웹 애플리케이션을 만듭니다. NuGet 패키지 관리자를 사용하여 프로젝트에 다음 패키지를 설치합니다.

  1. Microsoft.EntityFrameworkCore.SqlServer
  2. Microsoft.EntityFrameworkCore.Tools
  3. Microsoft.EntityFrameworkCore.Design
  4. MediatR
  5. MediatR.Extensions.Microsoft.DependencyInjection

처음 세 개의 패키지는 Entity Framework Core (Database First) 접근 방식 을 사용하는 데 필요 하며 ASP.NET Core 프로젝트에서 Entity Framework Core를 사용하는 방법에 대해 자세히 알아 보려면 EF Core를 사용하여 ASP.NET Core에서  (Database First) 및 EF Core를 사용한 ASP.NET Core의 Data Access (Code First)  를 활용하여, 게시물 Data Access 를 읽을 수 있습니다.

 

마지막 두 패키지는 ASP.NET Core에서 MediatR 라이브러리를 사용하는 데 필요합니다 . MediatR  은 종속성이없는 Mediator 패턴의 단순하고 프로세스 내 구현이있는 가장 인기있는 라이브러리 중 하나입니다. mediator pattern(중재자 패턴) 은 애플리케이션 계층의 CQRS와 완벽하게 맞으며 Mediator와 CQRS를 결합하면 애플리케이션에 최고의 읽기-쓰기 성능, thing controllerssingle responsibility(단일 책임)을 가진 handlers 를 제공 할 수 있습니다.

 

 

읽기 :   Dapper ORM을 사용하여 ASP.NET Core에서 JQuery DataTables 페이징, 정렬 및 필터링

 

중재자 패턴에 대해 자세히 알아 보려면 ASP.NET Core의 중재자 디자인 패턴( Mediator Design Pattern in ASP.NET Core.) 게시물을 읽을 수 있습니다 .

또한 프로젝트 appsettings.json 파일에서 다음 데이터베이스 연결 문자열을 정의해야합니다.

 

 

appsettings.json

 

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=DB_SERVER; Database=FootballDb; Trusted_Connection=True; MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

 

 

EF Core를 사용하여 모델 생성 (데이터베이스 우선)

이 게시물에서는 Entity Framework Core (Database First) 접근 방식 과 함께 SQL Server 데이터베이스를 사용합니다 . football players 에 대한 정보가 있는  Players 테이블을 사용합니다 .

프로젝트의 패키지 관리자 콘솔을 열고 다음 Scaffold-DbContext 명령을 복사 / 붙여 넣기 하고 Enter 키를 누릅니다. 이 명령은 Models  폴더 의 데이터베이스 테이블에서 엔터티 모델 클래스를  생성 하고 Data  폴더  FootballDbContext 클래스 도 생성 합니다. 

 

Scaffold-DbContext -Connection "Server=DB_SERVER; Database= FootballDb; Trusted_Connection=True; MultipleActiveResultSets=true;" -Provider Microsoft.EntityFrameworkCore.SqlServer -OutputDir "Models" -ContextDir "Data" -Context "FootballDbContext"


또한 Startup.cs 파일 에서 FootballDbContext 를 다음과 같이 구성해야 합니다.

 

 

Startup.cs

 

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
 
    services.AddDbContext<FootballDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); 
}

 

애플리케이션 서비스 구현

CQRS Commands 및 Queries 에서 DbContext를 직접 사용할 수 있지만, Single Responsibility Principle(단일 책임 원칙)의 열렬한 팬입니다. 모든 EF Core 관련 코드를 Commands 및 Queries 를 망가트리는 대신 별도의 계층에 보관하고 싶습니다.

 

프로젝트에 Services  폴더를 생성합니다. 그리고 IPlayersService  인터페이스를 정의합니다. IPlayersService interface에 정의 된 메소드는 Players  테이블에서 수행할 기본 CRUD 를 보여주는 작업을 합니다.

 

IPlayersService

 

public interface IPlayersService
{
    Task<IEnumerable<Player>> GetPlayersList();
    Task<Player> GetPlayerById(int id);
    Task<Player> CreatePlayer(Player player);
    Task<int> UpdatePlayer(Player player);
    Task<int> DeletePlayer(Player player);
}

 

다음으로, 다음과 같은 만들  PlayersService의 클래스를 위의 구현  IPlayersService의 인터페이스를 제공합니다. .NET Core에서 사용할 수있는 종속성 주입 기능을 사용하여 PlayersService 클래스 의 생성자에  FootballDbContext 의 인스턴스를 주입합니다  . PlayersService에 정의 된 메서드  는 단순히 CRUD 작업을 구현하는 것입니다.

 

PlayersService

 

public class PlayersService : IPlayersService
{
    private readonly FootballDbContext _context;
 
    public PlayersService(FootballDbContext context)
    {
        _context = context;
    }
 
    public async Task<IEnumerable<Player>> GetPlayersList()
    {
        return await _context.Players
            .ToListAsync();
    }
 
    public async Task<Player> GetPlayerById(int id)
    {
        return await _context.Players
            .FirstOrDefaultAsync(x => x.Id == id);
    }
 
    public async Task<Player> CreatePlayer(Player player)
    {
        _context.Players.Add(player);
        await _context.SaveChangesAsync();
        return player;
    }
     
    public async Task<int> UpdatePlayer(Player player)
    {
        _context.Players.Update(player);
        return await _context.SaveChangesAsync();
    }
 
    public async Task<int> DeletePlayer(Player player)
    {
        _context.Players.Remove(player);
        return await _context.SaveChangesAsync();
    }
}

 

또한 아래 Startup.cs 파일에 서비스를 등록해야합니다.

 

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
 
    services.AddDbContext<FootballDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
 
    services.AddScoped<IPlayersService, PlayersService>(); 
}

 

CQRS 패턴을 사용하여 Commands 및 Queries 구성

CQRS 패턴을 사용하여 Commands 및 Queries 구현을 시작하기 전에 모든 Commands 및 Queries 를 구성하는 방법을 결정해야합니다. 대규모 프로젝트에서는 수백 개의 명령과 쿼리가 서로 다른 작업을 수행 할 수 있으며 제대로 구성하지 않을 경우 이를 발견하고 유지 관리하는 것은 시니어 개발자에게도 악몽이 될 수 있기 때문에 중요합니다.

 

내가 좋아하는 접근 방식 중 하나는 기능별 폴더를 만드는 것입니다. 하나의 기능과 관련된 모든 CQRS Commands , Queries, 처리기, 유효성 검사기를 하나의 폴더에 그룹화 할 수 있습니다. 예를 들어 Player 와 관련된 모든 Commands 및 Queries 를 생성하기 위해 Players 폴더가 있고 해당 폴더 안에 Commands  Queries에 대한 별도의 폴더가 있을 수 있습니다 .

 

CQRS 패턴을 사용하여 쿼리 구현

이 섹션에서는 Queries 폴더 내에 두 개의 쿼리 GetAllPlayersQuery  GetPlayerByIdQuery를 생성 합니다. 이러한 쿼리가 수행 할 작업을 이름에서 쉽게 추측 할 수 있습니다. 다음은 GetAllPlayersQuery 클래스 의 구현입니다 .

 

GetAllPlayersQuery.cs

 

public class GetAllPlayersQuery : IRequest<IEnumerable<Player>>
{
    public class GetAllPlayersQueryHandler : IRequestHandler<GetAllPlayersQuery, IEnumerable<Player>>
    {
        private readonly IPlayersService _playerService;
 
        public GetAllPlayersQueryHandler(IPlayersService playerService)
        {
            _playerService = playerService;
        }
 
        public async Task<IEnumerable<Player>> Handle(GetAllPlayersQuery query, CancellationToken cancellationToken)
        {
            return await _playerService.GetPlayersList();
        }
    }
}

 

 

IRequest <T> 인터페이스를 구현한 GetAllPlayersQuery 은  MediatR 라이브러리에서 사용할 수 있으며, 쿼리로 부터 IEnumerable<Player> 의 return 받기 원함을 명시 합니다.

 

public class GetAllPlayersQuery : IRequest<IEnumerable<Player>>

 

다음으로 IRequestHandler 인터페이스 를 구현 하는 중첩 된 쿼리 처리기 클래스 GetAllPlayersQueryHandler  정의합니다 . 이 클래스는 기본 GetAllPlayersQuery 클래스 외부에서도 정의 할 수 있지만 처리기를 쉽게 검색 할 수 있도록 중첩 클래스로 정의하는 것을 선호합니다.

 

public class GetAllPlayersQueryHandler : IRequestHandler<GetAllPlayersQuery, IEnumerable<Player>>

 

IRequest  IRequestHandler 인터페이스 에 대해 자세히 알아 보려면 ASP.NET Core에서 내 게시물 중재자 디자인 패턴을 읽어보기실 바랍니다.

 

IPlayerService 는 의존성 주입 을 통해 GetAllPlayersQueryHandler 클래스의 생성자를 통해 주입(injected ) 합니다. 그리고 players list  를 반환받을 서비스를 GetPlayersList 메소드를 통해 호출합니다.

 

public async Task<IEnumerable<Player>> Handle(GetAllPlayersQuery query, CancellationToken cancellationToken)
{
    return await _playerService.GetPlayersList();
}

 

다음 쿼리 GetPlayerByIdQuery 는 위 쿼리와 비슷하지만 이번에는 ID별로 단일 Player를 반환합니다.

 

GetPlayerByIdQuery.cs

 

public class GetPlayerByIdQuery : IRequest<Player>
{
    public int Id { get; set; }
 
    public class GetPlayerByIdQueryHandler : IRequestHandler<GetPlayerByIdQuery, Player>
    {
        private readonly IPlayersService _playerService;
 
        public GetPlayerByIdQueryHandler(IPlayersService playerService)
        {
            _playerService = playerService;
        }
 
        public async Task<Player> Handle(GetPlayerByIdQuery query, CancellationToken cancellationToken)
        {
            return await _playerService.GetPlayerById(query.Id);
        }
    }
}

 

CQRS 패턴을 사용하여 Commands 구현

이 섹션에서는 Commands 폴더 내에 다음 세 가지 명령을 만듭니다 .

 

  • CreatePlayerCommand
  • UpdatePlayerCommand
  • DeletePlayerCommand

 

읽기 : Gulp를 사용한 ASP.NET Core 번들링 및 축소

 

CreatePlayerCommand는 내부 새로운 플레이어 생성 Handle method 내부에 새로운 Player 를 생성합니다. 그런 다음 데이터베이스에서 하나의 player 를 생성하기 위해서 PlayerService  의 CreatePlayer 메소드를 호출합니다.

 

 

CreatePlayerCommand.cs

 

public class CreatePlayerCommand : IRequest<Player>
{
    public int? ShirtNo { get; set; }
    public string Name { get; set; }
    public int? Appearances { get; set; }
    public int? Goals { get; set; }
 
    public class CreatePlayerCommandHandler : IRequestHandler<CreatePlayerCommand, Player>
    {
        private readonly IPlayersService _playerService;
 
        public CreatePlayerCommandHandler(IPlayersService playerService)
        {
            _playerService = playerService;
        }
 
        public async Task<Player> Handle(CreatePlayerCommand command, CancellationToken cancellationToken)
        {
            var player = new Player()
            {
                ShirtNo = command.ShirtNo,
                Name = command.Name,
                Appearances = command.Appearances,
                Goals = command.Goals
            };
 
            return await _playerService.CreatePlayer(player);
        }
    }
}

 

UpdatePlayerCommand는 PlayerServiceclass 의 GetPlayerById 메소드를 사용하여 데이터베이스로 부터 player 가져옵니다. 그리고 Handle 메소드 안에 Player 를 업데이트 합니다. 그 다음 데이터에스에서 player 를 업데이트하기 위해서 PlayerServiceUpdatePlayer 메소드를 마지막으로 호출합니다.

 

UpdatePlayerCommand.cs

 

public class UpdatePlayerCommand : IRequest<int>
{
    public int Id { get; set; }
    public int? ShirtNo { get; set; }
    public string Name { get; set; }
    public int? Appearances { get; set; }
    public int? Goals { get; set; }
 
    public class UpdatePlayerCommandHandler : IRequestHandler<UpdatePlayerCommand, int>
    {
        private readonly IPlayersService _playerService;
 
        public UpdatePlayerCommandHandler(IPlayersService playerService)
        {
            _playerService = playerService;
        }
 
        public async Task<int> Handle(UpdatePlayerCommand command, CancellationToken cancellationToken)
        {
            var player = await _playerService.GetPlayerById(command.Id);
            if (player == null)
                return default;
 
            player.ShirtNo = command.ShirtNo;
            player.Name = command.Name;
            player.Appearances = command.Appearances;
            player.Goals = command.Goals;
 
            return await _playerService.UpdatePlayer(player);
        }
    }
}

 

PlayerServiceclass 의 GetPlayerById 메소드를 사용하여 데이터베이스로 부터 player  가져옵니다. 그 다음 Player 를 데이터베이스로 부터 삭제하기위해 PlayerService  클래스 안에 있는 DeletePlayer  메소드를 사용하여 삭제합니다.

 

DeletePlayerCommand.cs

 

public class DeletePlayerCommand : IRequest<int>
{
    public int Id { get; set; }
    public int? ShirtNo { get; set; }
    public string Name { get; set; }
    public int? Appearances { get; set; }
    public int? Goals { get; set; }
 
    public class DeletePlayerCommandHandler : IRequestHandler<DeletePlayerCommand, int>
    {
        private readonly IPlayersService _playerService;
 
        public DeletePlayerCommandHandler(IPlayersService playerService)
        {
            _playerService = playerService;
        }
 
        public async Task<int> Handle(DeletePlayerCommand command, CancellationToken cancellationToken)
        {
            var player = await _playerService.GetPlayerById(command.Id);
            if (player == null)
                return default;
 
            return await _playerService.DeletePlayer(player);
        }
    }
}

 

ASP.NET Core 에서 CQRS Commands 그리고 Queries 사용

위의 모든 commands 그리고 queries 를 사용하려면 ASP.NET Core MVC 컨트롤러 내에서 MediatR 라이브러리를 사용해야하므로 Controllers 폴더 내에 PlayerController를 만들어 보겠습니다 .  컨트롤러 생성자의 MediatR 라이브러리에서 사용할 수 있는 IMediator 인터페이스 inject (삽입) 해야합니다  .

 

PlayerController.cs

 

public class PlayerController : Controller
{
    private readonly IMediator _mediator;
 
    public PlayerController(IMediator mediator)
    {
        _mediator = mediator;
    }
}

 

또한 Startup.cs 파일의 handlers 와 함께 모든 commands 그리고 queries 을 등록해야합니다.  이 게시물의 시작 부분에서 설치  MediatR.Extensions.Microsoft.DependencyInjection 패키지 에는 어셈블리에 정의 된 모든 요청 및 RequestHandler 를 등록 할 수 있는 확장 메서드 AddMediatR(Assembly) 이 있습니다.

 

 

Startup.cs

 

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
 
    services.AddDbContext<FootballDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
 
    services.AddScoped<IPlayersService, PlayersService>();
 
    services.AddMediatR(Assembly.GetExecutingAssembly());
}

 

이제 PlayerController 에서 위의 모든 commands 그리고 queries 를 보낼 준비가 되었으며 컨트롤러에서 정의 할 action methods 가 깔끔하고 최소한의 코드가 있음을 알 수 있습니다.

 

 

Players 목록 페이지 구현

player 목록 페이지를 구현하려면 PlayerController 내부에 Index 의 action method 를 만들어야합니다 . 이 메서드는 mediator(중재자)의 도움으로 GetAllPlayersQuery  보냅니다 .

 

 

PlayerController.cs

 

public async Task<IActionResult> Index()
{
    return View(await _mediator.Send(new GetAllPlayersQuery()));
}

 

다음은 player  목록을 표시하는 Index 의 Razor 구문 페이지 의 코드입니다.

 

Index.cshtml

 

@model IEnumerable<CQRSDesignPatternDemo.Models.Player>
 
@{
    ViewData["Title"] = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
 
<div class="row">
    <div class="col">
        <h1>Players</h1>
    </div>
    <div class="col text-right">
        <a asp-action="Create" class="btn btn-success">Create New</a>
    </div>
</div>
 
<br/>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Id)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ShirtNo)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Appearances)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Goals)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
    @foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Id)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ShirtNo)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Appearances)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Goals)
            </td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { id = item.Id }, new { @class = "btn btn-primary" })  
                @Html.ActionLink("Details", "Details", new { id=item.Id }, new { @class = "btn btn-secondary" })  
                @Html.ActionLink("Delete", "Delete", new { id=item.Id }, new { @class = "btn btn-danger" })
            </td>
        </tr>
    }
    </tbody>
</table>

 

프로젝트를 실행하고 Player/Index 페이지로 이동하면 아래 스크린 샷과 같이 페이지에 표시된 Player 목록이 표시됩니다.

 

 

 

Players Detail 정보 페이지 구현

단일 player 의 세부 정보를 보려면 다음 Details  작업 메서드를 구현하십시오  . 이 메서드는 mediator 의 도움으로 GetPlayerByIdQuery  보냅니다 .

 

PlayerController.cs

 

public async Task<IActionResult> Details(int id)
{
    return View(await _mediator.Send(new GetPlayerByIdQuery() { Id = id }));
}

 

다음은 단일 플레이어의 세부 정보를 표시하는 세부 정보 면도기보기 페이지 의 코드 입니다.

 

READ ALSO:  Logging in ASP.NET Core 5 using Serilog

 

Details.cshtml

 

@model CQRSDesignPatternDemo.Models.Player
 
@{
    ViewData["Title"] = "Details";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
 
<h1>@Model.Name</h1>
<hr />
<div>
   
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Id)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Id)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Name)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Name)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.ShirtNo)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.ShirtNo)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Appearances)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Appearances)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Goals)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Goals)
        </dd>
    </dl>
</div>
<div>
    @Html.ActionLink("Edit", "Edit", new { id = Model.Id }, new { @class = "btn btn-primary" }) 
    <a asp-action="Index" class="btn btn-secondary">Back to List</a>
</div>

프로젝트를 실행하고 목록 페이지에서 플레이어  세부 정보 버튼을 클릭하면 아래 스크린 샷과 같이 페이지에서 선택한 플레이어 세부 정보를 볼 수 있습니다.

 

 

player  생성 페이지 구현

player  생성 페이지를 구현하려면 두 가지 작업 방법이 필요합니다. 첫 번째 작업 방법은 사용자로부터 player  정보를 수집하는 양식을 페이지에 표시합니다.

 

PlayerController.cs

 

public IActionResult Create()
{
    return View();
}

 

두 번째 Create 메서드는 HTTP POST 요청을 처리하고 데이터베이스에 플레이어 세부 정보를 저장합니다. 이 메서드는 중개자의 도움으로 CreatePlayerCommand  보냅니다 .

 

PlayerController.cs

 

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(CreatePlayerCommand command)
{
    try
    {
        if (ModelState.IsValid)
        {
            await _mediator.Send(command);
            return RedirectToAction(nameof(Index));
        }
    }
    catch (Exception ex)
    {
        ModelState.AddModelError("", "Unable to save changes.");
    }
    return View(command);
}

다음은 사용자가 플레이어 세부 정보를 입력 할 수있는 양식을 사용자에게 표시하는 Create razor 뷰 페이지 의 코드입니다 .

 

Create.cshtml

 

@model CQRSDesignPatternDemo.Models.Player
 
@{
    ViewData["Title"] = "Create";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
 
<h1>Create Player</h1>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create" method="post">
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ShirtNo" class="control-label"></label>
                <input asp-for="ShirtNo" class="form-control" />
                <span asp-validation-for="ShirtNo" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Appearances" class="control-label"></label>
                <input asp-for="Appearances" class="form-control" />
                <span asp-validation-for="Appearances" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Goals" class="control-label"></label>
                <input asp-for="Goals" class="form-control" />
                <span asp-validation-for="Goals" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
                <a asp-action="Index" class="btn btn-secondary">Back to List</a>
            </div>
        </form>
    </div>
</div>

 

프로젝트를 실행하고 Create New 버튼을 클릭 하면 다음 Player 만들기 form 이 표시됩니다. Player 에 대한 정보를 추가하고 Create 버튼을 클릭 합니다.

 

 

아래 스크린 샷과 같이 Player  목록 끝에 Player  가 추가 된 것을 볼 수 있습니다.

Player 편집 페이지 구현

Player  편집 페이지를 구현하려면 다시 두 가지 작업 방법이 필요합니다. 첫 번째 편집 작업 방법은 데이터베이스에서 선택한 플레이어 세부 정보를 가져 와서 볼 Player Model 을 보내 페이지에 편집 form 을 표시합니다.

 

PlayerController.cs

 

public async Task<IActionResult> Edit(int id)
{
    return View(await _mediator.Send(new GetPlayerByIdQuery() { Id = id }));
}

 

 

두 번째 Edit 메서드는 HTTP POST 요청을 처리하고 데이터베이스의 Player  세부 정보를 업데이트합니다. 이 메소드는 Mediator 의 도움으로 UpdatePlayerCommand  보냅니다 .

 

PlayerController.cs

 

 

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, UpdatePlayerCommand command)
{
    if (id != command.Id)
    {
        return BadRequest();
    }
 
    try
    {
        if (ModelState.IsValid)
        {
            await _mediator.Send(command);
            return RedirectToAction(nameof(Index));
        }
    }
    catch (Exception ex)
    {
        ModelState.AddModelError("", "Unable to save changes.");
    }
    return View(command);
}

 

다음은 Player  세부 정보가 포함 된 편집 양식을 표시하는 Edit.cshtml 의 Razor 구문 edit 페이지 의 코드입니다 .

 

@model CQRSDesignPatternDemo.Models.Player
 
@{
    ViewData["Title"] = "Edit";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
 
<h1>Edit Player</h1>
 
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Edit" method="post">
            <input asp-for="Id" class="form-control" type="hidden" />
            <div class="form-group">
                <label asp-for="Name" class="control-label"></label>
                <input asp-for="Name" class="form-control" />
                <span asp-validation-for="Name" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ShirtNo" class="control-label"></label>
                <input asp-for="ShirtNo" class="form-control" />
                <span asp-validation-for="ShirtNo" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Appearances" class="control-label"></label>
                <input asp-for="Appearances" class="form-control" />
                <span asp-validation-for="Appearances" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Goals" class="control-label"></label>
                <input asp-for="Goals" class="form-control" />
                <span asp-validation-for="Goals" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
                <a asp-action="Index" class="btn btn-secondary">Back to List</a>
            </div>
        </form>
    </div>
</div>

 

 

프로젝트를 실행하고 목록 페이지의 Player 에 대해 편집 버튼을 클릭하면 아래 스크린 샷과 같이 페이지에서 선택한 Player 세부 정보를 볼 수 있습니다. 저장 버튼을 클릭하면 Player 정보 데이터베이스가 저장됩니다.

 

Player 삭제 메소드 구현

삭제 조치 방법은 페이지를 필요로하지 않습니다. 플레이어의 ID와 함께 DeletePlayerCommand  전송 하고 데이터베이스에서 플레이어가 삭제 된 것을 볼 수 있습니다.

 

 

PlayerController.cs

 

[HttpGet]
public async Task<IActionResult> Delete(int id)
{
    try
    {
        await _mediator.Send(new DeletePlayerCommand() { Id = id });
    }
    catch (Exception ex)
    {
        ModelState.AddModelError("", "Unable to delete. ");
    }
 
    return RedirectToAction(nameof(Index));
}

 

 

요약

이 게시물에서는 CQRS 패턴의 기본 사항을 다루고 CQRS 패턴의 장단점에 대해 배웠습니다. 또한 Mediator 패턴과 함께 CQRS 패턴을 사용하여 확장 성과 유지 관리가 용이 ​​한 코드를 구현하는 방법을 배웠습니다. 

ASP.NET Core 5 MVC 웹 애플리케이션에서 CQRS Commands 및 Query 를 사용하여 데이터베이스 CRUD 작업을 수행하는 방법을 배웠습니다. 

 

 

번역사이트

 

https://www.ezzylearning.net/tutorial/implement-cqrs-pattern-in-asp-net-core-5

 

Implement CQRS Pattern in ASP.NET Core 5

Learn about CQRS Pattern, Pros and Cons of CQRS Pattern. Learn how to implement CRUD operations using CQRS and Mediator pattern in ASP.NET Core 5

www.ezzylearning.net

 

반응형

댓글

비밀글모드

  1. 주니어개발자
    안녕하세요 글 잘읽어 보았습니다. CQRS 패턴에 대해 공부중이었는데 많은 도움이되었어요. 다름이 아니라 글내용중 여쭤보고싶은 부분이 있어 댓글을 남깁니다.
    Players 테이블은 ID에만 PK 제약조건 말고 다른 조건은 없어도 되나요?
    제가 MVC 가 아닌 API로 구현하다보니 작성하신 내용 그대로 코딩하진 않았지만 내용 어디에도 ID를 넣거나 하는 부분이 없더라구요ㅜㅠㅠ 저는 ID에 자동증가 설정하니 현재는 문제없이 됩니다만 제가 놓친 부분이 있을까요?
    2021.11.09 09:44
    • Id 는 int 형 not null 자동증가 1 입니다. ;) 그리고 클러스터 인덱스 입니다
      2021.11.09 13:37 신고