git clone https://github.com/toreaurstadboss/CqrsDemoWebApi.git
The following Nuget packages are relevant for EF Core and MediatR.
<PackageReference Include="Mediatr" Version="10.0.1" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
The Wiki of MediatR contains some example code here:
https://github.com/jbogard/MediatR/wiki
We will first look into the Request /Reponse messages, dispatched to a single handler. These are either comands or queries in this sample.
Let us first consider a query that returns a list of books. Here is how it is implemented :
using CqrsDemoWebApi.Database;
using MediatR;
using Microsoft.EntityFrameworkCore;
namespace CqrsDemoWebApi.Features.Books.Query
{
public class GetBooks
{
public class Query : IRequest<IEnumerable<Book>>
{
}
public class QueryHandler : IRequestHandler<Query, IEnumerable<Book>>
{
private readonly BooksDbContext _db;
public QueryHandler(BooksDbContext db)
{
_db = db;
}
public async Task<IEnumerable<Book>> Handle(Query request, CancellationToken cancellationToken)
{
return await _db.Books.ToListAsync(cancellationToken);
}
}
}
}
We see that we are supposed to handle the query async since we must return a Task of TResponse (although we could use Task.FromResult here if we wanted to return it synchronously..) where the TRequest is of type IRequest
Program.cs (.NET 6 Web Api project)
using CqrsDemoWebApi.Database;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddMediatR(Assembly.GetExecutingAssembly()); //adding MediatR support here.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var connectionString = new ConnectionString(builder.Configuration.GetConnectionString("CqrsDemoBooksDb"));
builder.Services.AddSingleton(connectionString);
builder.Services.AddDbContext<BooksDbContext>(options =>
options.UseSqlServer(connectionString.Value));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Nothing special here, just basic setup of a standard web api in Net 6 here, using the AddMediatR method pointing to the assembly where our contracts reside (commands and queries and their respective handlers).
The BookController web api controller looks like this :
using CqrsDemoWebApi.Database;
using CqrsDemoWebApi.Features.Books;
using CqrsDemoWebApi.Features.Books.Command;
using CqrsDemoWebApi.Features.Books.Query;
using MediatR;
using Microsoft.AspNetCore.Mvc;
namespace CqrsDemoWebApi.Controllers
{
[Route("api/[controller]")]
public class BookController : ControllerBase
{
private readonly IMediator _mediator;
public BookController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<IEnumerable<Book>> GetBooks()
{
var books = await _mediator.Send(new GetBooks.Query());
return books;
}
[HttpGet("{id}")]
public async Task<Book> GetBook(int id)
{
var book = await _mediator.Send(new GetBooksById.Query { Id = id });
return book;
}
[HttpPost]
public async Task<ActionResult> AddBook([FromBody] AddNewBook.Command command)
{
var createdBookId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetBook), new { id = createdBookId }, null);
}
[HttpDelete("{id}")]
public async Task<ActionResult> DeleteBook(int id)
{
await _mediator.Send(new DeleteCommand.Command { Id = id });
return NoContent();
}
}
}
As we see, we inject the IMediator here and then use the Dependency Injected field to send the command of query instance. Note the use of [FromBody] in Post requests to send the POST body into the handler for the Command.
Each query or command is implemented as nested classes where Query and QueryHandler is nested inside, implement IRequest of T and IRequestHandler of T (and return type U back again if we want a return result). If we do not want a return result 'void' - we use the special type Unit. This signals that we do not return any results from the command or the query. We can return an int value for the Command after we have inserted a row. It is okay in CQRS to return a resource identifier. CQRS allows also return results from command as long as it is a resource identifier to use to look up the data via a query. (it could be Guid etc too if that is suitable). Here is a example of a command that creates a book:
using CqrsDemoWebApi.Database;
using MediatR;
namespace CqrsDemoWebApi.Features.Books.Command
{
public class AddNewBook
{
public class Command : IRequest<int>
{
public string? Title { get; set; }
public string? Author { get; set; }
public int Year { get; set; }
public string? ImageLink { get; set; }
public int Pages { get; set; }
public string? Country { get; set; }
public string? Language { get; set; }
public string? Link { get; set; }
}
public class CommandHandler : IRequestHandler<Command, int>
{
private readonly BooksDbContext _booksDbContext;
public CommandHandler(BooksDbContext booksDbContext)
{
_booksDbContext = booksDbContext;
}
public async Task<int> Handle(Command request, CancellationToken cancellationToken)
{
var book = new Book
{
Title = request.Title,
Author = request.Author,
Year = request.Year,
ImageLink = request.ImageLink,
Pages = request.Pages,
Country = request.Country,
Language = request.Language,
Link = request.Link
};
await _booksDbContext.Books.AddAsync(book, cancellationToken);
_booksDbContext.SaveChanges();
return book.BookId;
}
}
}
}
A sample json body for the POST is then:
{
"title": "Mythical Man Month",
"author": "Frederick Brooks",
"year": 1975,
"country": "USA",
"imagelink": "",
"pages": 222
}
We return the BookId of the new book as we impement IRequestHandler<int> . It is the auto generated id after saving the new book.
This database is seeded by the way via a json file like this :
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
namespace CqrsDemoWebApi.Database.Seed
{
public static class ModelBuilderExtensions
{
public static void SeedBooks(this ModelBuilder modelBuilder, string contentRootPath)
{
string jsonInput = File.ReadAllText(Path.Combine(contentRootPath, @"Database/Seed/books.json"));
var books = JsonConvert.DeserializeObject<Book[]>(jsonInput);
int bookId = 0;
foreach (var book in books!)
{
bookId++;
book.BookId = bookId;
}
modelBuilder.Entity<Book>().HasData(books);
modelBuilder.Entity<Book>().HasKey(b => b.BookId);
modelBuilder.Entity<Book>().Property(b => b.BookId).ValueGeneratedOnAdd();
}
}
}
And we have the DbContext like this:
using CqrsDemoWebApi.Database.Seed;
using Microsoft.EntityFrameworkCore;
namespace CqrsDemoWebApi.Database
{
public class BooksDbContext : DbContext
{
private readonly IWebHostEnvironment _hostingEnvironment;
public BooksDbContext(DbContextOptions<BooksDbContext> options,
IWebHostEnvironment hostingEnvironment) : base(options)
{
_hostingEnvironment = hostingEnvironment;
}
public DbSet<Book> Books { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
string contentRootPath = _hostingEnvironment.ContentRootPath;
modelBuilder.SeedBooks(contentRootPath);
}
}
}
Now, the naming of each nested command/command handler or query/queryhandler is given a generic name. It could be that you want to make this a more specific name, but the wrapping class is a good enough specifier for this.
I.e. GetBooks.Query() or AddNewBook.Command (we use [FromBody] here and there do not new up an instance like the Query) are specified enough allowing us to use generic nested class names..
MediatR simplifies the goal of implementing CQRS and following the mediator pattern. There are more possibilities with MediatR. Look at videos on Youtube for this topic for example.
No comments:
Post a Comment