Implementacja w ASP.NET Core WebAPI 5.0
W pierwszym kroku tworzymy nową aplikację opartą o szablon zgodny z ASP.NET Core WebAPI 5.0. Następnie dodajemy paczki dla MediatR:
Nie możemy również zapomnieć o paczkach związanych z EntityFramework ponieważ wykorzystamy podejście code-first do stworzenia bazy danych:
Wraz z poprawną instalacją paczek możemy przejść do konfiguracji projektu – przechodzimy do pliku Startup.cs dodając w metodzie ConfigureService usługę mediatora:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "CQRSwithMediatR", Version = "v1" });
});
services.AddMediatR(Assembly.GetExecutingAssembly());
}
Kolejnym krokiem jest dodanie do naszego projektu folderu Models i pierwszej klasy:
namespace CQRSwithMediatR.Models
{
public class Book
{
public int Id { get; set; }
public string Name { get; set; }
public string Author { get; set; }
public string Description { get; set; }
}
}
Następnie tworzymy folder Context w którym tworzymy interfejs IApplicationContext:
using CQRSwithMediatR.Models;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace CQRSwithMediatR.Context
{
public interface IApplicationContext
{
DbSet<Book> Books { get;set; }
Task<int> SaveChangesAsync();
}
}
oraz ApplicationContext dla modelu Book. Wykorzystamy teraz podejście code-first w celu utworzenia bazy danej z tabelą na bazie przygotowanego modelu (o tym za chwilkę):
using CQRSwithMediatR.Models;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace CQRSwithMediatR.Context
{
public class ApplicationContext : DbContext, IApplicationContext
{
public DbSet<Book> Books { get; set; }
public ApplicationContext(DbContextOptions<ApplicationContext> options): base(options)
{
}
public async Task<int> SaveChangesAsync()
{
return await base.SaveChangesAsync();
}
}
}
Jeżeli nie korzystacie z podejścia code-first lub chcecie dowiedzieć się więcej na ten temat odsyłam do artykułu, który opublikowałem jakiś czas temu: EF Core - podejście code-first
Zanim przejdziemy do wykonania migracji musimy jeszcze dokonać drobnych zmian w konfiguracji naszego projektu – brakuje nam zdefiniowanego connection stringa. W tym celu dokonamy modyfikacji naszej klasy kontekstowej:
using CQRSwithMediatR.Models;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
namespace CQRSwithMediatR.Context
{
public class ApplicationContext : DbContext, IApplicationContext
{
public DbSet<Book> Books { get; set; }
public ApplicationContext(DbContextOptions<ApplicationContext> options): base(options)
{
}
// W naszym przykładowym API trzymamy się konwencji
// ze zdefiniowaniem connection-string w metodzie OnConfiguring
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer(@"Server=DESKTOP-8LKDKGM;database=CQRSwithMediator;Integrated Security=true");
}
}
public async Task<int> SaveChangesAsync()
{
return await base.SaveChangesAsync();
}
}
}
Jesteśmy gotowi do utworzenia migracji i zaktualizowania schematu bazy danych. Pamiętacie jednak o instalacji paczki EntityFramework.Core.Tools o której wspomniałem we wpisie EF Core - instalacja:
add-migration initial
update-database
Musicie również dokonać drobnych zmian w konfiguracji projektu:
public void ConfigureServices(IServiceCollection services)
{
// Rejestrujemy klasę kontekstową
services.AddDbContext<ApplicationContext>();
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "CQRSwithMediatR", Version = "v1" });
});
// Spójrzcie na implementację Poleceń/Zapytań żeby rozwiać wszelkie wątpliwości
services.AddScoped<IApplicationContext>(provider => provider.GetService<ApplicationContext>());
services.AddMediatR(Assembly.GetExecutingAssembly());
}
O sposobach implementacji CQRS możemy przeczytać wiele artykułów. Jednym ze sposób jest przygotowanie dwóch osobych API, jedno dla poleceń a drugie dla zapytań. Na potrzebny tego artykułu wszystko tworzymy w ramach jednego projektu a rozdzielenie nastąpi na poziomie struktury folderów i odpowiednich klas. Tworzymy zatem folder Features(nie wiem czy to najlepsza nazwa) w którym utworzymy dwa foldery dla klas zdefiniowanych w ramach poleceń/zapytań:
Dla każdej z klas musimy przygotować prostą implementację poszczególnych metod. Dodatkowo, w każdej z nich użyjemy interfejsów IRequest oraz IRequestHandler z biblioteki mediatora w celu stworzenia luźno powiązanego kodu. Zaczynamy od klasy GetAllBooksQuery:
using CQRSwithMediatR.Context;
using CQRSwithMediatR.Models;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace CQRSwithMediatR.Features.Queries
{
public class GetAllBooksQuery : IRequest<IEnumerable<Book>>
{
public class GetAllBooksQueryHandler: IRequestHandler<GetAllBooksQuery, IEnumerable<Book>>
{
private readonly IApplicationContext _context;
public GetAllBooksQueryHandler(IApplicationContext context)
{
_context = context;
}
public async Task<IEnumerable<Book>> Handle(GetAllBooksQuery request, CancellationToken cancelationToken)
{
var bookList = await _context.Books.ToListAsync();
if(bookList == null)
{
return null;
}
return bookList.AsReadOnly();
}
}
}
}
Implementacja dla klasy GetBookByIdQuery:
using CQRSwithMediatR.Context;
using CQRSwithMediatR.Models;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace CQRSwithMediatR.Features.Queries
{
public class GetBookByIdQuery : IRequest<Book>
{
public int Id { get; set; }
public class GetBookByIdQueryHandler : IRequestHandler<GetBookByIdQuery, Book>
{
private readonly IApplicationContext _context;
public GetBookByIdQueryHandler(IApplicationContext context)
{
_context = context;
}
public Task<Book> Handle(GetBookByIdQuery request, CancellationToken cancellationToken)
{
var book = _context.Books.Where(a => a.Id == request.Id).FirstOrDefaultAsync();
if(book == null)
{
return null;
}
return book;
}
}
}
}
Implementacja dla klasy CreateBookCommand:
using CQRSwithMediatR.Context;
using CQRSwithMediatR.Models;
using MediatR;
using System.Threading;
using System.Threading.Tasks;
namespace CQRSwithMediatR.Features.Commands
{
public class CreateBookCommand : IRequest<int>
{
public int Id { get; set; }
public string Name { get; set; }
public string Author { get; set; }
public string Description { get; set; }
public class CreateBookCommandHandler : IRequestHandler<CreateBookCommand, int>
{
private readonly IApplicationContext _context;
public CreateBookCommandHandler(IApplicationContext context)
{
_context = context;
}
public async Task<int> Handle(CreateBookCommand request, CancellationToken cancellationToken)
{
var book = new Book();
book.Author = request.Author;
book.Description = request.Description;
book.Name = request.Name;
_context.Books.Add(book);
int result = await _context.SaveChangesAsync();
return result;
}
}
}
}
Implementacja dla klasy UpdateBookCommand:
using CQRSwithMediatR.Context;
using MediatR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace CQRSwithMediatR.Features.Commands
{
public class UpdateBookCommand : IRequest<int>
{
public int Id { get; set; }
public string Name { get; set; }
public string Author { get; set; }
public string Description { get; set; }
public class UpdateBookCommandHandler : IRequestHandler<UpdateBookCommand, int>
{
private readonly IApplicationContext _context;
public UpdateBookCommandHandler(IApplicationContext context)
{
_context = context;
}
public async Task<int> Handle(UpdateBookCommand request, CancellationToken cancellationToken)
{
var book = _context.Books.Where(a => a.Id == request.Id).FirstOrDefault();
if (book == null)
{
return default;
}
else
{
book.Author = request.Author;
book.Description = request.Description;
book.Name = request.Name;
int result = await _context.SaveChangesAsync();
return result;
}
}
}
}
}
Implementacja dla klasy DeleteBookCommand:
using CQRSwithMediatR.Context;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace CQRSwithMediatR.Features.Commands
{
public class DeleteBookCommand : IRequest<int>
{
public int Id { get; set; }
public class DeleteBookCommandHandler : IRequestHandler<DeleteBookCommand, int>
{
private readonly IApplicationContext _context;
public DeleteBookCommandHandler(IApplicationContext context)
{
_context = context;
}
public async Task<int> Handle(DeleteBookCommand request, CancellationToken cancellationToken)
{
var book = await _context.Books.Where(a => a.Id == request.Id).FirstOrDefaultAsync();
if(book == null)
{
return default;
}
_context.Books.Remove(book);
int result = await _context.SaveChangesAsync();
return result;
}
}
}
}
Żmudna część za nami – przygotowaliśmy implementacje dla zapytań i poleceń. Teraz możemy zobaczyć jak w praktyce wygląda użycie mediatora tworząc kontroler w którym mediator będzie odpowiedzialny za przekazanie obsługi z metody API do konkretnego Handlera. Tworzymy nowy kontroler BookControler, który może przyjąć poniższą implementację:
using CQRSwithMediatR.Features.Commands;
using CQRSwithMediatR.Features.Queries;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;
namespace CQRSwithMediatR.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BookController : ControllerBase
{
private readonly IMediator _mediator;
public BookController(IMediator mediator)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
}
// GET: api/<BookController>
[HttpGet]
public async Task<IActionResult> Get()
{
return Ok(await _mediator.Send(new GetAllBooksQuery()));
}
// GET:api/<BookController>/1
[HttpGet("{id}")]
public async Task<IActionResult> GetBookById(int id)
{
return Ok(await _mediator.Send(new GetBookByIdQuery() { Id = id }));
}
// POST: api/<BookController>/1
[HttpPost]
public async Task<IActionResult> UpdateBook(CreateBookCommand command)
{
return Ok(await _mediator.Send(command));
}
// PUT: api/<BookController>/1
[HttpPut("{id}")]
public async Task<IActionResult> UpdateBookByUd(int id, UpdateBookCommand command)
{
if (id != command.Id)
{
return BadRequest();
}
return Ok(await _mediator.Send(command));
}
// DELETE: api/<BookController>/1
[HttpDelete]
public async Task<IActionResult> Delete(int id)
{
return Ok(await _mediator.Send(new DeleteBookCommand { Id = id }));
}
}
}
Wszystko jest już gotowe. Możecie teraz dodać kilka elementów korzystając ze Swaggera a następnie wywołać metodę GET celem sprawdzenia czy implementacja jest poprawna a rekordy zostały dodane do bazy danych: