오늘날 개발자는 프로젝트의 아키텍처를 선택할 때 많은 선택권을 가지고 있습니다. 옵션 중 하나는 개발자가 유지 관리 및 확장이 용이 한 깨끗한 아키텍처를 구축하는 데 도움이되는 CQRS라는 패턴을 선택하는 것입니다. 이 자습서에서는 CQRS 패턴에 대한 개요를 제공하고 ASP.NET Core 프로젝트에서 사용하는 방법을 이해하는 데 도움이됩니다.
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 패턴을 구현하는 데 몇 가지 문제가 있습니다.
Added Complexity(복잡성 추가) – CQRS의 기본 아이디어는 간단하지만 더 큰 시스템에서는 읽기 작업도 동시에 데이터를 업데이트해야하는 시나리오에서 복잡성이 증가하는 경우가 있습니다. 이벤트 소싱과 같은 개념도 도입하면 복잡성도 증가했습니다.
Eventual consistency(최종 일관성) – 읽기 용으로 별도의 데이터베이스를 사용하고 쓰기 용으로 별도의 데이터베이스를 사용하는 경우 읽기 데이터 동기화가 문제가 됩니다. 우리는 오래된 데이터를 읽지 않도록 해야합니다.
이제 CQRS 패턴에 대한 기본적인 이해를 마쳤으므로 실제 ASP.NET Core 5 애플리케이션을 빌드하여이 패턴의 실제 구현에 대해 자세히 알아 보겠습니다. 간단하게하기 위해 애플리케이션의 다른 레이어를 분리하기 위해 여러 프로젝트를 만들지 않을 것입니다. 곧 ASP.NET Core 프로젝트에서 Clean (Onion) 아키텍처를 구현하는 방법에 대한 전체 기사를 작성할 것입니다.
Visual Studio 2019에서 새 ASP.NET Core 5 MVC 웹 애플리케이션을 만듭니다. NuGet 패키지 관리자를 사용하여 프로젝트에 다음 패키지를 설치합니다.
처음 세 개의 패키지는 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 controllers 및 single 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": "*"
}
이 게시물에서는 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 구현을 시작하기 전에 모든 Commands 및 Queries 를 구성하는 방법을 결정해야합니다. 대규모 프로젝트에서는 수백 개의 명령과 쿼리가 서로 다른 작업을 수행 할 수 있으며 제대로 구성하지 않을 경우 이를 발견하고 유지 관리하는 것은 시니어 개발자에게도 악몽이 될 수 있기 때문에 중요합니다.
내가 좋아하는 접근 방식 중 하나는 기능별 폴더를 만드는 것입니다. 하나의 기능과 관련된 모든 CQRS Commands , Queries, 처리기, 유효성 검사기를 하나의 폴더에 그룹화 할 수 있습니다. 예를 들어 Player 와 관련된 모든 Commands 및 Queries 를 생성하기 위해 Players 폴더가 있고 해당 폴더 안에 Commands 및 Queries에 대한 별도의 폴더가 있을 수 있습니다 .
이 섹션에서는 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);
}
}
}
이 섹션에서는 Commands 폴더 내에 다음 세 가지 명령을 만듭니다 .
읽기 : 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 를 업데이트하기 위해서 PlayerService 의 UpdatePlayer 메소드를 마지막으로 호출합니다.
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);
}
}
}
위의 모든 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 가 깔끔하고 최소한의 코드가 있음을 알 수 있습니다.
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 목록이 표시됩니다.
단일 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 정보를 수집하는 양식을 페이지에 표시합니다.
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 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 정보 데이터베이스가 저장됩니다.
삭제 조치 방법은 페이지를 필요로하지 않습니다. 플레이어의 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
Entity Framework Core 와 ASP.NET Core 를 활용한 cursor paging 사용해 보기 (0) | 2021.10.14 |
---|---|
Action Filters 를 사용하여 Secrets 을 ASP.NET Core MVC Action Arguments 로 Decrypt 하기 (0) | 2021.07.09 |
asp.net core 3.1 을 통해 aws 의 Lambda 서비스를 배포하여 실행 (0) | 2021.05.28 |
ASP.NET Core에서 HTTP Request Cancel 을 위한 CancellationToken 사용법 (1) | 2021.03.30 |
ASP.NET Core 5 의 SESSION Middleware 사용 및 가져오는 방법 (0) | 2021.01.27 |