diff --git a/src/VerticalSliceArchitectureTemplate/Common/Domain/BaseEntity.cs b/src/VerticalSliceArchitectureTemplate/Common/Domain/BaseEntity.cs index 9a70f72..eeea9a3 100644 --- a/src/VerticalSliceArchitectureTemplate/Common/Domain/BaseEntity.cs +++ b/src/VerticalSliceArchitectureTemplate/Common/Domain/BaseEntity.cs @@ -1,4 +1,6 @@ -namespace VerticalSliceArchitectureTemplate.Common.Domain; +using MediatR; + +namespace VerticalSliceArchitectureTemplate.Common.Domain; public abstract class BaseEntity { diff --git a/src/VerticalSliceArchitectureTemplate/Common/Persistence/EventPublisher.cs b/src/VerticalSliceArchitectureTemplate/Common/Persistence/EventPublisher.cs index af2f85a..544b1bc 100644 --- a/src/VerticalSliceArchitectureTemplate/Common/Persistence/EventPublisher.cs +++ b/src/VerticalSliceArchitectureTemplate/Common/Persistence/EventPublisher.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.Diagnostics; +using MediatR; +using Microsoft.EntityFrameworkCore.Diagnostics; using VerticalSliceArchitectureTemplate.Common.Domain; namespace VerticalSliceArchitectureTemplate.Common.Persistence; diff --git a/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/CompleteTodoCommand.cs b/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/CompleteTodo.cs similarity index 60% rename from src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/CompleteTodoCommand.cs rename to src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/CompleteTodo.cs index 68e821f..18b49d7 100644 --- a/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/CompleteTodoCommand.cs +++ b/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/CompleteTodo.cs @@ -2,11 +2,12 @@ namespace VerticalSliceArchitectureTemplate.Features.Todos.Commands; -public sealed record CompleteTodoCommand(Guid Id) : IRequest; -public sealed class CompleteTodoCommandHandler(AppDbContext dbContext) : IRequestHandler +[Handler] +public sealed partial class CompleteTodo { - public async Task Handle(CompleteTodoCommand request, CancellationToken cancellationToken) + public sealed record Command(Guid Id); + private static async ValueTask HandleAsync(Command request, AppDbContext dbContext, CancellationToken cancellationToken) { var todo = await dbContext.Todos.FindAsync([request.Id], cancellationToken); diff --git a/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/CreateTodoCommand.cs b/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/CreateTodo.cs similarity index 57% rename from src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/CreateTodoCommand.cs rename to src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/CreateTodo.cs index dfd6bd8..ef14b1c 100644 --- a/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/CreateTodoCommand.cs +++ b/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/CreateTodo.cs @@ -2,11 +2,12 @@ namespace VerticalSliceArchitectureTemplate.Features.Todos.Commands; -public sealed record CreateTodoCommand(string Text) : IRequest; - -public sealed class CreateTodoCommandHandler(AppDbContext dbContext) : IRequestHandler +[Handler] +public sealed partial class CreateTodo { - public async Task Handle(CreateTodoCommand request, CancellationToken cancellationToken) + public sealed record Command(string Text); + + private static async ValueTask HandleAsync(Command request, AppDbContext dbContext, CancellationToken cancellationToken) { var todo = new Todo { diff --git a/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/DeleteTodoCommand.cs b/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/DeleteTodo.cs similarity index 61% rename from src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/DeleteTodoCommand.cs rename to src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/DeleteTodo.cs index 4b8b231..c128788 100644 --- a/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/DeleteTodoCommand.cs +++ b/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/DeleteTodo.cs @@ -2,11 +2,12 @@ namespace VerticalSliceArchitectureTemplate.Features.Todos.Commands; -public sealed record DeleteTodoCommand(Guid Id) : IRequest; - -public sealed class DeleteTodoCommandHandler(AppDbContext dbContext) : IRequestHandler +[Handler] +public sealed partial class DeleteTodo { - public async Task Handle(DeleteTodoCommand request, CancellationToken cancellationToken) + public sealed record Command(Guid Id); + + private static async ValueTask HandleAsync(Command request, AppDbContext dbContext, CancellationToken cancellationToken) { var todo = await dbContext.Todos.FindAsync([request.Id], cancellationToken); diff --git a/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/UpdateTodoCommand.cs b/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/UpdateTodo.cs similarity index 60% rename from src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/UpdateTodoCommand.cs rename to src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/UpdateTodo.cs index dd9f619..926560f 100644 --- a/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/UpdateTodoCommand.cs +++ b/src/VerticalSliceArchitectureTemplate/Features/Todos/Commands/UpdateTodo.cs @@ -2,11 +2,13 @@ namespace VerticalSliceArchitectureTemplate.Features.Todos.Commands; -public sealed record UpdateTodoCommand(Guid Id, string Text) : IRequest; -public sealed class UpdateTodoCommandHandler(AppDbContext dbContext) : IRequestHandler +[Handler] +public sealed partial class UpdateTodo { - public async Task Handle(UpdateTodoCommand request, CancellationToken cancellationToken) + public sealed record Command(Guid Id, string Text); + + private static async ValueTask HandleAsync(Command request, AppDbContext dbContext, CancellationToken cancellationToken) { var todo = await dbContext.Todos.FindAsync([request.Id], cancellationToken); diff --git a/src/VerticalSliceArchitectureTemplate/Features/Todos/Domain/Events/TodoCompletedEvent.cs b/src/VerticalSliceArchitectureTemplate/Features/Todos/Domain/Events/TodoCompletedEvent.cs index c7a68bc..5a2097d 100644 --- a/src/VerticalSliceArchitectureTemplate/Features/Todos/Domain/Events/TodoCompletedEvent.cs +++ b/src/VerticalSliceArchitectureTemplate/Features/Todos/Domain/Events/TodoCompletedEvent.cs @@ -1,3 +1,5 @@ -namespace VerticalSliceArchitectureTemplate.Features.Todos.Domain.Events; +using MediatR; + +namespace VerticalSliceArchitectureTemplate.Features.Todos.Domain.Events; public record TodoCompletedEvent(Guid TodoId) : INotification; \ No newline at end of file diff --git a/src/VerticalSliceArchitectureTemplate/Features/Todos/Domain/Events/TodoCreatedEvent.cs b/src/VerticalSliceArchitectureTemplate/Features/Todos/Domain/Events/TodoCreatedEvent.cs index 53a19a7..f343fb2 100644 --- a/src/VerticalSliceArchitectureTemplate/Features/Todos/Domain/Events/TodoCreatedEvent.cs +++ b/src/VerticalSliceArchitectureTemplate/Features/Todos/Domain/Events/TodoCreatedEvent.cs @@ -1,3 +1,5 @@ -namespace VerticalSliceArchitectureTemplate.Features.Todos.Domain.Events; +using MediatR; + +namespace VerticalSliceArchitectureTemplate.Features.Todos.Domain.Events; public record TodoCreatedEvent(Guid TodoId) : INotification; \ No newline at end of file diff --git a/src/VerticalSliceArchitectureTemplate/Features/Todos/Queries/GetAllTodos.cs b/src/VerticalSliceArchitectureTemplate/Features/Todos/Queries/GetAllTodos.cs new file mode 100644 index 0000000..351d252 --- /dev/null +++ b/src/VerticalSliceArchitectureTemplate/Features/Todos/Queries/GetAllTodos.cs @@ -0,0 +1,18 @@ +using VerticalSliceArchitectureTemplate.Features.Todos.Domain; + +namespace VerticalSliceArchitectureTemplate.Features.Todos.Queries; + +[Handler] +public sealed partial class GetAllTodos +{ + public sealed record Query(bool? IsCompleted = null); + + private static async ValueTask> HandleAsync(Query request, AppDbContext dbContext, CancellationToken cancellationToken) + { + var todos = await dbContext.Todos + .Where(x => request.IsCompleted == null || x.IsCompleted == request.IsCompleted) + .ToListAsync(cancellationToken); + + return todos.AsReadOnly(); + } +} \ No newline at end of file diff --git a/src/VerticalSliceArchitectureTemplate/Features/Todos/Queries/GetAllTodosQuery.cs b/src/VerticalSliceArchitectureTemplate/Features/Todos/Queries/GetAllTodosQuery.cs deleted file mode 100644 index c0fc8e2..0000000 --- a/src/VerticalSliceArchitectureTemplate/Features/Todos/Queries/GetAllTodosQuery.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Immutable; -using VerticalSliceArchitectureTemplate.Features.Todos.Domain; - -namespace VerticalSliceArchitectureTemplate.Features.Todos.Queries; - -public sealed record GetAllTodosQuery(bool? IsCompleted = null) : IRequest>; - -public sealed class GetAllTodosQueryHandler(AppDbContext dbContext) : IRequestHandler> -{ - public async Task> Handle(GetAllTodosQuery request, CancellationToken cancellationToken) - { - var todos = await dbContext.Todos - .Where(x => request.IsCompleted == null || x.IsCompleted == request.IsCompleted) - .ToListAsync(cancellationToken); - - return todos.AsReadOnly(); - } -} \ No newline at end of file diff --git a/src/VerticalSliceArchitectureTemplate/Features/Todos/Queries/GetTodoQuery.cs b/src/VerticalSliceArchitectureTemplate/Features/Todos/Queries/GetTodo.cs similarity index 57% rename from src/VerticalSliceArchitectureTemplate/Features/Todos/Queries/GetTodoQuery.cs rename to src/VerticalSliceArchitectureTemplate/Features/Todos/Queries/GetTodo.cs index c502069..5979856 100644 --- a/src/VerticalSliceArchitectureTemplate/Features/Todos/Queries/GetTodoQuery.cs +++ b/src/VerticalSliceArchitectureTemplate/Features/Todos/Queries/GetTodo.cs @@ -2,11 +2,12 @@ namespace VerticalSliceArchitectureTemplate.Features.Todos.Queries; -public sealed record GetTodoQuery(Guid Id) : IRequest; - -public sealed class GetTodoQueryHandler(AppDbContext dbContext) : IRequestHandler +[Handler] +public sealed partial class GetTodo { - public async Task Handle(GetTodoQuery request, CancellationToken cancellationToken) + public sealed record Query(Guid Id); + + private static async ValueTask HandleAsync(Query request, AppDbContext dbContext, CancellationToken cancellationToken) { var todo = await dbContext.Todos.FindAsync([request.Id], cancellationToken); diff --git a/src/VerticalSliceArchitectureTemplate/Features/Todos/WebApi/TodoEndpoints.cs b/src/VerticalSliceArchitectureTemplate/Features/Todos/WebApi/TodoEndpoints.cs index 6ca813b..b48e9ad 100644 --- a/src/VerticalSliceArchitectureTemplate/Features/Todos/WebApi/TodoEndpoints.cs +++ b/src/VerticalSliceArchitectureTemplate/Features/Todos/WebApi/TodoEndpoints.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using Microsoft.AspNetCore.Mvc; using VerticalSliceArchitectureTemplate.Features.Todos.Commands; using VerticalSliceArchitectureTemplate.Features.Todos.Domain; using VerticalSliceArchitectureTemplate.Features.Todos.Queries; @@ -13,9 +14,9 @@ public static void MapEndpoints(IEndpointRouteBuilder endpoints) .WithTags(nameof(Todo)); group.MapPost("", - async (CreateTodoCommand command, ISender sender, CancellationToken cancellationToken) => + async (CreateTodo.Command command, CreateTodo.Handler handler, CancellationToken cancellationToken) => { - var id = await sender.Send(command, cancellationToken); + var id = await handler.HandleAsync(command, cancellationToken); return Results.Created($"/todos/{id}", id); }) .Produces(StatusCodes.Status201Created) @@ -24,9 +25,12 @@ public static void MapEndpoints(IEndpointRouteBuilder endpoints) .WithTags(nameof(Todo)); group.MapPut("/{id:guid}", - async (Guid id, UpdateTodoCommand command, ISender sender, CancellationToken cancellationToken) => + async (Guid id, UpdateTodo.Command command, UpdateTodo.Handler handler, CancellationToken cancellationToken) => { - await sender.Send(command with { Id = id }, cancellationToken); + await handler.HandleAsync(command with + { + Id = id // TODO: Remove this duplication + }, cancellationToken); return Results.NoContent(); }) .Produces(StatusCodes.Status204NoContent) @@ -36,9 +40,9 @@ public static void MapEndpoints(IEndpointRouteBuilder endpoints) .WithTags(nameof(Todo)); group.MapPut("/{id:guid}/complete", - async (Guid id, ISender sender, CancellationToken cancellationToken) => + async ([FromRoute] Guid id, CompleteTodo.Handler handler, CancellationToken cancellationToken) => { - await sender.Send(new CompleteTodoCommand(id), cancellationToken); + await handler.HandleAsync(new CompleteTodo.Command(id), cancellationToken); return Results.NoContent(); }) .Produces(StatusCodes.Status204NoContent) @@ -48,9 +52,9 @@ public static void MapEndpoints(IEndpointRouteBuilder endpoints) .WithTags(nameof(Todo)); group.MapDelete("/{id:guid}", - async (Guid id, ISender sender, CancellationToken cancellationToken) => + async ([FromRoute] Guid id, DeleteTodo.Handler handler, CancellationToken cancellationToken) => { - await sender.Send(new DeleteTodoCommand(id), cancellationToken); + await handler.HandleAsync(new DeleteTodo.Command(id), cancellationToken); return Results.NoContent(); }) .Produces(StatusCodes.Status204NoContent) @@ -60,17 +64,17 @@ public static void MapEndpoints(IEndpointRouteBuilder endpoints) .WithTags(nameof(Todo)); group.MapGet("/{id:guid}", - (Guid id, ISender sender, CancellationToken cancellationToken) - => sender.Send(new GetTodoQuery(id), cancellationToken)) + (Guid id, GetTodo.Handler handler, CancellationToken cancellationToken) + => handler.HandleAsync(new GetTodo.Query(id), cancellationToken)) .Produces() .Produces(StatusCodes.Status404NotFound) .ProducesProblem(StatusCodes.Status500InternalServerError) .WithTags(nameof(Todo)); group.MapGet("", - (bool? isCompleted, ISender sender, CancellationToken cancellationToken) - => sender.Send(new GetAllTodosQuery(isCompleted), cancellationToken)) - .Produces>() + ([AsParameters] GetAllTodos.Query query, GetAllTodos.Handler handler, CancellationToken cancellationToken) + => handler.HandleAsync(query, cancellationToken)) + .Produces>() .Produces(StatusCodes.Status404NotFound) .ProducesProblem(StatusCodes.Status500InternalServerError) .WithTags(nameof(Todo)); diff --git a/src/VerticalSliceArchitectureTemplate/GlobalUsings.cs b/src/VerticalSliceArchitectureTemplate/GlobalUsings.cs index 08d3c73..64b8db4 100644 --- a/src/VerticalSliceArchitectureTemplate/GlobalUsings.cs +++ b/src/VerticalSliceArchitectureTemplate/GlobalUsings.cs @@ -2,4 +2,4 @@ global using VerticalSliceArchitectureTemplate.Common.Exceptions; global using VerticalSliceArchitectureTemplate.Common.Features; global using VerticalSliceArchitectureTemplate.Common.Persistence; -global using MediatR; \ No newline at end of file +global using Immediate.Handlers.Shared; \ No newline at end of file diff --git a/src/VerticalSliceArchitectureTemplate/Program.cs b/src/VerticalSliceArchitectureTemplate/Program.cs index a24791d..4b1612a 100644 --- a/src/VerticalSliceArchitectureTemplate/Program.cs +++ b/src/VerticalSliceArchitectureTemplate/Program.cs @@ -1,4 +1,5 @@ using System.Reflection; +using VerticalSliceArchitectureTemplate; using VerticalSliceArchitectureTemplate.Host; var appAssembly = Assembly.GetExecutingAssembly(); @@ -8,6 +9,13 @@ builder.Services.AddEfCore(); // Host +builder.Services.AddHandlers(); +builder.Services.AddBehaviors(); +builder.Services.AddSwaggerGen( options => +{ + options.CustomSchemaIds(x => x.FullName?.Replace("+", ".", StringComparison.Ordinal)); +}); + builder.Services.AddMediatR(configure => configure.RegisterServicesFromAssemblyContaining()); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); diff --git a/src/VerticalSliceArchitectureTemplate/VerticalSliceArchitectureTemplate.csproj b/src/VerticalSliceArchitectureTemplate/VerticalSliceArchitectureTemplate.csproj index d6966ae..e70d546 100644 --- a/src/VerticalSliceArchitectureTemplate/VerticalSliceArchitectureTemplate.csproj +++ b/src/VerticalSliceArchitectureTemplate/VerticalSliceArchitectureTemplate.csproj @@ -7,6 +7,7 @@ +