From c4ce1443cd4cf418ed08252b3b657c4eefbca87f Mon Sep 17 00:00:00 2001 From: Nadir Badnjevic Date: Fri, 15 Mar 2024 22:13:49 +0100 Subject: [PATCH] refactor: use records for dto, remove automapper --- Directory.Packages.props | 3 +- README.md | 3 +- src/Application/ApiPaths.cs | 6 ++ src/Application/Application.csproj | 1 - src/Application/Common/Mappings/IMapFrom.cs | 11 --- .../Common/Mappings/MappingExtensions.cs | 12 +-- .../Common/Mappings/MappingProfile.cs | 31 ------- src/Application/ConfigureServices.cs | 1 - src/Application/Domain/Todos/TodoItem.cs | 17 +--- .../Domain/Todos/TodoItemCompletedEvent.cs | 8 ++ .../Domain/Todos/TodoItemCreatedEvent.cs | 8 ++ .../Domain/Todos/TodoItemDeletedEvent.cs | 8 ++ .../Features/TodoItems/CreateTodoItem.cs | 28 +----- .../Features/TodoItems/DeleteTodoItem.cs | 26 +----- .../TodoItemCompletedEventHandler.cs | 9 +- .../TodoItemCreatedEventHandler.cs | 10 +-- .../TodoItems/GetTodoItemsWithPagination.cs | 49 +++-------- .../Features/TodoItems/UpdateTodoItem.cs | 26 ++---- .../TodoItems/UpdateTodoItemDetail.cs | 28 ++---- .../Features/TodoLists/CreateTodoList.cs | 22 ++--- .../Features/TodoLists/DeleteTodoList.cs | 20 ++--- .../Features/TodoLists/ExportTodos.cs | 44 ++-------- .../Features/TodoLists/GetTodos.cs | 87 +++++++------------ .../Infrastructure/Files/CsvFileBuilder.cs | 1 - .../Files/{Maps => }/TodoItemRecordMap.cs | 2 +- .../Common/Behaviours/RequestLoggerTests.cs | 4 +- .../Common/Mappings/MappingTests.cs | 42 --------- 27 files changed, 131 insertions(+), 376 deletions(-) create mode 100644 src/Application/ApiPaths.cs delete mode 100644 src/Application/Common/Mappings/IMapFrom.cs delete mode 100644 src/Application/Common/Mappings/MappingProfile.cs create mode 100644 src/Application/Domain/Todos/TodoItemCompletedEvent.cs create mode 100644 src/Application/Domain/Todos/TodoItemCreatedEvent.cs create mode 100644 src/Application/Domain/Todos/TodoItemDeletedEvent.cs rename src/Application/Infrastructure/Files/{Maps => }/TodoItemRecordMap.cs (93%) delete mode 100644 tests/Application.UnitTests/Common/Mappings/MappingTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index b7ca5fa..8a3520a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -16,8 +16,7 @@ - - + diff --git a/README.md b/README.md index a1db4f9..46fee29 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,8 @@ This project repository is created based on [Clean Architecture solution templat - [ASP.NET API with .NET 8](https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-8.0) - CQRS with [MediatR](https://github.com/jbogard/MediatR) - [FluentValidation](https://fluentvalidation.net/) -- [AutoMapper](https://automapper.org/) - [Entity Framework Core 8](https://docs.microsoft.com/en-us/ef/core/) -- [NUnit](https://nunit.org/), [FluentAssertions](https://fluentassertions.com/), [Moq](https://github.com/moq) +- [xUnit](https://xunit.net/), [FluentAssertions](https://fluentassertions.com/), [Moq](https://github.com/moq) Afterwards, the projects and architecture is refactored towards the Vertical slice architecture style. diff --git a/src/Application/ApiPaths.cs b/src/Application/ApiPaths.cs new file mode 100644 index 0000000..7d41de9 --- /dev/null +++ b/src/Application/ApiPaths.cs @@ -0,0 +1,6 @@ +namespace Application; + +internal static class ApiPaths +{ + internal const string Root = "api"; +} \ No newline at end of file diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index 8920bc6..adbc22a 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -4,7 +4,6 @@ - diff --git a/src/Application/Common/Mappings/IMapFrom.cs b/src/Application/Common/Mappings/IMapFrom.cs deleted file mode 100644 index 79a20ac..0000000 --- a/src/Application/Common/Mappings/IMapFrom.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace VerticalSliceArchitecture.Application.Common.Mappings; - -public interface IMapFrom -{ - void Mapping(Profile profile) - { - profile.CreateMap(typeof(T), GetType()); - } -} \ No newline at end of file diff --git a/src/Application/Common/Mappings/MappingExtensions.cs b/src/Application/Common/Mappings/MappingExtensions.cs index 39d406b..a2337e7 100644 --- a/src/Application/Common/Mappings/MappingExtensions.cs +++ b/src/Application/Common/Mappings/MappingExtensions.cs @@ -1,9 +1,4 @@ -using AutoMapper; -using AutoMapper.QueryableExtensions; - -using Microsoft.EntityFrameworkCore; - -using VerticalSliceArchitecture.Application.Common.Models; +using VerticalSliceArchitecture.Application.Common.Models; namespace VerticalSliceArchitecture.Application.Common.Mappings; @@ -13,9 +8,4 @@ public static Task> PaginatedListAsync { return PaginatedList.CreateAsync(queryable, pageNumber, pageSize); } - - public static Task> ProjectToListAsync(this IQueryable queryable, IConfigurationProvider configuration) - { - return queryable.ProjectTo(configuration).ToListAsync(); - } } \ No newline at end of file diff --git a/src/Application/Common/Mappings/MappingProfile.cs b/src/Application/Common/Mappings/MappingProfile.cs deleted file mode 100644 index 99cbfb0..0000000 --- a/src/Application/Common/Mappings/MappingProfile.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Reflection; - -using AutoMapper; - -namespace VerticalSliceArchitecture.Application.Common.Mappings; - -public class MappingProfile : Profile -{ - public MappingProfile() - { - ApplyMappingsFromAssembly(Assembly.GetExecutingAssembly()); - } - - private void ApplyMappingsFromAssembly(Assembly assembly) - { - var types = assembly.GetExportedTypes() - .Where(t => t.GetInterfaces().Any(i => - i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapFrom<>))) - .ToList(); - - foreach (var type in types) - { - var instance = Activator.CreateInstance(type); - - var methodInfo = type.GetMethod("Mapping") - ?? type.GetInterface("IMapFrom`1")!.GetMethod("Mapping"); - - methodInfo?.Invoke(instance, new object[] { this }); - } - } -} \ No newline at end of file diff --git a/src/Application/ConfigureServices.cs b/src/Application/ConfigureServices.cs index 8088e93..cda66c7 100644 --- a/src/Application/ConfigureServices.cs +++ b/src/Application/ConfigureServices.cs @@ -16,7 +16,6 @@ public static class DependencyInjection { public static IServiceCollection AddApplication(this IServiceCollection services) { - services.AddAutoMapper(typeof(DependencyInjection).Assembly); services.AddValidatorsFromAssembly(typeof(DependencyInjection).Assembly); services.AddMediatR(options => diff --git a/src/Application/Domain/Todos/TodoItem.cs b/src/Application/Domain/Todos/TodoItem.cs index 50d9252..4a9715f 100644 --- a/src/Application/Domain/Todos/TodoItem.cs +++ b/src/Application/Domain/Todos/TodoItem.cs @@ -37,16 +37,6 @@ public bool Done public List DomainEvents { get; } = new List(); } -public class TodoItemCompletedEvent : DomainEvent -{ - public TodoItemCompletedEvent(TodoItem item) - { - Item = item; - } - - public TodoItem Item { get; } -} - public enum PriorityLevel { None = 0, @@ -55,9 +45,4 @@ public enum PriorityLevel High = 3, } -public class TodoItemRecord : IMapFrom -{ - public string? Title { get; set; } - - public bool Done { get; set; } -} \ No newline at end of file +public record TodoItemRecord(string? Title, bool Done); \ No newline at end of file diff --git a/src/Application/Domain/Todos/TodoItemCompletedEvent.cs b/src/Application/Domain/Todos/TodoItemCompletedEvent.cs new file mode 100644 index 0000000..55adeb8 --- /dev/null +++ b/src/Application/Domain/Todos/TodoItemCompletedEvent.cs @@ -0,0 +1,8 @@ +using VerticalSliceArchitecture.Application.Common; + +namespace VerticalSliceArchitecture.Application.Domain.Todos; + +internal sealed class TodoItemCompletedEvent(TodoItem item) : DomainEvent +{ + public TodoItem Item { get; } = item; +} \ No newline at end of file diff --git a/src/Application/Domain/Todos/TodoItemCreatedEvent.cs b/src/Application/Domain/Todos/TodoItemCreatedEvent.cs new file mode 100644 index 0000000..13f33ff --- /dev/null +++ b/src/Application/Domain/Todos/TodoItemCreatedEvent.cs @@ -0,0 +1,8 @@ +using VerticalSliceArchitecture.Application.Common; + +namespace VerticalSliceArchitecture.Application.Domain.Todos; + +internal sealed class TodoItemCreatedEvent(TodoItem item) : DomainEvent +{ + public TodoItem Item { get; } = item; +} \ No newline at end of file diff --git a/src/Application/Domain/Todos/TodoItemDeletedEvent.cs b/src/Application/Domain/Todos/TodoItemDeletedEvent.cs new file mode 100644 index 0000000..6f2dff5 --- /dev/null +++ b/src/Application/Domain/Todos/TodoItemDeletedEvent.cs @@ -0,0 +1,8 @@ +using VerticalSliceArchitecture.Application.Common; + +namespace VerticalSliceArchitecture.Application.Domain.Todos; + +internal sealed class TodoItemDeletedEvent(TodoItem item) : DomainEvent +{ + public TodoItem Item { get; } = item; +} \ No newline at end of file diff --git a/src/Application/Features/TodoItems/CreateTodoItem.cs b/src/Application/Features/TodoItems/CreateTodoItem.cs index 620b0be..6238a0f 100644 --- a/src/Application/Features/TodoItems/CreateTodoItem.cs +++ b/src/Application/Features/TodoItems/CreateTodoItem.cs @@ -19,14 +19,9 @@ public async Task> Create(CreateTodoItemCommand command) } } -public class CreateTodoItemCommand : IRequest -{ - public int ListId { get; set; } - - public string? Title { get; set; } -} +public record CreateTodoItemCommand(int ListId, string? Title) : IRequest; -public class CreateTodoItemCommandValidator : AbstractValidator +internal sealed class CreateTodoItemCommandValidator : AbstractValidator { public CreateTodoItemCommandValidator() { @@ -36,14 +31,9 @@ public CreateTodoItemCommandValidator() } } -internal sealed class CreateTodoItemCommandHandler : IRequestHandler +internal sealed class CreateTodoItemCommandHandler(ApplicationDbContext context) : IRequestHandler { - private readonly ApplicationDbContext _context; - - public CreateTodoItemCommandHandler(ApplicationDbContext context) - { - _context = context; - } + private readonly ApplicationDbContext _context = context; public async Task Handle(CreateTodoItemCommand request, CancellationToken cancellationToken) { @@ -62,14 +52,4 @@ public async Task Handle(CreateTodoItemCommand request, CancellationToken c return entity.Id; } -} - -internal sealed class TodoItemCreatedEvent : DomainEvent -{ - public TodoItemCreatedEvent(TodoItem item) - { - Item = item; - } - - public TodoItem Item { get; } } \ No newline at end of file diff --git a/src/Application/Features/TodoItems/DeleteTodoItem.cs b/src/Application/Features/TodoItems/DeleteTodoItem.cs index 3be9178..b9acfad 100644 --- a/src/Application/Features/TodoItems/DeleteTodoItem.cs +++ b/src/Application/Features/TodoItems/DeleteTodoItem.cs @@ -14,25 +14,17 @@ public class DeleteTodoItemController : ApiControllerBase [HttpDelete("/api/todo-items/{id}")] public async Task Delete(int id) { - await Mediator.Send(new DeleteTodoItemCommand { Id = id }); + await Mediator.Send(new DeleteTodoItemCommand(id)); return NoContent(); } } -public class DeleteTodoItemCommand : IRequest -{ - public int Id { get; set; } -} +public record DeleteTodoItemCommand(int Id) : IRequest; -internal sealed class DeleteTodoItemCommandHandler : IRequestHandler +internal sealed class DeleteTodoItemCommandHandler(ApplicationDbContext context) : IRequestHandler { - private readonly ApplicationDbContext _context; - - public DeleteTodoItemCommandHandler(ApplicationDbContext context) - { - _context = context; - } + private readonly ApplicationDbContext _context = context; public async Task Handle(DeleteTodoItemCommand request, CancellationToken cancellationToken) { @@ -44,14 +36,4 @@ public async Task Handle(DeleteTodoItemCommand request, CancellationToken cancel await _context.SaveChangesAsync(cancellationToken); } -} - -internal class TodoItemDeletedEvent : DomainEvent -{ - public TodoItemDeletedEvent(TodoItem item) - { - Item = item; - } - - public TodoItem Item { get; } } \ No newline at end of file diff --git a/src/Application/Features/TodoItems/EventHandlers/TodoItemCompletedEventHandler.cs b/src/Application/Features/TodoItems/EventHandlers/TodoItemCompletedEventHandler.cs index 491870c..b84b496 100644 --- a/src/Application/Features/TodoItems/EventHandlers/TodoItemCompletedEventHandler.cs +++ b/src/Application/Features/TodoItems/EventHandlers/TodoItemCompletedEventHandler.cs @@ -7,14 +7,9 @@ namespace VerticalSliceArchitecture.Application.Features.TodoItems.EventHandlers; -public class TodoItemCompletedEventHandler : INotificationHandler> +internal sealed class TodoItemCompletedEventHandler(ILogger logger) : INotificationHandler> { - private readonly ILogger _logger; - - public TodoItemCompletedEventHandler(ILogger logger) - { - _logger = logger; - } + private readonly ILogger _logger = logger; public Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) { diff --git a/src/Application/Features/TodoItems/EventHandlers/TodoItemCreatedEventHandler.cs b/src/Application/Features/TodoItems/EventHandlers/TodoItemCreatedEventHandler.cs index 8ce1383..90d4db7 100644 --- a/src/Application/Features/TodoItems/EventHandlers/TodoItemCreatedEventHandler.cs +++ b/src/Application/Features/TodoItems/EventHandlers/TodoItemCreatedEventHandler.cs @@ -3,17 +3,13 @@ using Microsoft.Extensions.Logging; using VerticalSliceArchitecture.Application.Common.Models; +using VerticalSliceArchitecture.Application.Domain.Todos; namespace VerticalSliceArchitecture.Application.Features.TodoItems.EventHandlers; -internal sealed class TodoItemCreatedEventHandler : INotificationHandler> +internal sealed class TodoItemCreatedEventHandler(ILogger logger) : INotificationHandler> { - private readonly ILogger _logger; - - public TodoItemCreatedEventHandler(ILogger logger) - { - _logger = logger; - } + private readonly ILogger _logger = logger; public Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) { diff --git a/src/Application/Features/TodoItems/GetTodoItemsWithPagination.cs b/src/Application/Features/TodoItems/GetTodoItemsWithPagination.cs index 59ebee9..a941231 100644 --- a/src/Application/Features/TodoItems/GetTodoItemsWithPagination.cs +++ b/src/Application/Features/TodoItems/GetTodoItemsWithPagination.cs @@ -1,7 +1,4 @@ -using AutoMapper; -using AutoMapper.QueryableExtensions; - -using FluentValidation; +using FluentValidation; using MediatR; @@ -18,31 +15,17 @@ namespace VerticalSliceArchitecture.Application.Features.TodoItems; public class GetTodoItemsWithPaginationController : ApiControllerBase { [HttpGet("/api/todo-items")] - public Task> GetTodoItemsWithPagination([FromQuery] GetTodoItemsWithPaginationQuery query) + public Task> GetTodoItemsWithPagination([FromQuery] GetTodoItemsWithPaginationQuery query) { return Mediator.Send(query); } } -public class TodoItemBriefDto : IMapFrom -{ - public int Id { get; set; } - - public int ListId { get; set; } - - public string? Title { get; set; } - - public bool Done { get; set; } -} +public record TodoItemBriefResponse(int Id, int ListId, string? Title, bool Done); -public class GetTodoItemsWithPaginationQuery : IRequest> -{ - public int ListId { get; set; } - public int PageNumber { get; set; } = 1; - public int PageSize { get; set; } = 10; -} +public record GetTodoItemsWithPaginationQuery(int ListId, int PageNumber = 1, int PageSize = 10) : IRequest>; -public class GetTodoItemsWithPaginationQueryValidator : AbstractValidator +internal sealed class GetTodoItemsWithPaginationQueryValidator : AbstractValidator { public GetTodoItemsWithPaginationQueryValidator() { @@ -57,23 +40,19 @@ public GetTodoItemsWithPaginationQueryValidator() } } -internal sealed class GetTodoItemsWithPaginationQueryHandler : IRequestHandler> +internal sealed class GetTodoItemsWithPaginationQueryHandler(ApplicationDbContext context) : IRequestHandler> { - private readonly ApplicationDbContext _context; - private readonly IMapper _mapper; - - public GetTodoItemsWithPaginationQueryHandler(ApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } + private readonly ApplicationDbContext _context = context; - public Task> Handle(GetTodoItemsWithPaginationQuery request, CancellationToken cancellationToken) + public Task> Handle(GetTodoItemsWithPaginationQuery request, CancellationToken cancellationToken) { return _context.TodoItems - .Where(x => x.ListId == request.ListId) - .OrderBy(x => x.Title) - .ProjectTo(_mapper.ConfigurationProvider) + .Where(item => item.ListId == request.ListId) + .OrderBy(item => item.Title) + .Select(item => ToDto(item)) .PaginatedListAsync(request.PageNumber, request.PageSize); } + + private static TodoItemBriefResponse ToDto(TodoItem todoItem) => + new(todoItem.Id, todoItem.ListId, todoItem.Title, todoItem.Done); } \ No newline at end of file diff --git a/src/Application/Features/TodoItems/UpdateTodoItem.cs b/src/Application/Features/TodoItems/UpdateTodoItem.cs index c21ed03..d9585bb 100644 --- a/src/Application/Features/TodoItems/UpdateTodoItem.cs +++ b/src/Application/Features/TodoItems/UpdateTodoItem.cs @@ -27,16 +27,9 @@ public async Task Update(int id, UpdateTodoItemCommand command) } } -public class UpdateTodoItemCommand : IRequest -{ - public int Id { get; set; } - - public string? Title { get; set; } - - public bool Done { get; set; } -} +public record UpdateTodoItemCommand(int Id, string? Title, bool Done) : IRequest; -public class UpdateTodoItemCommandValidator : AbstractValidator +internal sealed class UpdateTodoItemCommandValidator : AbstractValidator { public UpdateTodoItemCommandValidator() { @@ -46,22 +39,17 @@ public UpdateTodoItemCommandValidator() } } -internal sealed class UpdateTodoItemCommandHandler : IRequestHandler +internal sealed class UpdateTodoItemCommandHandler(ApplicationDbContext context) : IRequestHandler { - private readonly ApplicationDbContext _context; - - public UpdateTodoItemCommandHandler(ApplicationDbContext context) - { - _context = context; - } + private readonly ApplicationDbContext _context = context; public async Task Handle(UpdateTodoItemCommand request, CancellationToken cancellationToken) { - var entity = await _context.TodoItems + var todoItem = await _context.TodoItems .FindAsync(new object[] { request.Id }, cancellationToken) ?? throw new NotFoundException(nameof(TodoItem), request.Id); - entity.Title = request.Title; - entity.Done = request.Done; + todoItem.Title = request.Title; + todoItem.Done = request.Done; await _context.SaveChangesAsync(cancellationToken); } diff --git a/src/Application/Features/TodoItems/UpdateTodoItemDetail.cs b/src/Application/Features/TodoItems/UpdateTodoItemDetail.cs index 9f6d5d0..6168f3a 100644 --- a/src/Application/Features/TodoItems/UpdateTodoItemDetail.cs +++ b/src/Application/Features/TodoItems/UpdateTodoItemDetail.cs @@ -25,34 +25,20 @@ public async Task UpdateItemDetails(int id, UpdateTodoItemDetailCo } } -public class UpdateTodoItemDetailCommand : IRequest -{ - public int Id { get; set; } - - public int ListId { get; set; } - - public PriorityLevel Priority { get; set; } - - public string? Note { get; set; } -} +public record UpdateTodoItemDetailCommand(int Id, int ListId, PriorityLevel Priority, string? Note) : IRequest; -public class UpdateTodoItemDetailCommandHandler : IRequestHandler +internal sealed class UpdateTodoItemDetailCommandHandler(ApplicationDbContext context) : IRequestHandler { - private readonly ApplicationDbContext _context; - - public UpdateTodoItemDetailCommandHandler(ApplicationDbContext context) - { - _context = context; - } + private readonly ApplicationDbContext _context = context; public async Task Handle(UpdateTodoItemDetailCommand request, CancellationToken cancellationToken) { - var entity = await _context.TodoItems + var todoItem = await _context.TodoItems .FindAsync(new object[] { request.Id }, cancellationToken) ?? throw new NotFoundException(nameof(TodoItem), request.Id); - entity.ListId = request.ListId; - entity.Priority = request.Priority; - entity.Note = request.Note; + todoItem.ListId = request.ListId; + todoItem.Priority = request.Priority; + todoItem.Note = request.Note; await _context.SaveChangesAsync(cancellationToken); } diff --git a/src/Application/Features/TodoLists/CreateTodoList.cs b/src/Application/Features/TodoLists/CreateTodoList.cs index 0dd2f67..2f456e0 100644 --- a/src/Application/Features/TodoLists/CreateTodoList.cs +++ b/src/Application/Features/TodoLists/CreateTodoList.cs @@ -20,12 +20,9 @@ public async Task> Create(CreateTodoListCommand command) } } -public class CreateTodoListCommand : IRequest -{ - public string? Title { get; set; } -} +public record CreateTodoListCommand(string? Title) : IRequest; -public class CreateTodoListCommandValidator : AbstractValidator +internal sealed class CreateTodoListCommandValidator : AbstractValidator { private readonly ApplicationDbContext _context; @@ -46,23 +43,18 @@ private Task BeUniqueTitle(string title, CancellationToken cancellationTok } } -internal sealed class CreateTodoListCommandHandler : IRequestHandler +internal sealed class CreateTodoListCommandHandler(ApplicationDbContext context) : IRequestHandler { - private readonly ApplicationDbContext _context; - - public CreateTodoListCommandHandler(ApplicationDbContext context) - { - _context = context; - } + private readonly ApplicationDbContext _context = context; public async Task Handle(CreateTodoListCommand request, CancellationToken cancellationToken) { - var entity = new TodoList { Title = request.Title }; + var todoList = new TodoList { Title = request.Title }; - _context.TodoLists.Add(entity); + _context.TodoLists.Add(todoList); await _context.SaveChangesAsync(cancellationToken); - return entity.Id; + return todoList.Id; } } \ No newline at end of file diff --git a/src/Application/Features/TodoLists/DeleteTodoList.cs b/src/Application/Features/TodoLists/DeleteTodoList.cs index 546b3c3..9d17202 100644 --- a/src/Application/Features/TodoLists/DeleteTodoList.cs +++ b/src/Application/Features/TodoLists/DeleteTodoList.cs @@ -15,33 +15,25 @@ public class DeleteTodoListController : ApiControllerBase [HttpDelete("/api/todo-lists/{id}")] public async Task Delete(int id) { - await Mediator.Send(new DeleteTodoListCommand { Id = id }); + await Mediator.Send(new DeleteTodoListCommand(id)); return NoContent(); } } -public class DeleteTodoListCommand : IRequest -{ - public int Id { get; set; } -} +public record DeleteTodoListCommand(int Id) : IRequest; -internal sealed class DeleteTodoListCommandHandler : IRequestHandler +internal sealed class DeleteTodoListCommandHandler(ApplicationDbContext context) : IRequestHandler { - private readonly ApplicationDbContext _context; - - public DeleteTodoListCommandHandler(ApplicationDbContext context) - { - _context = context; - } + private readonly ApplicationDbContext _context = context; public async Task Handle(DeleteTodoListCommand request, CancellationToken cancellationToken) { - var entity = await _context.TodoLists + var todoList = await _context.TodoLists .Where(l => l.Id == request.Id) .SingleOrDefaultAsync(cancellationToken) ?? throw new NotFoundException(nameof(TodoList), request.Id); - _context.TodoLists.Remove(entity); + _context.TodoLists.Remove(todoList); await _context.SaveChangesAsync(cancellationToken); } diff --git a/src/Application/Features/TodoLists/ExportTodos.cs b/src/Application/Features/TodoLists/ExportTodos.cs index da080c9..3e4be29 100644 --- a/src/Application/Features/TodoLists/ExportTodos.cs +++ b/src/Application/Features/TodoLists/ExportTodos.cs @@ -1,7 +1,4 @@ -using AutoMapper; -using AutoMapper.QueryableExtensions; - -using MediatR; +using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -18,51 +15,26 @@ public class ExportTodosController : ApiControllerBase [HttpGet("/api/todo-lists/{id}")] public async Task Get(int id) { - var vm = await Mediator.Send(new ExportTodosQuery { ListId = id }); + var vm = await Mediator.Send(new ExportTodosQuery(id)); return File(vm.Content, vm.ContentType, vm.FileName); } } -public class ExportTodosQuery : IRequest -{ - public int ListId { get; set; } -} - -public class ExportTodosVm -{ - public ExportTodosVm(string fileName, string contentType, byte[] content) - { - FileName = fileName; - ContentType = contentType; - Content = content; - } +public record ExportTodosQuery(int ListId) : IRequest; - public string FileName { get; set; } +public record ExportTodosVm(string FileName, string ContentType, byte[] Content); - public string ContentType { get; set; } - - public byte[] Content { get; set; } -} - -internal sealed class ExportTodosQueryHandler : IRequestHandler +internal sealed class ExportTodosQueryHandler(ApplicationDbContext context, ICsvFileBuilder fileBuilder) : IRequestHandler { - private readonly ApplicationDbContext _context; - private readonly IMapper _mapper; - private readonly ICsvFileBuilder _fileBuilder; - - public ExportTodosQueryHandler(ApplicationDbContext context, IMapper mapper, ICsvFileBuilder fileBuilder) - { - _context = context; - _mapper = mapper; - _fileBuilder = fileBuilder; - } + private readonly ApplicationDbContext _context = context; + private readonly ICsvFileBuilder _fileBuilder = fileBuilder; public async Task Handle(ExportTodosQuery request, CancellationToken cancellationToken) { var records = await _context.TodoItems .Where(t => t.ListId == request.ListId) - .ProjectTo(_mapper.ConfigurationProvider) + .Select(item => new TodoItemRecord(item.Title, item.Done)) .ToListAsync(cancellationToken); var vm = new ExportTodosVm( diff --git a/src/Application/Features/TodoLists/GetTodos.cs b/src/Application/Features/TodoLists/GetTodos.cs index 42a13af..a30b74f 100644 --- a/src/Application/Features/TodoLists/GetTodos.cs +++ b/src/Application/Features/TodoLists/GetTodos.cs @@ -1,13 +1,9 @@ -using AutoMapper; -using AutoMapper.QueryableExtensions; - -using MediatR; +using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using VerticalSliceArchitecture.Application.Common; -using VerticalSliceArchitecture.Application.Common.Mappings; using VerticalSliceArchitecture.Application.Domain.Todos; using VerticalSliceArchitecture.Application.Infrastructure.Persistence; @@ -22,9 +18,7 @@ public async Task> Get() } } -public class GetTodosQuery : IRequest -{ -} +public record GetTodosQuery : IRequest; public class TodosVm { @@ -33,60 +27,21 @@ public class TodosVm public IList Lists { get; set; } = new List(); } -public class PriorityLevelDto -{ - public int Value { get; set; } +public record PriorityLevelDto(int Value, string? Name); - public string? Name { get; set; } -} - -public class TodoListDto : IMapFrom +public record TodoListDto(int Id, string? Title, string? Colour, IList Items) { public TodoListDto() + : this(default, null, null, new List()) { - Items = new List(); } - - public int Id { get; set; } - - public string? Title { get; set; } - - public string? Colour { get; set; } - - public IList Items { get; set; } } -public class TodoItemDto : IMapFrom -{ - public int Id { get; set; } - - public int ListId { get; set; } - - public string? Title { get; set; } - - public bool Done { get; set; } - - public int Priority { get; set; } - - public string? Note { get; set; } - - public void Mapping(Profile profile) - { - profile.CreateMap() - .ForMember(d => d.Priority, opt => opt.MapFrom(s => (int)s.Priority)); - } -} +public record TodoItemDto(int Id, int ListId, string? Title, bool Done, int Priority, string? Note); -internal sealed class GetTodosQueryHandler : IRequestHandler +internal sealed class GetTodosQueryHandler(ApplicationDbContext context) : IRequestHandler { - private readonly ApplicationDbContext _context; - private readonly IMapper _mapper; - - public GetTodosQueryHandler(ApplicationDbContext context, IMapper mapper) - { - _context = context; - _mapper = mapper; - } + private readonly ApplicationDbContext _context = context; public async Task Handle(GetTodosQuery request, CancellationToken cancellationToken) { @@ -94,14 +49,36 @@ public async Task Handle(GetTodosQuery request, CancellationToken cance { PriorityLevels = Enum.GetValues(typeof(PriorityLevel)) .Cast() - .Select(p => new PriorityLevelDto { Value = (int)p, Name = p.ToString() }) + .Select(p => new PriorityLevelDto((int)p, p.ToString())) .ToList(), Lists = await _context.TodoLists .AsNoTracking() - .ProjectTo(_mapper.ConfigurationProvider) .OrderBy(t => t.Title) + .Select(todoListItem => ToDto(todoListItem)) .ToListAsync(cancellationToken), }; } + + private static TodoItemDto ToDto(TodoItem todoItem) + { + var todoItemDto = new TodoItemDto(todoItem.Id, todoItem.ListId, todoItem.Title, todoItem.Done, (int)todoItem.Priority, todoItem.Note); + + return todoItemDto; + } + + private static TodoListDto ToDto(TodoList todoList) + { + var todoListDto = new TodoListDto + { + Id = todoList.Id, + Title = todoList.Title, + Colour = todoList.Colour, + Items = todoList.Items + .Select(item => ToDto(item)) + .ToList(), + }; + + return todoListDto; + } } \ No newline at end of file diff --git a/src/Application/Infrastructure/Files/CsvFileBuilder.cs b/src/Application/Infrastructure/Files/CsvFileBuilder.cs index 5dbcea6..d423695 100644 --- a/src/Application/Infrastructure/Files/CsvFileBuilder.cs +++ b/src/Application/Infrastructure/Files/CsvFileBuilder.cs @@ -4,7 +4,6 @@ using VerticalSliceArchitecture.Application.Common.Interfaces; using VerticalSliceArchitecture.Application.Domain.Todos; -using VerticalSliceArchitecture.Application.Infrastructure.Files.Maps; namespace VerticalSliceArchitecture.Application.Infrastructure.Files; diff --git a/src/Application/Infrastructure/Files/Maps/TodoItemRecordMap.cs b/src/Application/Infrastructure/Files/TodoItemRecordMap.cs similarity index 93% rename from src/Application/Infrastructure/Files/Maps/TodoItemRecordMap.cs rename to src/Application/Infrastructure/Files/TodoItemRecordMap.cs index d098454..5c2d2d2 100644 --- a/src/Application/Infrastructure/Files/Maps/TodoItemRecordMap.cs +++ b/src/Application/Infrastructure/Files/TodoItemRecordMap.cs @@ -4,7 +4,7 @@ using VerticalSliceArchitecture.Application.Domain.Todos; -namespace VerticalSliceArchitecture.Application.Infrastructure.Files.Maps; +namespace VerticalSliceArchitecture.Application.Infrastructure.Files; public class TodoItemRecordMap : ClassMap { diff --git a/tests/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs b/tests/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs index 19518df..130b4d4 100644 --- a/tests/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs +++ b/tests/Application.UnitTests/Common/Behaviours/RequestLoggerTests.cs @@ -26,7 +26,7 @@ public async Task ShouldCallGetUserNameAsyncOnceIfAuthenticated() var requestLogger = new LoggingBehaviour(_logger.Object, _currentUserService.Object); - await requestLogger.Process(new CreateTodoItemCommand { ListId = 1, Title = "title" }, CancellationToken.None); + await requestLogger.Process(new CreateTodoItemCommand(1, "title"), CancellationToken.None); } [Fact] @@ -34,6 +34,6 @@ public async Task ShouldNotCallGetUserNameAsyncOnceIfUnauthenticated() { var requestLogger = new LoggingBehaviour(_logger.Object, _currentUserService.Object); - await requestLogger.Process(new CreateTodoItemCommand { ListId = 1, Title = "title" }, CancellationToken.None); + await requestLogger.Process(new CreateTodoItemCommand(1, "title"), CancellationToken.None); } } \ No newline at end of file diff --git a/tests/Application.UnitTests/Common/Mappings/MappingTests.cs b/tests/Application.UnitTests/Common/Mappings/MappingTests.cs deleted file mode 100644 index af0918c..0000000 --- a/tests/Application.UnitTests/Common/Mappings/MappingTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -using AutoMapper; - -using VerticalSliceArchitecture.Application.Common.Mappings; -using VerticalSliceArchitecture.Application.Domain.Todos; -using VerticalSliceArchitecture.Application.Features.TodoLists; - -namespace VerticalSliceArchitecture.Application.UnitTests.Common.Mappings; - -public class MappingTests -{ - private readonly IConfigurationProvider _configuration; - private readonly IMapper _mapper; - - public MappingTests() - { - _configuration = new MapperConfiguration(config => - config.AddProfile()); - - _mapper = _configuration.CreateMapper(); - } - - [Fact] - public void ShouldHaveValidConfiguration() - { - _configuration.AssertConfigurationIsValid(); - } - - [Theory] - [InlineData(typeof(TodoList), typeof(TodoListDto))] - [InlineData(typeof(TodoItem), typeof(TodoItemDto))] - public void ShouldSupportMappingFromSourceToDestination(Type source, Type destination) - { - var instance = GetInstanceOf(source); - - _mapper.Map(instance, source, destination); - } - - private static object GetInstanceOf(Type type) - { - return Activator.CreateInstance(type)!; - } -} \ No newline at end of file