From b22deda1721250cd10fb61ad928791247772bb50 Mon Sep 17 00:00:00 2001 From: Mezdelex Date: Mon, 11 Nov 2024 00:24:05 +0100 Subject: [PATCH] refactor(features,naming,structure): restructured features folders together with commands, queries and domainevents; refactor of tests based on new changes; added global usings as well --- .env | 6 +- docker-compose.yml | 12 +- .../DeleteAsync/DeleteCategoryCommand.cs | 5 - .../DeleteCategoryCommandHandler.cs | 23 ---- .../GetAllAsync/GetAllCategoriesQuery.cs | 7 - .../GetAllCategoriesQueryHandler.cs | 48 ------- .../Categories/GetAsync/GetCategoryQuery.cs | 6 - .../GetAsync/GetCategoryQueryHandler.cs | 36 ------ .../PatchAsync/PatchCategoryCommand.cs | 5 - .../PatchAsync/PatchCategoryCommandHandler.cs | 48 ------- .../PatchCategoryCommandValidator.cs | 47 ------- .../PatchAsync/PatchedCategoryEvent.cs | 3 - .../PatchedCategoryEventConsumer.cs | 21 --- .../PostAsync/PostCategoryCommand.cs | 5 - .../PostAsync/PostCategoryCommandHandler.cs | 48 ------- .../PostAsync/PostCategoryCommandValidator.cs | 39 ------ .../PostAsync/PostedCategoryEvent.cs | 3 - .../PostAsync/PostedCategoryEventConsumer.cs | 21 --- .../Categories/Shared/CategoryDTO.cs | 5 - .../Categories/Shared/ExpenseDTO.cs | 5 - .../Contexts/IApplicationDbContext.cs | 6 +- .../DeleteAsync/DeleteExpenseCommand.cs | 5 - .../DeleteExpenseCommandHandler.cs | 23 ---- .../GetAllAsync/GetAllExpensesQuery.cs | 7 - .../GetAllAsync/GetAllExpensesQueryHandler.cs | 46 ------- .../Expenses/GetAsync/GetExpenseQuery.cs | 6 - .../GetAsync/GetExpenseQueryHandler.cs | 34 ----- .../PatchAsync/PatchExpenseCommand.cs | 11 -- .../PatchAsync/PatchExpenseCommandHandler.cs | 56 -------- .../PatchExpenseCommandValidator.cs | 57 --------- .../PatchAsync/PatchedExpenseEvent.cs | 9 -- .../PatchAsync/PatchedExpenseEventConsumer.cs | 21 --- .../Expenses/PostAsync/PostExpenseCommand.cs | 6 - .../PostAsync/PostExpenseCommandHandler.cs | 56 -------- .../PostAsync/PostExpenseCommandValidator.cs | 51 -------- .../Expenses/PostAsync/PostedExpenseEvent.cs | 9 -- .../PostAsync/PostedExpenseEventConsumer.cs | 21 --- src/Application/Expenses/Shared/ExpenseDTO.cs | 3 - .../Extensions/ApplicationExtensions.cs | 3 - .../Commands/DeleteCategoryCommand.cs | 32 +++++ .../Features/Commands/DeleteExpenseCommand.cs | 32 +++++ .../Features/Commands/PatchCategoryCommand.cs | 92 ++++++++++++++ .../Features/Commands/PatchExpenseCommand.cs | 120 ++++++++++++++++++ .../Features/Commands/PostCategoryCommand.cs | 82 ++++++++++++ .../Features/Commands/PostExpenseCommand.cs | 113 +++++++++++++++++ .../DomainEvents/PatchedCategoryEvent.cs | 21 +++ .../DomainEvents/PatchedExpenseEvent.cs | 27 ++++ .../DomainEvents/PostedCategoryEvent.cs | 21 +++ .../DomainEvents/PostedExpenseEvent.cs | 27 ++++ .../Features/Queries/GetAllCategoriesQuery.cs | 46 +++++++ .../Features/Queries/GetAllExpensesQuery.cs | 44 +++++++ .../Features/Queries/GetCategoryQuery.cs | 44 +++++++ .../Features/Queries/GetExpenseQuery.cs | 44 +++++++ .../Features/Shared/CategoryDTO.cs | 3 + src/Application/Features/Shared/ExpenseDTO.cs | 3 + src/Application/GlobalUsings.cs | 17 +++ .../Repositories}/ICategoriesRepository.cs | 8 +- .../Repositories}/IExpensesRepository.cs | 8 +- src/Dockerfile | 1 - .../{Categories => Entities}/Category.cs | 6 +- src/Domain/{Expenses => Entities}/Expense.cs | 8 +- .../CategoryExceptions.cs} | 2 +- .../ExpensesExceptions.cs | 2 +- src/Domain/Extensions/Collections.cs | 2 - src/Domain/GlobalUsings.cs | 5 + src/Domain/Identity/ApplicationUser.cs | 3 + src/Domain/Users/User.cs | 5 - src/Infrastructure/Cache/RedisCache.cs | 18 ++- .../Configurations/CategoriesConfiguration.cs | 9 -- .../Configurations/ExpensesConfiguration.cs | 14 +- .../Contexts/ApplicationDbContext.cs | 9 +- .../Extensions/MigrationExtension.cs | 7 +- src/Infrastructure/GlobalUsings.cs | 18 ++- .../RabbitMQ/RabbitMQEventBus.cs | 5 +- .../RabbitMQ/RabbitMQSettings.cs | 8 -- .../20240727192023_categories_expenses.cs | 64 ---------- ....cs => 20241110225607_Initial.Designer.cs} | 24 ++-- ...5_initial.cs => 20241110225607_Initial.cs} | 47 ++++++- ...=> 20241110231659_NonNullable.Designer.cs} | 80 +++++++++++- .../Migrations/20241110231659_NonNullable.cs | 22 ++++ .../ApplicationDbContextModelSnapshot.cs | 20 +-- src/Infrastructure/Persistence/UnitOfWork.cs | 3 - .../Repositories/CategoriesRepository.cs | 3 - .../Repositories/ExpensesRepository.cs | 5 +- .../Endpoints/CategoriesEndpoints.cs | 12 -- .../Endpoints/ExpensesEndpoints.cs | 12 -- src/Presentation/GlobalUsings.cs | 8 ++ src/WebApi/GlobalUsings.cs | 8 ++ src/WebApi/Program.cs | 13 +- .../DeleteCategoryCommandHandlerTests.cs | 5 - .../PatchCategoryCommandHandlerTests.cs | 8 -- .../PostCategoryCommandHandlerTests.cs | 8 -- .../GetAllCategoriesQueryHandlerTests.cs | 14 +- .../Queries}/GetCategoryQueryHandlerTests.cs | 7 - tests/UnitTests/GlobalUsings.cs | 24 ++++ .../UnitTests/Shared/DbSetMock.cs | 9 +- .../UnitTests.csproj} | 10 +- 97 files changed, 1054 insertions(+), 1081 deletions(-) delete mode 100644 src/Application/Categories/DeleteAsync/DeleteCategoryCommand.cs delete mode 100644 src/Application/Categories/DeleteAsync/DeleteCategoryCommandHandler.cs delete mode 100644 src/Application/Categories/GetAllAsync/GetAllCategoriesQuery.cs delete mode 100644 src/Application/Categories/GetAllAsync/GetAllCategoriesQueryHandler.cs delete mode 100644 src/Application/Categories/GetAsync/GetCategoryQuery.cs delete mode 100644 src/Application/Categories/GetAsync/GetCategoryQueryHandler.cs delete mode 100644 src/Application/Categories/PatchAsync/PatchCategoryCommand.cs delete mode 100644 src/Application/Categories/PatchAsync/PatchCategoryCommandHandler.cs delete mode 100644 src/Application/Categories/PatchAsync/PatchCategoryCommandValidator.cs delete mode 100644 src/Application/Categories/PatchAsync/PatchedCategoryEvent.cs delete mode 100644 src/Application/Categories/PatchAsync/PatchedCategoryEventConsumer.cs delete mode 100644 src/Application/Categories/PostAsync/PostCategoryCommand.cs delete mode 100644 src/Application/Categories/PostAsync/PostCategoryCommandHandler.cs delete mode 100644 src/Application/Categories/PostAsync/PostCategoryCommandValidator.cs delete mode 100644 src/Application/Categories/PostAsync/PostedCategoryEvent.cs delete mode 100644 src/Application/Categories/PostAsync/PostedCategoryEventConsumer.cs delete mode 100644 src/Application/Categories/Shared/CategoryDTO.cs delete mode 100644 src/Application/Categories/Shared/ExpenseDTO.cs delete mode 100644 src/Application/Expenses/DeleteAsync/DeleteExpenseCommand.cs delete mode 100644 src/Application/Expenses/DeleteAsync/DeleteExpenseCommandHandler.cs delete mode 100644 src/Application/Expenses/GetAllAsync/GetAllExpensesQuery.cs delete mode 100644 src/Application/Expenses/GetAllAsync/GetAllExpensesQueryHandler.cs delete mode 100644 src/Application/Expenses/GetAsync/GetExpenseQuery.cs delete mode 100644 src/Application/Expenses/GetAsync/GetExpenseQueryHandler.cs delete mode 100644 src/Application/Expenses/PatchAsync/PatchExpenseCommand.cs delete mode 100644 src/Application/Expenses/PatchAsync/PatchExpenseCommandHandler.cs delete mode 100644 src/Application/Expenses/PatchAsync/PatchExpenseCommandValidator.cs delete mode 100644 src/Application/Expenses/PatchAsync/PatchedExpenseEvent.cs delete mode 100644 src/Application/Expenses/PatchAsync/PatchedExpenseEventConsumer.cs delete mode 100644 src/Application/Expenses/PostAsync/PostExpenseCommand.cs delete mode 100644 src/Application/Expenses/PostAsync/PostExpenseCommandHandler.cs delete mode 100644 src/Application/Expenses/PostAsync/PostExpenseCommandValidator.cs delete mode 100644 src/Application/Expenses/PostAsync/PostedExpenseEvent.cs delete mode 100644 src/Application/Expenses/PostAsync/PostedExpenseEventConsumer.cs delete mode 100644 src/Application/Expenses/Shared/ExpenseDTO.cs create mode 100644 src/Application/Features/Commands/DeleteCategoryCommand.cs create mode 100644 src/Application/Features/Commands/DeleteExpenseCommand.cs create mode 100644 src/Application/Features/Commands/PatchCategoryCommand.cs create mode 100644 src/Application/Features/Commands/PatchExpenseCommand.cs create mode 100644 src/Application/Features/Commands/PostCategoryCommand.cs create mode 100644 src/Application/Features/Commands/PostExpenseCommand.cs create mode 100644 src/Application/Features/DomainEvents/PatchedCategoryEvent.cs create mode 100644 src/Application/Features/DomainEvents/PatchedExpenseEvent.cs create mode 100644 src/Application/Features/DomainEvents/PostedCategoryEvent.cs create mode 100644 src/Application/Features/DomainEvents/PostedExpenseEvent.cs create mode 100644 src/Application/Features/Queries/GetAllCategoriesQuery.cs create mode 100644 src/Application/Features/Queries/GetAllExpensesQuery.cs create mode 100644 src/Application/Features/Queries/GetCategoryQuery.cs create mode 100644 src/Application/Features/Queries/GetExpenseQuery.cs create mode 100644 src/Application/Features/Shared/CategoryDTO.cs create mode 100644 src/Application/Features/Shared/ExpenseDTO.cs create mode 100644 src/Application/GlobalUsings.cs rename src/{Domain/Categories => Application/Repositories}/ICategoriesRepository.cs (57%) rename src/{Domain/Expenses => Application/Repositories}/IExpensesRepository.cs (65%) rename src/Domain/{Categories => Entities}/Category.cs (72%) rename src/Domain/{Expenses => Entities}/Expense.cs (80%) rename src/Domain/{Categories/CategoriesExceptions.cs => Exceptions/CategoryExceptions.cs} (85%) rename src/Domain/{Expenses => Exceptions}/ExpensesExceptions.cs (85%) create mode 100644 src/Domain/GlobalUsings.cs create mode 100644 src/Domain/Identity/ApplicationUser.cs delete mode 100644 src/Domain/Users/User.cs delete mode 100644 src/Infrastructure/MessageBrokers/RabbitMQ/RabbitMQSettings.cs delete mode 100644 src/Infrastructure/Migrations/20240727192023_categories_expenses.cs rename src/Infrastructure/Migrations/{20240727192023_categories_expenses.Designer.cs => 20241110225607_Initial.Designer.cs} (94%) rename src/Infrastructure/Migrations/{20240727150405_initial.cs => 20241110225607_Initial.cs} (83%) rename src/Infrastructure/Migrations/{20240727150405_initial.Designer.cs => 20241110231659_NonNullable.Designer.cs} (78%) create mode 100644 src/Infrastructure/Migrations/20241110231659_NonNullable.cs create mode 100644 src/Presentation/GlobalUsings.cs create mode 100644 src/WebApi/GlobalUsings.cs rename tests/{Application/UnitTests/Categories/DeleteAsync => UnitTests/Features/Commands}/DeleteCategoryCommandHandlerTests.cs (91%) rename tests/{Application/UnitTests/Categories/PatchAsync => UnitTests/Features/Commands}/PatchCategoryCommandHandlerTests.cs (90%) rename tests/{Application/UnitTests/Categories/PostAsync => UnitTests/Features/Commands}/PostCategoryCommandHandlerTests.cs (90%) rename tests/{Application/UnitTests/Categories/GetAllAsync => UnitTests/Features/Queries}/GetAllCategoriesQueryHandlerTests.cs (90%) rename tests/{Application/UnitTests/Categories/GetAsync => UnitTests/Features/Queries}/GetCategoryQueryHandlerTests.cs (92%) create mode 100644 tests/UnitTests/GlobalUsings.cs rename tests/{Application => }/UnitTests/Shared/DbSetMock.cs (91%) rename tests/{Application/Application.Tests.csproj => UnitTests/UnitTests.csproj} (79%) diff --git a/.env b/.env index 0518e79..c65ac47 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ -DATABASE=cleantemplate -USERNAME=mezdelex -PASSWORD=LapGMhZn55d3omG2jlLZ +APP_DATABASE=cleantemplate +APP_USERNAME=mezdelex +APP_PASSWORD=LapGMhZn55d3omG2jlLZ diff --git a/docker-compose.yml b/docker-compose.yml index 0d9d473..0ea09aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,8 @@ services: rabbitmq: image: rabbitmq:management environment: - RABBITMQ_DEFAULT_USER: ${USERNAME} - RABBITMQ_DEFAULT_PASS: ${PASSWORD} + RABBITMQ_DEFAULT_USER: ${APP_USERNAME} + RABBITMQ_DEFAULT_PASS: ${APP_PASSWORD} healthcheck: test: rabbitmq-diagnostics -q ping interval: 30s @@ -29,7 +29,7 @@ services: image: mcr.microsoft.com/mssql/server environment: ACCEPT_EULA: Y - MSSQL_SA_PASSWORD: ${PASSWORD} + MSSQL_SA_PASSWORD: ${APP_PASSWORD} ports: - "1433:1433" restart: always @@ -50,8 +50,8 @@ services: condition: service_started environment: ASPNETCORE_HTTP_PORTS: 8000 - DATABASE: ${DATABASE} - USERNAME: ${USERNAME} - PASSWORD: ${PASSWORD} + DATABASE: ${APP_DATABASE} + USERNAME: ${APP_USERNAME} + PASSWORD: ${APP_PASSWORD} ports: - "8000:8000" diff --git a/src/Application/Categories/DeleteAsync/DeleteCategoryCommand.cs b/src/Application/Categories/DeleteAsync/DeleteCategoryCommand.cs deleted file mode 100644 index f3970b3..0000000 --- a/src/Application/Categories/DeleteAsync/DeleteCategoryCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -using MediatR; - -namespace Application.Categories.DeleteAsync; - -public record DeleteCategoryCommand(Guid id) : IRequest; diff --git a/src/Application/Categories/DeleteAsync/DeleteCategoryCommandHandler.cs b/src/Application/Categories/DeleteAsync/DeleteCategoryCommandHandler.cs deleted file mode 100644 index a97d15d..0000000 --- a/src/Application/Categories/DeleteAsync/DeleteCategoryCommandHandler.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Domain.Categories; -using Domain.Persistence; -using MediatR; - -namespace Application.Categories.DeleteAsync; - -public sealed class DeleteCategoryCommandHandler : IRequestHandler -{ - private readonly ICategoriesRepository _repository; - private readonly IUnitOfWork _uow; - - public DeleteCategoryCommandHandler(ICategoriesRepository repository, IUnitOfWork uow) - { - _repository = repository; - _uow = uow; - } - - public async Task Handle(DeleteCategoryCommand request, CancellationToken cancellationToken) - { - await _repository.DeleteAsync(request.id, cancellationToken); - await _uow.SaveChangesAsync(cancellationToken); - } -} \ No newline at end of file diff --git a/src/Application/Categories/GetAllAsync/GetAllCategoriesQuery.cs b/src/Application/Categories/GetAllAsync/GetAllCategoriesQuery.cs deleted file mode 100644 index 18a2f7f..0000000 --- a/src/Application/Categories/GetAllAsync/GetAllCategoriesQuery.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Application.Categories.Shared; -using MediatR; -using static Domain.Extensions.Collections.Collections; - -namespace Application.Categories.GetAllAsync; - -public record GetAllCategoriesQuery(int Page, int PageSize) : IRequest>; \ No newline at end of file diff --git a/src/Application/Categories/GetAllAsync/GetAllCategoriesQueryHandler.cs b/src/Application/Categories/GetAllAsync/GetAllCategoriesQueryHandler.cs deleted file mode 100644 index d30c236..0000000 --- a/src/Application/Categories/GetAllAsync/GetAllCategoriesQueryHandler.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Application.Categories.Shared; -using Application.Contexts; -using Domain.Cache; -using MediatR; -using Microsoft.EntityFrameworkCore; -using static Domain.Extensions.Collections.Collections; - -namespace Application.Categories.GetAllAsync; - -public sealed class GetAllCategoriesQueryHandler - : IRequestHandler> -{ - private readonly IApplicationDbContext _context; - private readonly IRedisCache _redisCache; - - public GetAllCategoriesQueryHandler(IApplicationDbContext context, IRedisCache redisCache) - { - _context = context; - _redisCache = redisCache; - } - - public async Task> Handle( - GetAllCategoriesQuery request, - CancellationToken cancellationToken - ) - { - var redisKey = $"{nameof(GetAllCategoriesQuery)}#{request.Page}#{request.PageSize}"; - var cachedGetAllCategoriesQuery = await _redisCache.GetCachedData>( - redisKey - ); - if (cachedGetAllCategoriesQuery != null) - return cachedGetAllCategoriesQuery; - - var pagedCategories = await _context - .Categories.Include(c => c.Expenses) - .OrderBy(c => c.Name) - .Select(c => new CategoryDTO(c.Id, c.Name, c.Description, c.Expenses)) - .ToPagedListAsync(request.Page, request.PageSize, cancellationToken); - - await _redisCache.SetCachedData>( - redisKey, - pagedCategories, - DateTimeOffset.Now.AddMinutes(5) - ); - - return pagedCategories; - } -} \ No newline at end of file diff --git a/src/Application/Categories/GetAsync/GetCategoryQuery.cs b/src/Application/Categories/GetAsync/GetCategoryQuery.cs deleted file mode 100644 index 30181f2..0000000 --- a/src/Application/Categories/GetAsync/GetCategoryQuery.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Application.Categories.Shared; -using MediatR; - -namespace Application.Categories.GetAsync; - -public record GetCategoryQuery(Guid id) : IRequest; diff --git a/src/Application/Categories/GetAsync/GetCategoryQueryHandler.cs b/src/Application/Categories/GetAsync/GetCategoryQueryHandler.cs deleted file mode 100644 index 2f75382..0000000 --- a/src/Application/Categories/GetAsync/GetCategoryQueryHandler.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Application.Categories.Shared; -using Application.Contexts; -using Domain.Categories; -using MediatR; -using Microsoft.EntityFrameworkCore; - -namespace Application.Categories.GetAsync; - -public class GetCategoryQueryHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - - public GetCategoryQueryHandler(IApplicationDbContext context) - { - _context = context; - } - - public async Task Handle( - GetCategoryQuery request, - CancellationToken cancellationToken - ) - { - var category = - await _context - .Categories.Include(c => c.Expenses) - .FirstOrDefaultAsync(c => c.Id == request.id, cancellationToken) - ?? throw new CategoryNotFoundException(request.id); - - return new CategoryDTO( - category.Id, - category.Name, - category.Description, - category?.Expenses - ); - } -} diff --git a/src/Application/Categories/PatchAsync/PatchCategoryCommand.cs b/src/Application/Categories/PatchAsync/PatchCategoryCommand.cs deleted file mode 100644 index 47e9fbc..0000000 --- a/src/Application/Categories/PatchAsync/PatchCategoryCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -using MediatR; - -namespace Application.Categories.PatchAsync; - -public record PatchCategoryCommand(Guid Id, string Name, string Description) : IRequest; diff --git a/src/Application/Categories/PatchAsync/PatchCategoryCommandHandler.cs b/src/Application/Categories/PatchAsync/PatchCategoryCommandHandler.cs deleted file mode 100644 index 2d816a9..0000000 --- a/src/Application/Categories/PatchAsync/PatchCategoryCommandHandler.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Application.Abstractions; -using Domain.Categories; -using Domain.Persistence; -using FluentValidation; -using MediatR; - -namespace Application.Categories.PatchAsync; - -public sealed class PatchCategoryCommandHandler : IRequestHandler -{ - private readonly IValidator _validator; - private readonly ICategoriesRepository _repository; - private readonly IUnitOfWork _uow; - private readonly IEventBus _eventBus; - - public PatchCategoryCommandHandler( - IValidator validator, - ICategoriesRepository repository, - IUnitOfWork uow, - IEventBus eventBus - ) - { - _validator = validator; - _repository = repository; - _uow = uow; - _eventBus = eventBus; - } - - public async Task Handle(PatchCategoryCommand request, CancellationToken cancellationToken) - { - var results = await _validator.ValidateAsync(request, cancellationToken); - if (!results.IsValid) - throw new ValidationException(results.ToString().Replace("\r\n", " ")); - - var categoryToPatch = new Category(request.Id, request.Name, request.Description); - - await _repository.PatchAsync(categoryToPatch, cancellationToken); - await _uow.SaveChangesAsync(cancellationToken); - await _eventBus.PublishAsync( - new PatchedCategoryEvent( - categoryToPatch.Id, - categoryToPatch.Name, - categoryToPatch.Description - ), - cancellationToken - ); - } -} diff --git a/src/Application/Categories/PatchAsync/PatchCategoryCommandValidator.cs b/src/Application/Categories/PatchAsync/PatchCategoryCommandValidator.cs deleted file mode 100644 index 75924d4..0000000 --- a/src/Application/Categories/PatchAsync/PatchCategoryCommandValidator.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Application.Messages; -using FluentValidation; - -namespace Application.Categories.PatchAsync; - -public sealed class PatchCategoryCommandValidator : AbstractValidator -{ - private readonly int NameMaxLength = 30; - private readonly int DescriptionMaxLength = 256; - - public PatchCategoryCommandValidator() - { - RuleFor(c => c.Id) - .NotEmpty() - .WithMessage( - GenericValidationMessages.ShouldNotBeEmpty(nameof(PatchCategoryCommand.Id)) - ) - .Must(id => id.GetType() == typeof(Guid)) - .WithMessage(GenericValidationMessages.ShouldBeAGuid(nameof(PatchCategoryCommand.Id))); - - RuleFor(c => c.Name) - .NotEmpty() - .WithMessage( - GenericValidationMessages.ShouldNotBeEmpty(nameof(PatchCategoryCommand.Name)) - ) - .MaximumLength(NameMaxLength) - .WithMessage( - GenericValidationMessages.ShouldNotBeLongerThan( - nameof(PatchCategoryCommand.Name), - NameMaxLength - ) - ); - - RuleFor(c => c.Description) - .NotEmpty() - .WithMessage( - GenericValidationMessages.ShouldNotBeEmpty(nameof(PatchCategoryCommand.Description)) - ) - .MaximumLength(DescriptionMaxLength) - .WithMessage( - GenericValidationMessages.ShouldNotBeLongerThan( - nameof(PatchCategoryCommand.Description), - DescriptionMaxLength - ) - ); - } -} diff --git a/src/Application/Categories/PatchAsync/PatchedCategoryEvent.cs b/src/Application/Categories/PatchAsync/PatchedCategoryEvent.cs deleted file mode 100644 index d43da87..0000000 --- a/src/Application/Categories/PatchAsync/PatchedCategoryEvent.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Application.Categories.PatchAsync; - -public record PatchedCategoryEvent(Guid Id, string Name, string Description); diff --git a/src/Application/Categories/PatchAsync/PatchedCategoryEventConsumer.cs b/src/Application/Categories/PatchAsync/PatchedCategoryEventConsumer.cs deleted file mode 100644 index 00e5411..0000000 --- a/src/Application/Categories/PatchAsync/PatchedCategoryEventConsumer.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; - -namespace Application.Categories.PatchAsync; - -public sealed class PatchedCategoryEventConsumer : IConsumer -{ - private readonly ILogger _logger; - - public PatchedCategoryEventConsumer(ILogger logger) - { - _logger = logger; - } - - public Task Consume(ConsumeContext context) - { - _logger.LogInformation("Category patched: {@Category}", context.Message); - - return Task.CompletedTask; - } -} diff --git a/src/Application/Categories/PostAsync/PostCategoryCommand.cs b/src/Application/Categories/PostAsync/PostCategoryCommand.cs deleted file mode 100644 index b8f2b18..0000000 --- a/src/Application/Categories/PostAsync/PostCategoryCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -using MediatR; - -namespace Application.Categories.PostAsync; - -public record PostCategoryCommand(string Name, string Description) : IRequest; \ No newline at end of file diff --git a/src/Application/Categories/PostAsync/PostCategoryCommandHandler.cs b/src/Application/Categories/PostAsync/PostCategoryCommandHandler.cs deleted file mode 100644 index bf60d20..0000000 --- a/src/Application/Categories/PostAsync/PostCategoryCommandHandler.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Application.Abstractions; -using Domain.Categories; -using Domain.Persistence; -using FluentValidation; -using MediatR; - -namespace Application.Categories.PostAsync; - -public sealed class PostCategoryCommandHandler : IRequestHandler -{ - private readonly IValidator _validator; - private readonly ICategoriesRepository _repository; - private readonly IUnitOfWork _uow; - private readonly IEventBus _eventBus; - - public PostCategoryCommandHandler( - IValidator validator, - ICategoriesRepository repository, - IUnitOfWork uow, - IEventBus eventBus - ) - { - _validator = validator; - _repository = repository; - _uow = uow; - _eventBus = eventBus; - } - - public async Task Handle(PostCategoryCommand request, CancellationToken cancellationToken) - { - var results = await _validator.ValidateAsync(request, cancellationToken); - if (!results.IsValid) - throw new ValidationException(results.ToString().Replace("\r\n", " ")); - - var categoryToPost = new Category(Guid.NewGuid(), request.Name, request.Description); - - await _repository.PostAsync(categoryToPost, cancellationToken); - await _uow.SaveChangesAsync(cancellationToken); - await _eventBus.PublishAsync( - new PostedCategoryEvent( - categoryToPost.Id, - categoryToPost.Name, - categoryToPost.Description - ), - cancellationToken - ); - } -} diff --git a/src/Application/Categories/PostAsync/PostCategoryCommandValidator.cs b/src/Application/Categories/PostAsync/PostCategoryCommandValidator.cs deleted file mode 100644 index 54432a2..0000000 --- a/src/Application/Categories/PostAsync/PostCategoryCommandValidator.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Application.Messages; -using FluentValidation; - -namespace Application.Categories.PostAsync; - -public sealed class PostCategoryCommandValidator : AbstractValidator -{ - private readonly int NameMaxLength = 30; - private readonly int DescriptionMaxLength = 256; - - public PostCategoryCommandValidator() - { - RuleFor(c => c.Name) - .NotEmpty() - .WithMessage( - GenericValidationMessages.ShouldNotBeEmpty(nameof(PostCategoryCommand.Name)) - ) - .MaximumLength(NameMaxLength) - .WithMessage( - GenericValidationMessages.ShouldNotBeLongerThan( - nameof(PostCategoryCommand.Name), - NameMaxLength - ) - ); - - RuleFor(c => c.Description) - .NotEmpty() - .WithMessage( - GenericValidationMessages.ShouldNotBeEmpty(nameof(PostCategoryCommand.Description)) - ) - .MaximumLength(DescriptionMaxLength) - .WithMessage( - GenericValidationMessages.ShouldNotBeLongerThan( - nameof(PostCategoryCommand.Description), - DescriptionMaxLength - ) - ); - } -} diff --git a/src/Application/Categories/PostAsync/PostedCategoryEvent.cs b/src/Application/Categories/PostAsync/PostedCategoryEvent.cs deleted file mode 100644 index 399276d..0000000 --- a/src/Application/Categories/PostAsync/PostedCategoryEvent.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Application.Categories.PostAsync; - -public record PostedCategoryEvent(Guid Id, string Name, string Description); diff --git a/src/Application/Categories/PostAsync/PostedCategoryEventConsumer.cs b/src/Application/Categories/PostAsync/PostedCategoryEventConsumer.cs deleted file mode 100644 index 854ea6d..0000000 --- a/src/Application/Categories/PostAsync/PostedCategoryEventConsumer.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; - -namespace Application.Categories.PostAsync; - -public sealed class PostedCategoryEventConsumer : IConsumer -{ - private readonly ILogger _logger; - - public PostedCategoryEventConsumer(ILogger logger) - { - _logger = logger; - } - - public Task Consume(ConsumeContext context) - { - _logger.LogInformation("Category created: {@Category}", context.Message); - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/Application/Categories/Shared/CategoryDTO.cs b/src/Application/Categories/Shared/CategoryDTO.cs deleted file mode 100644 index 00d6b1e..0000000 --- a/src/Application/Categories/Shared/CategoryDTO.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Domain.Expenses; - -namespace Application.Categories.Shared; - -public record CategoryDTO(Guid Id, string Name, string Description, List? Expenses); diff --git a/src/Application/Categories/Shared/ExpenseDTO.cs b/src/Application/Categories/Shared/ExpenseDTO.cs deleted file mode 100644 index 8c89fd7..0000000 --- a/src/Application/Categories/Shared/ExpenseDTO.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Domain.Expenses; - -namespace Application.Expenses.Shared; - -public record CategoryDTO(Guid Id, string Name, string Description, List? Expenses); \ No newline at end of file diff --git a/src/Application/Contexts/IApplicationDbContext.cs b/src/Application/Contexts/IApplicationDbContext.cs index e43388d..382cad0 100644 --- a/src/Application/Contexts/IApplicationDbContext.cs +++ b/src/Application/Contexts/IApplicationDbContext.cs @@ -1,11 +1,7 @@ -using Domain.Categories; -using Domain.Expenses; -using Microsoft.EntityFrameworkCore; - namespace Application.Contexts; public interface IApplicationDbContext { DbSet Categories { get; set; } DbSet Expenses { get; set; } -} \ No newline at end of file +} diff --git a/src/Application/Expenses/DeleteAsync/DeleteExpenseCommand.cs b/src/Application/Expenses/DeleteAsync/DeleteExpenseCommand.cs deleted file mode 100644 index a9e383c..0000000 --- a/src/Application/Expenses/DeleteAsync/DeleteExpenseCommand.cs +++ /dev/null @@ -1,5 +0,0 @@ -using MediatR; - -namespace Application.Expenses.DeleteAsync; - -public record DeleteExpenseCommand(Guid Id) : IRequest; diff --git a/src/Application/Expenses/DeleteAsync/DeleteExpenseCommandHandler.cs b/src/Application/Expenses/DeleteAsync/DeleteExpenseCommandHandler.cs deleted file mode 100644 index a027df6..0000000 --- a/src/Application/Expenses/DeleteAsync/DeleteExpenseCommandHandler.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Domain.Expenses; -using Domain.Persistence; -using MediatR; - -namespace Application.Expenses.DeleteAsync; - -public sealed class PostExpenseCommandHandler : IRequestHandler -{ - private readonly IExpensesRepository _repository; - private readonly IUnitOfWork _uow; - - public PostExpenseCommandHandler(IExpensesRepository repository, IUnitOfWork uow) - { - _repository = repository; - _uow = uow; - } - - public async Task Handle(DeleteExpenseCommand request, CancellationToken cancellationToken) - { - await _repository.DeleteAsync(request.Id, cancellationToken); - await _uow.SaveChangesAsync(cancellationToken); - } -} diff --git a/src/Application/Expenses/GetAllAsync/GetAllExpensesQuery.cs b/src/Application/Expenses/GetAllAsync/GetAllExpensesQuery.cs deleted file mode 100644 index 4547ca5..0000000 --- a/src/Application/Expenses/GetAllAsync/GetAllExpensesQuery.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Application.Shared; -using MediatR; -using static Domain.Extensions.Collections.Collections; - -namespace Application.Expenses.GetAllAsync; - -public record GetAllExpensesQuery(int Page, int PageSize) : IRequest>; \ No newline at end of file diff --git a/src/Application/Expenses/GetAllAsync/GetAllExpensesQueryHandler.cs b/src/Application/Expenses/GetAllAsync/GetAllExpensesQueryHandler.cs deleted file mode 100644 index fd263cd..0000000 --- a/src/Application/Expenses/GetAllAsync/GetAllExpensesQueryHandler.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Application.Contexts; -using Application.Shared; -using Domain.Cache; -using MediatR; -using static Domain.Extensions.Collections.Collections; - -namespace Application.Expenses.GetAllAsync; - -public sealed class GetAllExpensesQueryHandler - : IRequestHandler> -{ - private readonly IApplicationDbContext _context; - private readonly IRedisCache _redisCache; - - public GetAllExpensesQueryHandler(IApplicationDbContext context, IRedisCache redisCache) - { - _context = context; - _redisCache = redisCache; - } - - public async Task> Handle( - GetAllExpensesQuery request, - CancellationToken cancellationToken - ) - { - var redisKey = $"{nameof(GetAllExpensesQuery)}#{request.Page}#{request.PageSize}"; - var cachedGetAllExpensesQuery = await _redisCache.GetCachedData>( - redisKey - ); - if (cachedGetAllExpensesQuery != null) - return cachedGetAllExpensesQuery; - - var pagedExpenses = await _context - .Expenses.OrderBy(e => e.Name) - .Select(e => new ExpenseDTO(e.Id, e.Name, e.Description, e.Value, e.CategoryId)) - .ToPagedListAsync(request.Page, request.PageSize, cancellationToken); - - await _redisCache.SetCachedData>( - redisKey, - pagedExpenses, - DateTimeOffset.Now.AddMinutes(5) - ); - - return pagedExpenses; - } -} diff --git a/src/Application/Expenses/GetAsync/GetExpenseQuery.cs b/src/Application/Expenses/GetAsync/GetExpenseQuery.cs deleted file mode 100644 index 5ffb900..0000000 --- a/src/Application/Expenses/GetAsync/GetExpenseQuery.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Application.Shared; -using MediatR; - -namespace Application.Expenses.GetAsync; - -public record GetExpenseQuery(Guid Id) : IRequest; diff --git a/src/Application/Expenses/GetAsync/GetExpenseQueryHandler.cs b/src/Application/Expenses/GetAsync/GetExpenseQueryHandler.cs deleted file mode 100644 index a457f46..0000000 --- a/src/Application/Expenses/GetAsync/GetExpenseQueryHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Application.Contexts; -using Application.Shared; -using Domain.Expenses; -using MediatR; - -namespace Application.Expenses.GetAsync; - -public sealed class GetExpenseQueryHandler : IRequestHandler -{ - private readonly IApplicationDbContext _context; - - public GetExpenseQueryHandler(IApplicationDbContext context) - { - _context = context; - } - - public async Task Handle( - GetExpenseQuery request, - CancellationToken cancellationToken - ) - { - var expense = - await _context.Expenses.FindAsync(request.Id, cancellationToken) - ?? throw new ExpenseNotFoundException(request.Id); - - return new ExpenseDTO( - expense.Id, - expense.Name, - expense.Description, - expense.Value, - expense.CategoryId - ); - } -} diff --git a/src/Application/Expenses/PatchAsync/PatchExpenseCommand.cs b/src/Application/Expenses/PatchAsync/PatchExpenseCommand.cs deleted file mode 100644 index c846516..0000000 --- a/src/Application/Expenses/PatchAsync/PatchExpenseCommand.cs +++ /dev/null @@ -1,11 +0,0 @@ -using MediatR; - -namespace Application.Expenses.PatchAsync; - -public record PatchExpenseCommand( - Guid Id, - string Name, - string Description, - double Value, - Guid CategoryId -) : IRequest; diff --git a/src/Application/Expenses/PatchAsync/PatchExpenseCommandHandler.cs b/src/Application/Expenses/PatchAsync/PatchExpenseCommandHandler.cs deleted file mode 100644 index feeb98a..0000000 --- a/src/Application/Expenses/PatchAsync/PatchExpenseCommandHandler.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Application.Abstractions; -using Domain.Expenses; -using Domain.Persistence; -using FluentValidation; -using MediatR; - -namespace Application.Expenses.PatchAsync; - -public sealed class PatchExpenseCommandHandler : IRequestHandler -{ - private readonly IValidator _validator; - private readonly IExpensesRepository _repository; - private readonly IUnitOfWork _uow; - private readonly IEventBus _eventBus; - - public PatchExpenseCommandHandler( - IValidator validator, - IExpensesRepository repository, - IUnitOfWork uow, - IEventBus eventBus - ) - { - _validator = validator; - _repository = repository; - _uow = uow; - _eventBus = eventBus; - } - - public async Task Handle(PatchExpenseCommand request, CancellationToken cancellationToken) - { - var results = await _validator.ValidateAsync(request, cancellationToken); - if (!results.IsValid) - throw new ValidationException(results.ToString().Replace("\r\n", " ")); - - var expenseToPatch = new Expense( - request.Id, - request.Name, - request.Description, - request.Value, - request.CategoryId - ); - - await _repository.PatchAsync(expenseToPatch, cancellationToken); - await _uow.SaveChangesAsync(cancellationToken); - await _eventBus.PublishAsync( - new PatchedExpenseEvent( - expenseToPatch.Id, - expenseToPatch.Name, - expenseToPatch.Description, - expenseToPatch.Value, - expenseToPatch.CategoryId - ), - cancellationToken - ); - } -} diff --git a/src/Application/Expenses/PatchAsync/PatchExpenseCommandValidator.cs b/src/Application/Expenses/PatchAsync/PatchExpenseCommandValidator.cs deleted file mode 100644 index cc1c227..0000000 --- a/src/Application/Expenses/PatchAsync/PatchExpenseCommandValidator.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Application.Messages; -using FluentValidation; - -namespace Application.Expenses.PatchAsync; - -public sealed class PatchExpenseCommandValidator : AbstractValidator -{ - private readonly int NameMaxLength = 30; - private readonly int DescriptionMaxLength = 256; - - public PatchExpenseCommandValidator() - { - RuleFor(c => c.Id) - .NotEmpty() - .WithMessage(GenericValidationMessages.ShouldNotBeEmpty(nameof(PatchExpenseCommand.Id))) - .Must(id => id.GetType() == typeof(Guid)) - .WithMessage(GenericValidationMessages.ShouldBeAGuid(nameof(PatchExpenseCommand.Id))); - - RuleFor(c => c.Name) - .NotEmpty() - .WithMessage( - GenericValidationMessages.ShouldNotBeEmpty(nameof(PatchExpenseCommand.Name)) - ) - .MaximumLength(NameMaxLength) - .WithMessage( - GenericValidationMessages.ShouldNotBeLongerThan( - nameof(PatchExpenseCommand.Name), - NameMaxLength - ) - ); - - RuleFor(c => c.Description) - .NotEmpty() - .WithMessage( - GenericValidationMessages.ShouldNotBeEmpty(nameof(PatchExpenseCommand.Description)) - ) - .MaximumLength(DescriptionMaxLength) - .WithMessage( - GenericValidationMessages.ShouldNotBeLongerThan( - nameof(PatchExpenseCommand.Description), - DescriptionMaxLength - ) - ); - - RuleFor(c => c.Value) - .NotEmpty() - .WithMessage( - GenericValidationMessages.ShouldNotBeEmpty(nameof(PatchExpenseCommand.Value)) - ); - - RuleFor(c => c.CategoryId) - .NotEmpty() - .WithMessage( - GenericValidationMessages.ShouldNotBeEmpty(nameof(PatchExpenseCommand.CategoryId)) - ); - } -} diff --git a/src/Application/Expenses/PatchAsync/PatchedExpenseEvent.cs b/src/Application/Expenses/PatchAsync/PatchedExpenseEvent.cs deleted file mode 100644 index da521e9..0000000 --- a/src/Application/Expenses/PatchAsync/PatchedExpenseEvent.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Application.Expenses.PatchAsync; - -public record PatchedExpenseEvent( - Guid Id, - string Name, - string Description, - double Value, - Guid CategoryId -); \ No newline at end of file diff --git a/src/Application/Expenses/PatchAsync/PatchedExpenseEventConsumer.cs b/src/Application/Expenses/PatchAsync/PatchedExpenseEventConsumer.cs deleted file mode 100644 index 60a04b4..0000000 --- a/src/Application/Expenses/PatchAsync/PatchedExpenseEventConsumer.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; - -namespace Application.Expenses.PatchAsync; - -public sealed class PatchedExpenseEventConsumer : IConsumer -{ - private readonly ILogger _logger; - - public PatchedExpenseEventConsumer(ILogger logger) - { - _logger = logger; - } - - public Task Consume(ConsumeContext context) - { - _logger.LogInformation("Expense patched: {@Expense}", context.Message); - - return Task.CompletedTask; - } -} diff --git a/src/Application/Expenses/PostAsync/PostExpenseCommand.cs b/src/Application/Expenses/PostAsync/PostExpenseCommand.cs deleted file mode 100644 index 9500f81..0000000 --- a/src/Application/Expenses/PostAsync/PostExpenseCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -using MediatR; - -namespace Application.Expenses.PostAsync; - -public record PostExpenseCommand(string Name, string Description, double Value, Guid CategoryId) - : IRequest; diff --git a/src/Application/Expenses/PostAsync/PostExpenseCommandHandler.cs b/src/Application/Expenses/PostAsync/PostExpenseCommandHandler.cs deleted file mode 100644 index 34022d9..0000000 --- a/src/Application/Expenses/PostAsync/PostExpenseCommandHandler.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Application.Abstractions; -using Domain.Expenses; -using Domain.Persistence; -using FluentValidation; -using MediatR; - -namespace Application.Expenses.PostAsync; - -public sealed class PostExpenseCommandHandler : IRequestHandler -{ - private readonly IValidator _validator; - private readonly IExpensesRepository _repository; - private readonly IUnitOfWork _uow; - private readonly IEventBus _eventBus; - - public PostExpenseCommandHandler( - IValidator validator, - IExpensesRepository repository, - IUnitOfWork uow, - IEventBus eventBus - ) - { - _validator = validator; - _repository = repository; - _uow = uow; - _eventBus = eventBus; - } - - public async Task Handle(PostExpenseCommand request, CancellationToken cancellationToken) - { - var results = await _validator.ValidateAsync(request, cancellationToken); - if (!results.IsValid) - throw new ValidationException(results.ToString().Replace("\r\n", " ")); - - var expenseToPost = new Expense( - Guid.NewGuid(), - request.Name, - request.Description, - request.Value, - request.CategoryId - ); - - await _repository.PostAsync(expenseToPost, cancellationToken); - await _uow.SaveChangesAsync(cancellationToken); - await _eventBus.PublishAsync( - new PostedExpenseEvent( - expenseToPost.Id, - expenseToPost.Name, - expenseToPost.Description, - expenseToPost.Value, - expenseToPost.CategoryId - ), - cancellationToken - ); - } -} \ No newline at end of file diff --git a/src/Application/Expenses/PostAsync/PostExpenseCommandValidator.cs b/src/Application/Expenses/PostAsync/PostExpenseCommandValidator.cs deleted file mode 100644 index 94ffc4b..0000000 --- a/src/Application/Expenses/PostAsync/PostExpenseCommandValidator.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Application.Messages; -using FluentValidation; - -namespace Application.Expenses.PostAsync; - -public sealed class PostExpenseCommandValidator : AbstractValidator -{ - private readonly int NameMaxLength = 30; - private readonly int DescriptionMaxLength = 256; - - public PostExpenseCommandValidator() - { - RuleFor(c => c.Name) - .NotEmpty() - .WithMessage( - GenericValidationMessages.ShouldNotBeEmpty(nameof(PostExpenseCommand.Name)) - ) - .MaximumLength(NameMaxLength) - .WithMessage( - GenericValidationMessages.ShouldNotBeLongerThan( - nameof(PostExpenseCommand.Name), - NameMaxLength - ) - ); - - RuleFor(c => c.Description) - .NotEmpty() - .WithMessage( - GenericValidationMessages.ShouldNotBeEmpty(nameof(PostExpenseCommand.Description)) - ) - .MaximumLength(DescriptionMaxLength) - .WithMessage( - GenericValidationMessages.ShouldNotBeLongerThan( - nameof(PostExpenseCommand.Description), - DescriptionMaxLength - ) - ); - - RuleFor(c => c.Value) - .NotEmpty() - .WithMessage( - GenericValidationMessages.ShouldNotBeEmpty(nameof(PostExpenseCommand.Value)) - ); - - RuleFor(c => c.CategoryId) - .NotEmpty() - .WithMessage( - GenericValidationMessages.ShouldNotBeEmpty(nameof(PostExpenseCommand.CategoryId)) - ); - } -} \ No newline at end of file diff --git a/src/Application/Expenses/PostAsync/PostedExpenseEvent.cs b/src/Application/Expenses/PostAsync/PostedExpenseEvent.cs deleted file mode 100644 index d3dfed5..0000000 --- a/src/Application/Expenses/PostAsync/PostedExpenseEvent.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Application.Expenses.PostAsync; - -public record PostedExpenseEvent( - Guid Id, - string Name, - string Description, - double Value, - Guid CategoryId -); diff --git a/src/Application/Expenses/PostAsync/PostedExpenseEventConsumer.cs b/src/Application/Expenses/PostAsync/PostedExpenseEventConsumer.cs deleted file mode 100644 index 5a5d2f8..0000000 --- a/src/Application/Expenses/PostAsync/PostedExpenseEventConsumer.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MassTransit; -using Microsoft.Extensions.Logging; - -namespace Application.Expenses.PostAsync; - -public sealed class PostedExpenseEventConsumer : IConsumer -{ - private readonly ILogger _logger; - - public PostedExpenseEventConsumer(ILogger logger) - { - _logger = logger; - } - - public Task Consume(ConsumeContext context) - { - _logger.LogInformation("Expense posted: {@Expense}", context.Message); - - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/src/Application/Expenses/Shared/ExpenseDTO.cs b/src/Application/Expenses/Shared/ExpenseDTO.cs deleted file mode 100644 index b8caf86..0000000 --- a/src/Application/Expenses/Shared/ExpenseDTO.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Application.Shared; - -public record ExpenseDTO(Guid id, string Name, string Description, double Value, Guid CategoryId); diff --git a/src/Application/Extensions/ApplicationExtensions.cs b/src/Application/Extensions/ApplicationExtensions.cs index e144c41..4c56b01 100644 --- a/src/Application/Extensions/ApplicationExtensions.cs +++ b/src/Application/Extensions/ApplicationExtensions.cs @@ -1,6 +1,3 @@ -using FluentValidation; -using Microsoft.Extensions.DependencyInjection; - namespace Application.Extensions; public static class ApplicationExtension diff --git a/src/Application/Features/Commands/DeleteCategoryCommand.cs b/src/Application/Features/Commands/DeleteCategoryCommand.cs new file mode 100644 index 0000000..ca1e24b --- /dev/null +++ b/src/Application/Features/Commands/DeleteCategoryCommand.cs @@ -0,0 +1,32 @@ +namespace Application.Features.Commands; + +public sealed record DeleteCategoryCommand(Guid Id) : IRequest +{ + public sealed class DeleteCategoryCommandHandler : IRequestHandler + { + private readonly ICategoriesRepository _repository; + private readonly IUnitOfWork _uow; + + public DeleteCategoryCommandHandler(ICategoriesRepository repository, IUnitOfWork uow) + { + _repository = repository; + _uow = uow; + } + + public async Task Handle(DeleteCategoryCommand request, CancellationToken cancellationToken) + { + await _repository.DeleteAsync(request.Id, cancellationToken); + await _uow.SaveChangesAsync(cancellationToken); + } + } + + public sealed class DeleteCategoryCommandValidator : AbstractValidator + { + public DeleteCategoryCommandValidator() + { + RuleFor(x => x.Id) + .NotEmpty() + .WithMessage(GenericValidationMessages.ShouldNotBeEmpty("Id")); + } + } +} diff --git a/src/Application/Features/Commands/DeleteExpenseCommand.cs b/src/Application/Features/Commands/DeleteExpenseCommand.cs new file mode 100644 index 0000000..5509ca4 --- /dev/null +++ b/src/Application/Features/Commands/DeleteExpenseCommand.cs @@ -0,0 +1,32 @@ +namespace Application.Features.Commands; + +public record DeleteExpenseCommand(Guid Id) : IRequest +{ + public sealed class DeleteExpenseCommandHandler : IRequestHandler + { + private readonly IExpensesRepository _repository; + private readonly IUnitOfWork _uow; + + public DeleteExpenseCommandHandler(IExpensesRepository repository, IUnitOfWork uow) + { + _repository = repository; + _uow = uow; + } + + public async Task Handle(DeleteExpenseCommand request, CancellationToken cancellationToken) + { + await _repository.DeleteAsync(request.Id, cancellationToken); + await _uow.SaveChangesAsync(cancellationToken); + } + } + + public sealed class DeleteExpenseCommandValidator : AbstractValidator + { + public DeleteExpenseCommandValidator() + { + RuleFor(x => x.Id) + .NotEmpty() + .WithMessage(GenericValidationMessages.ShouldNotBeEmpty("Id")); + } + } +} diff --git a/src/Application/Features/Commands/PatchCategoryCommand.cs b/src/Application/Features/Commands/PatchCategoryCommand.cs new file mode 100644 index 0000000..ee0d67e --- /dev/null +++ b/src/Application/Features/Commands/PatchCategoryCommand.cs @@ -0,0 +1,92 @@ +namespace Application.Features.Commands; + +public sealed record PatchCategoryCommand(Guid Id, string Name, string Description) : IRequest +{ + public sealed class PatchCategoryCommandHandler : IRequestHandler + { + private readonly IValidator _validator; + private readonly ICategoriesRepository _repository; + private readonly IUnitOfWork _uow; + private readonly IEventBus _eventBus; + + public PatchCategoryCommandHandler( + IValidator validator, + ICategoriesRepository repository, + IUnitOfWork uow, + IEventBus eventBus + ) + { + _validator = validator; + _repository = repository; + _uow = uow; + _eventBus = eventBus; + } + + public async Task Handle(PatchCategoryCommand request, CancellationToken cancellationToken) + { + var results = await _validator.ValidateAsync(request, cancellationToken); + if (!results.IsValid) + throw new ValidationException(results.ToString().Replace("\r\n", " ")); + + var categoryToPatch = new Category(request.Id, request.Name, request.Description); + + await _repository.PatchAsync(categoryToPatch, cancellationToken); + await _uow.SaveChangesAsync(cancellationToken); + await _eventBus.PublishAsync( + new PatchedCategoryEvent( + categoryToPatch.Id, + categoryToPatch.Name, + categoryToPatch.Description + ), + cancellationToken + ); + } + } + + public sealed class PatchCategoryCommandValidator : AbstractValidator + { + private readonly int NameMaxLength = 30; + private readonly int DescriptionMaxLength = 256; + + public PatchCategoryCommandValidator() + { + RuleFor(c => c.Id) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty(nameof(PatchCategoryCommand.Id)) + ) + .Must(id => id.GetType() == typeof(Guid)) + .WithMessage( + GenericValidationMessages.ShouldBeAGuid(nameof(PatchCategoryCommand.Id)) + ); + + RuleFor(c => c.Name) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty(nameof(PatchCategoryCommand.Name)) + ) + .MaximumLength(NameMaxLength) + .WithMessage( + GenericValidationMessages.ShouldNotBeLongerThan( + nameof(PatchCategoryCommand.Name), + NameMaxLength + ) + ); + + RuleFor(c => c.Description) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty( + nameof(PatchCategoryCommand.Description) + ) + ) + .MaximumLength(DescriptionMaxLength) + .WithMessage( + GenericValidationMessages.ShouldNotBeLongerThan( + nameof(PatchCategoryCommand.Description), + DescriptionMaxLength + ) + ); + } + } +} diff --git a/src/Application/Features/Commands/PatchExpenseCommand.cs b/src/Application/Features/Commands/PatchExpenseCommand.cs new file mode 100644 index 0000000..acb4abe --- /dev/null +++ b/src/Application/Features/Commands/PatchExpenseCommand.cs @@ -0,0 +1,120 @@ +namespace Application.Features.Commands; + +public sealed record PatchExpenseCommand( + Guid Id, + string Name, + string Description, + double Value, + Guid CategoryId +) : IRequest +{ + public sealed class PatchExpenseCommandHandler : IRequestHandler + { + private readonly IValidator _validator; + private readonly IExpensesRepository _repository; + private readonly IUnitOfWork _uow; + private readonly IEventBus _eventBus; + + public PatchExpenseCommandHandler( + IValidator validator, + IExpensesRepository repository, + IUnitOfWork uow, + IEventBus eventBus + ) + { + _validator = validator; + _repository = repository; + _uow = uow; + _eventBus = eventBus; + } + + public async Task Handle(PatchExpenseCommand request, CancellationToken cancellationToken) + { + var results = await _validator.ValidateAsync(request, cancellationToken); + if (!results.IsValid) + throw new ValidationException(results.ToString().Replace("\r\n", " ")); + + var expenseToPatch = new Expense( + request.Id, + request.Name, + request.Description, + request.Value, + request.CategoryId + ); + + await _repository.PatchAsync(expenseToPatch, cancellationToken); + await _uow.SaveChangesAsync(cancellationToken); + await _eventBus.PublishAsync( + new PatchedExpenseEvent( + expenseToPatch.Id, + expenseToPatch.Name, + expenseToPatch.Description, + expenseToPatch.Value, + expenseToPatch.CategoryId + ), + cancellationToken + ); + } + } + + public sealed class PatchExpenseCommandValidator : AbstractValidator + { + private readonly int NameMaxLength = 30; + private readonly int DescriptionMaxLength = 256; + + public PatchExpenseCommandValidator() + { + RuleFor(c => c.Id) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty(nameof(PatchExpenseCommand.Id)) + ) + .Must(id => id.GetType() == typeof(Guid)) + .WithMessage( + GenericValidationMessages.ShouldBeAGuid(nameof(PatchExpenseCommand.Id)) + ); + + RuleFor(c => c.Name) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty(nameof(PatchExpenseCommand.Name)) + ) + .MaximumLength(NameMaxLength) + .WithMessage( + GenericValidationMessages.ShouldNotBeLongerThan( + nameof(PatchExpenseCommand.Name), + NameMaxLength + ) + ); + + RuleFor(c => c.Description) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty( + nameof(PatchExpenseCommand.Description) + ) + ) + .MaximumLength(DescriptionMaxLength) + .WithMessage( + GenericValidationMessages.ShouldNotBeLongerThan( + nameof(PatchExpenseCommand.Description), + DescriptionMaxLength + ) + ); + + RuleFor(c => c.Value) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty(nameof(PatchExpenseCommand.Value)) + ); + + RuleFor(c => c.CategoryId) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty( + nameof(PatchExpenseCommand.CategoryId) + ) + ); + } + } +} diff --git a/src/Application/Features/Commands/PostCategoryCommand.cs b/src/Application/Features/Commands/PostCategoryCommand.cs new file mode 100644 index 0000000..b9127fe --- /dev/null +++ b/src/Application/Features/Commands/PostCategoryCommand.cs @@ -0,0 +1,82 @@ +namespace Application.Features.Commands; + +public sealed record PostCategoryCommand(string Name, string Description) : IRequest +{ + public sealed class PostCategoryCommandHandler : IRequestHandler + { + private readonly IValidator _validator; + private readonly ICategoriesRepository _repository; + private readonly IUnitOfWork _uow; + private readonly IEventBus _eventBus; + + public PostCategoryCommandHandler( + IValidator validator, + ICategoriesRepository repository, + IUnitOfWork uow, + IEventBus eventBus + ) + { + _validator = validator; + _repository = repository; + _uow = uow; + _eventBus = eventBus; + } + + public async Task Handle(PostCategoryCommand request, CancellationToken cancellationToken) + { + var results = await _validator.ValidateAsync(request, cancellationToken); + if (!results.IsValid) + throw new ValidationException(results.ToString().Replace("\r\n", " ")); + + var categoryToPost = new Category(Guid.NewGuid(), request.Name, request.Description); + + await _repository.PostAsync(categoryToPost, cancellationToken); + await _uow.SaveChangesAsync(cancellationToken); + await _eventBus.PublishAsync( + new PostedCategoryEvent( + categoryToPost.Id, + categoryToPost.Name, + categoryToPost.Description + ), + cancellationToken + ); + } + } + + public sealed class PostCategoryCommandValidator : AbstractValidator + { + private readonly int NameMaxLength = 30; + private readonly int DescriptionMaxLength = 256; + + public PostCategoryCommandValidator() + { + RuleFor(c => c.Name) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty(nameof(PostCategoryCommand.Name)) + ) + .MaximumLength(NameMaxLength) + .WithMessage( + GenericValidationMessages.ShouldNotBeLongerThan( + nameof(PostCategoryCommand.Name), + NameMaxLength + ) + ); + + RuleFor(c => c.Description) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty( + nameof(PostCategoryCommand.Description) + ) + ) + .MaximumLength(DescriptionMaxLength) + .WithMessage( + GenericValidationMessages.ShouldNotBeLongerThan( + nameof(PostCategoryCommand.Description), + DescriptionMaxLength + ) + ); + } + } +} diff --git a/src/Application/Features/Commands/PostExpenseCommand.cs b/src/Application/Features/Commands/PostExpenseCommand.cs new file mode 100644 index 0000000..df399b5 --- /dev/null +++ b/src/Application/Features/Commands/PostExpenseCommand.cs @@ -0,0 +1,113 @@ +namespace Application.Features.Commands; + +public sealed record PostExpenseCommand( + string Name, + string Description, + double Value, + Guid CategoryId +) : IRequest +{ + public sealed class PostExpenseCommandHandler : IRequestHandler + { + private readonly IValidator _validator; + private readonly IExpensesRepository _repository; + private readonly IUnitOfWork _uow; + private readonly IEventBus _eventBus; + + public PostExpenseCommandHandler( + IValidator validator, + IExpensesRepository repository, + IUnitOfWork uow, + IEventBus eventBus + ) + { + _validator = validator; + _repository = repository; + _uow = uow; + _eventBus = eventBus; + } + + public async Task Handle(PostExpenseCommand request, CancellationToken cancellationToken) + { + var results = await _validator.ValidateAsync(request, cancellationToken); + if (!results.IsValid) + throw new ValidationException(results.ToString().Replace("\r\n", " ")); + + var expenseToPost = new Expense( + Guid.NewGuid(), + request.Name, + request.Description, + request.Value, + request.CategoryId + ); + + await _repository.PostAsync(expenseToPost, cancellationToken); + await _uow.SaveChangesAsync(cancellationToken); + await _eventBus.PublishAsync( + new PostedExpenseEvent( + expenseToPost.Id, + expenseToPost.Name, + expenseToPost.Description, + expenseToPost.Value, + expenseToPost.CategoryId + ), + cancellationToken + ); + } + } + + public sealed class PostExpenseCommandValidator : AbstractValidator + { + private readonly int NameMaxLength = 30; + private readonly int DescriptionMaxLength = 256; + + public PostExpenseCommandValidator() + { + RuleFor(c => c.Name) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty(nameof(PostExpenseCommand.Name)) + ) + .MaximumLength(NameMaxLength) + .WithMessage( + GenericValidationMessages.ShouldNotBeLongerThan( + nameof(PostExpenseCommand.Name), + NameMaxLength + ) + ); + + RuleFor(c => c.Description) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty( + nameof(PostExpenseCommand.Description) + ) + ) + .MaximumLength(DescriptionMaxLength) + .WithMessage( + GenericValidationMessages.ShouldNotBeLongerThan( + nameof(PostExpenseCommand.Description), + DescriptionMaxLength + ) + ); + + RuleFor(c => c.Value) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty(nameof(PostExpenseCommand.Value)) + ); + + RuleFor(c => c.CategoryId) + .NotEmpty() + .WithMessage( + GenericValidationMessages.ShouldNotBeEmpty( + nameof(PostExpenseCommand.CategoryId) + ) + ) + .Must(x => x.GetType() == typeof(Guid)) + .WithMessage( + GenericValidationMessages.ShouldBeAGuid(nameof(PostExpenseCommand.CategoryId)) + ); + } + } +} diff --git a/src/Application/Features/DomainEvents/PatchedCategoryEvent.cs b/src/Application/Features/DomainEvents/PatchedCategoryEvent.cs new file mode 100644 index 0000000..35ce924 --- /dev/null +++ b/src/Application/Features/DomainEvents/PatchedCategoryEvent.cs @@ -0,0 +1,21 @@ +namespace Application.Features.DomainEvents; + +public sealed record PatchedCategoryEvent(Guid Id, string Name, string Description) +{ + public sealed class PatchedCategoryEventConsumer : IConsumer + { + private readonly ILogger _logger; + + public PatchedCategoryEventConsumer(ILogger logger) + { + _logger = logger; + } + + public Task Consume(ConsumeContext context) + { + _logger.LogInformation("Category patched: {@Category}", context.Message); + + return Task.CompletedTask; + } + } +} diff --git a/src/Application/Features/DomainEvents/PatchedExpenseEvent.cs b/src/Application/Features/DomainEvents/PatchedExpenseEvent.cs new file mode 100644 index 0000000..9cbae73 --- /dev/null +++ b/src/Application/Features/DomainEvents/PatchedExpenseEvent.cs @@ -0,0 +1,27 @@ +namespace Application.Features.DomainEvents; + +public sealed record PatchedExpenseEvent( + Guid Id, + string Name, + string Description, + double Value, + Guid CategoryId +) +{ + public sealed class PatchedExpenseEventConsumer : IConsumer + { + private readonly ILogger _logger; + + public PatchedExpenseEventConsumer(ILogger logger) + { + _logger = logger; + } + + public Task Consume(ConsumeContext context) + { + _logger.LogInformation("Expense patched: {@Expense}", context.Message); + + return Task.CompletedTask; + } + } +} diff --git a/src/Application/Features/DomainEvents/PostedCategoryEvent.cs b/src/Application/Features/DomainEvents/PostedCategoryEvent.cs new file mode 100644 index 0000000..87d17a6 --- /dev/null +++ b/src/Application/Features/DomainEvents/PostedCategoryEvent.cs @@ -0,0 +1,21 @@ +namespace Application.Features.DomainEvents; + +public sealed record PostedCategoryEvent(Guid Id, string Name, string Description) +{ + public sealed class PostedCategoryEventConsumer : IConsumer + { + private readonly ILogger _logger; + + public PostedCategoryEventConsumer(ILogger logger) + { + _logger = logger; + } + + public Task Consume(ConsumeContext context) + { + _logger.LogInformation("Category created: {@Category}", context.Message); + + return Task.CompletedTask; + } + } +} diff --git a/src/Application/Features/DomainEvents/PostedExpenseEvent.cs b/src/Application/Features/DomainEvents/PostedExpenseEvent.cs new file mode 100644 index 0000000..d92de73 --- /dev/null +++ b/src/Application/Features/DomainEvents/PostedExpenseEvent.cs @@ -0,0 +1,27 @@ +namespace Application.Features.DomainEvents; + +public record PostedExpenseEvent( + Guid Id, + string Name, + string Description, + double Value, + Guid CategoryId +) +{ + public sealed class PostedExpenseEventConsumer : IConsumer + { + private readonly ILogger _logger; + + public PostedExpenseEventConsumer(ILogger logger) + { + _logger = logger; + } + + public Task Consume(ConsumeContext context) + { + _logger.LogInformation("Expense posted: {@Expense}", context.Message); + + return Task.CompletedTask; + } + } +} diff --git a/src/Application/Features/Queries/GetAllCategoriesQuery.cs b/src/Application/Features/Queries/GetAllCategoriesQuery.cs new file mode 100644 index 0000000..528dfce --- /dev/null +++ b/src/Application/Features/Queries/GetAllCategoriesQuery.cs @@ -0,0 +1,46 @@ +namespace Application.Features.Queries; + +public sealed record GetAllCategoriesQuery(int Page, int PageSize) + : IRequest> +{ + public sealed class GetAllCategoriesQueryHandler + : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IRedisCache _redisCache; + + public GetAllCategoriesQueryHandler(IApplicationDbContext context, IRedisCache redisCache) + { + _context = context; + _redisCache = redisCache; + } + + public async Task> Handle( + GetAllCategoriesQuery request, + CancellationToken cancellationToken + ) + { + var redisKey = $"{nameof(GetAllCategoriesQuery)}#{request.Page}#{request.PageSize}"; + var cachedGetAllCategoriesQuery = await _redisCache.GetCachedData< + PagedList + >(redisKey); + if (cachedGetAllCategoriesQuery != null) + return cachedGetAllCategoriesQuery; + + var pagedCategories = await _context + .Categories.AsNoTracking() + .Include(c => c.Expenses) + .OrderBy(c => c.Name) + .Select(c => new CategoryDTO(c.Id, c.Name, c.Description, c.Expenses)) + .ToPagedListAsync(request.Page, request.PageSize, cancellationToken); + + await _redisCache.SetCachedData>( + redisKey, + pagedCategories, + DateTimeOffset.Now.AddMinutes(5) + ); + + return pagedCategories; + } + } +} diff --git a/src/Application/Features/Queries/GetAllExpensesQuery.cs b/src/Application/Features/Queries/GetAllExpensesQuery.cs new file mode 100644 index 0000000..064a28a --- /dev/null +++ b/src/Application/Features/Queries/GetAllExpensesQuery.cs @@ -0,0 +1,44 @@ +namespace Application.Features.Queries; + +public record GetAllExpensesQuery(int Page, int PageSize) : IRequest> +{ + public sealed class GetAllExpensesQueryHandler + : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IRedisCache _redisCache; + + public GetAllExpensesQueryHandler(IApplicationDbContext context, IRedisCache redisCache) + { + _context = context; + _redisCache = redisCache; + } + + public async Task> Handle( + GetAllExpensesQuery request, + CancellationToken cancellationToken + ) + { + var redisKey = $"{nameof(GetAllExpensesQuery)}#{request.Page}#{request.PageSize}"; + var cachedGetAllExpensesQuery = await _redisCache.GetCachedData>( + redisKey + ); + if (cachedGetAllExpensesQuery != null) + return cachedGetAllExpensesQuery; + + var pagedExpenses = await _context + .Expenses.AsNoTracking() + .OrderBy(e => e.Name) + .Select(e => new ExpenseDTO(e.Id, e.Name, e.Description, e.Value, e.CategoryId)) + .ToPagedListAsync(request.Page, request.PageSize, cancellationToken); + + await _redisCache.SetCachedData>( + redisKey, + pagedExpenses, + DateTimeOffset.Now.AddMinutes(5) + ); + + return pagedExpenses; + } + } +} diff --git a/src/Application/Features/Queries/GetCategoryQuery.cs b/src/Application/Features/Queries/GetCategoryQuery.cs new file mode 100644 index 0000000..2046979 --- /dev/null +++ b/src/Application/Features/Queries/GetCategoryQuery.cs @@ -0,0 +1,44 @@ +namespace Application.Features.Queries; + +public sealed record GetCategoryQuery(Guid Id) : IRequest +{ + public class GetCategoryQueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + + public GetCategoryQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle( + GetCategoryQuery request, + CancellationToken cancellationToken + ) + { + var category = + await _context + .Categories.AsNoTracking() + .Include(c => c.Expenses) + .FirstOrDefaultAsync(c => c.Id == request.Id, cancellationToken) + ?? throw new CategoryNotFoundException(request.Id); + + return new CategoryDTO( + category.Id, + category.Name, + category.Description, + category?.Expenses + ); + } + } + + public class GetCategoryQueryValidator : AbstractValidator + { + public GetCategoryQueryValidator() + { + RuleFor(x => x.Id) + .NotEmpty() + .WithMessage(GenericValidationMessages.ShouldNotBeEmpty("Id")); + } + } +} diff --git a/src/Application/Features/Queries/GetExpenseQuery.cs b/src/Application/Features/Queries/GetExpenseQuery.cs new file mode 100644 index 0000000..be09126 --- /dev/null +++ b/src/Application/Features/Queries/GetExpenseQuery.cs @@ -0,0 +1,44 @@ +namespace Application.Features.Queries; + +public sealed record GetExpenseQuery(Guid Id) : IRequest +{ + public sealed class GetExpenseQueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + + public GetExpenseQueryHandler(IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle( + GetExpenseQuery request, + CancellationToken cancellationToken + ) + { + var expense = + await _context + .Expenses.AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken) + ?? throw new ExpenseNotFoundException(request.Id); + + return new ExpenseDTO( + expense.Id, + expense.Name, + expense.Description, + expense.Value, + expense.CategoryId + ); + } + } + + public class GetExpenseQueryValidator : AbstractValidator + { + public GetExpenseQueryValidator() + { + RuleFor(x => x.Id) + .NotEmpty() + .WithMessage(GenericValidationMessages.ShouldNotBeEmpty("Id")); + } + } +} diff --git a/src/Application/Features/Shared/CategoryDTO.cs b/src/Application/Features/Shared/CategoryDTO.cs new file mode 100644 index 0000000..bed93ca --- /dev/null +++ b/src/Application/Features/Shared/CategoryDTO.cs @@ -0,0 +1,3 @@ +namespace Application.Features.Shared; + +public record CategoryDTO(Guid Id, string Name, string Description, List Expenses); diff --git a/src/Application/Features/Shared/ExpenseDTO.cs b/src/Application/Features/Shared/ExpenseDTO.cs new file mode 100644 index 0000000..8add300 --- /dev/null +++ b/src/Application/Features/Shared/ExpenseDTO.cs @@ -0,0 +1,3 @@ +namespace Application.Features.Shared; + +public record ExpenseDTO(Guid Id, string Name, string Description, double Value, Guid CategoryId); diff --git a/src/Application/GlobalUsings.cs b/src/Application/GlobalUsings.cs new file mode 100644 index 0000000..bb59481 --- /dev/null +++ b/src/Application/GlobalUsings.cs @@ -0,0 +1,17 @@ +global using Application.Abstractions; +global using Application.Contexts; +global using Application.Features.DomainEvents; +global using Application.Features.Shared; +global using Application.Messages; +global using Application.Repositories; +global using Domain.Cache; +global using Domain.Entities; +global using Domain.Exceptions; +global using static Domain.Extensions.Collections.Collections; +global using Domain.Persistence; +global using FluentValidation; +global using MassTransit; +global using MediatR; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; diff --git a/src/Domain/Categories/ICategoriesRepository.cs b/src/Application/Repositories/ICategoriesRepository.cs similarity index 57% rename from src/Domain/Categories/ICategoriesRepository.cs rename to src/Application/Repositories/ICategoriesRepository.cs index bb7baeb..edb4a67 100644 --- a/src/Domain/Categories/ICategoriesRepository.cs +++ b/src/Application/Repositories/ICategoriesRepository.cs @@ -1,8 +1,8 @@ -namespace Domain.Categories; +namespace Application.Repositories; public interface ICategoriesRepository { - Task PatchAsync(Category category, CancellationToken cancellation); - Task PostAsync(Category category, CancellationToken cancellation); - Task DeleteAsync(Guid Id, CancellationToken cancellation); + Task PostAsync(Category category, CancellationToken cancellationToken); + Task PatchAsync(Category category, CancellationToken cancellationToken); + Task DeleteAsync(Guid id, CancellationToken cancellationToken); } diff --git a/src/Domain/Expenses/IExpensesRepository.cs b/src/Application/Repositories/IExpensesRepository.cs similarity index 65% rename from src/Domain/Expenses/IExpensesRepository.cs rename to src/Application/Repositories/IExpensesRepository.cs index d1fdd54..f6a96ea 100644 --- a/src/Domain/Expenses/IExpensesRepository.cs +++ b/src/Application/Repositories/IExpensesRepository.cs @@ -1,8 +1,8 @@ -namespace Domain.Expenses; +namespace Application.Repositories; public interface IExpensesRepository { - Task PatchAsync(Expense expense, CancellationToken cancellationToken); Task PostAsync(Expense expense, CancellationToken cancellationToken); - Task DeleteAsync(Guid Id, CancellationToken cancellationToken); -} \ No newline at end of file + Task PatchAsync(Expense expense, CancellationToken cancellationToken); + Task DeleteAsync(Guid id, CancellationToken cancellationToken); +} diff --git a/src/Dockerfile b/src/Dockerfile index 2772b3c..77b7ab5 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -1,5 +1,4 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -ARG PASSWORD WORKDIR /app COPY . . RUN dotnet restore ./WebApi/WebApi.csproj diff --git a/src/Domain/Categories/Category.cs b/src/Domain/Entities/Category.cs similarity index 72% rename from src/Domain/Categories/Category.cs rename to src/Domain/Entities/Category.cs index 8637d93..2b88a4a 100644 --- a/src/Domain/Categories/Category.cs +++ b/src/Domain/Entities/Category.cs @@ -1,6 +1,4 @@ -using Domain.Expenses; - -namespace Domain.Categories; +namespace Domain.Entities; public class Category { @@ -15,5 +13,5 @@ public Category(Guid id, string name, string description) public string Name { get; set; } public string Description { get; set; } - public List? Expenses { get; set; } = default; + public virtual List Expenses { get; set; } = default!; } diff --git a/src/Domain/Expenses/Expense.cs b/src/Domain/Entities/Expense.cs similarity index 80% rename from src/Domain/Expenses/Expense.cs rename to src/Domain/Entities/Expense.cs index 851711b..82ddff8 100644 --- a/src/Domain/Expenses/Expense.cs +++ b/src/Domain/Entities/Expense.cs @@ -1,6 +1,4 @@ -using Domain.Categories; - -namespace Domain.Expenses; +namespace Domain.Entities; public class Expense { @@ -19,5 +17,5 @@ public Expense(Guid id, string name, string description, double value, Guid cate public double Value { get; set; } public Guid CategoryId { get; set; } - public Category? Category { get; set; } = default; -} \ No newline at end of file + public virtual Category? Category { get; set; } = default; +} diff --git a/src/Domain/Categories/CategoriesExceptions.cs b/src/Domain/Exceptions/CategoryExceptions.cs similarity index 85% rename from src/Domain/Categories/CategoriesExceptions.cs rename to src/Domain/Exceptions/CategoryExceptions.cs index b37a8b9..ccbce88 100644 --- a/src/Domain/Categories/CategoriesExceptions.cs +++ b/src/Domain/Exceptions/CategoryExceptions.cs @@ -1,4 +1,4 @@ -namespace Domain.Categories; +namespace Domain.Exceptions; public sealed class CategoryNotFoundException : Exception { diff --git a/src/Domain/Expenses/ExpensesExceptions.cs b/src/Domain/Exceptions/ExpensesExceptions.cs similarity index 85% rename from src/Domain/Expenses/ExpensesExceptions.cs rename to src/Domain/Exceptions/ExpensesExceptions.cs index 19f2f79..cbb6db8 100644 --- a/src/Domain/Expenses/ExpensesExceptions.cs +++ b/src/Domain/Exceptions/ExpensesExceptions.cs @@ -1,4 +1,4 @@ -namespace Domain.Expenses; +namespace Domain.Exceptions; public sealed class ExpenseNotFoundException : Exception { diff --git a/src/Domain/Extensions/Collections.cs b/src/Domain/Extensions/Collections.cs index b6098ed..9e2fcfa 100644 --- a/src/Domain/Extensions/Collections.cs +++ b/src/Domain/Extensions/Collections.cs @@ -1,5 +1,3 @@ -using Microsoft.EntityFrameworkCore; - namespace Domain.Extensions.Collections; public static class Collections diff --git a/src/Domain/GlobalUsings.cs b/src/Domain/GlobalUsings.cs new file mode 100644 index 0000000..74797d2 --- /dev/null +++ b/src/Domain/GlobalUsings.cs @@ -0,0 +1,5 @@ +global using Domain.Identity; +global using Microsoft.AspNetCore.Identity; +global using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +global using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +global using Microsoft.EntityFrameworkCore; diff --git a/src/Domain/Identity/ApplicationUser.cs b/src/Domain/Identity/ApplicationUser.cs new file mode 100644 index 0000000..a001d48 --- /dev/null +++ b/src/Domain/Identity/ApplicationUser.cs @@ -0,0 +1,3 @@ +namespace Domain.Identity; + +public class ApplicationUser : IdentityUser { } diff --git a/src/Domain/Users/User.cs b/src/Domain/Users/User.cs deleted file mode 100644 index 063b682..0000000 --- a/src/Domain/Users/User.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Microsoft.AspNetCore.Identity; - -namespace Domain.Users; - -public class User : IdentityUser { } \ No newline at end of file diff --git a/src/Infrastructure/Cache/RedisCache.cs b/src/Infrastructure/Cache/RedisCache.cs index 19b3543..6be466c 100644 --- a/src/Infrastructure/Cache/RedisCache.cs +++ b/src/Infrastructure/Cache/RedisCache.cs @@ -1,7 +1,3 @@ -using System.Text.Json; -using Domain.Cache; -using StackExchange.Redis; - namespace Infrastructure.Cache; public sealed class RedisCache : IRedisCache @@ -25,7 +21,19 @@ public RedisCache(IDatabase redisDB) public async Task SetCachedData(string key, T value, DateTimeOffset dateTimeOffset) { var expirationTime = dateTimeOffset.DateTime.Subtract(DateTime.Now); - await _redisDB.StringSetAsync(key, JsonSerializer.Serialize(value), expirationTime); + await _redisDB.StringSetAsync( + key, + JsonSerializer.Serialize( + value, + value!.GetType(), + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + WriteIndented = true, + } + ), + expirationTime + ); } public async Task RemoveData(string key) diff --git a/src/Infrastructure/Configurations/CategoriesConfiguration.cs b/src/Infrastructure/Configurations/CategoriesConfiguration.cs index 2b30700..f7c7bdc 100644 --- a/src/Infrastructure/Configurations/CategoriesConfiguration.cs +++ b/src/Infrastructure/Configurations/CategoriesConfiguration.cs @@ -1,7 +1,3 @@ -using Domain.Categories; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - namespace Infrastructure.Configurations; public class CategoriesConfiguration : IEntityTypeConfiguration @@ -11,10 +7,5 @@ public void Configure(EntityTypeBuilder builder) builder.HasKey(c => c.Id); builder.Property(c => c.Name).HasMaxLength(30).IsRequired(); builder.Property(c => c.Description).HasMaxLength(256); - builder - .HasMany(c => c.Expenses) - .WithOne(e => e.Category) - .HasForeignKey(e => e.CategoryId) - .OnDelete(DeleteBehavior.Cascade); } } diff --git a/src/Infrastructure/Configurations/ExpensesConfiguration.cs b/src/Infrastructure/Configurations/ExpensesConfiguration.cs index e32ce4b..1c4de8f 100644 --- a/src/Infrastructure/Configurations/ExpensesConfiguration.cs +++ b/src/Infrastructure/Configurations/ExpensesConfiguration.cs @@ -1,7 +1,3 @@ -using Domain.Expenses; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - namespace Infrastructure.Configurations; public class ExpensesConfiguration : IEntityTypeConfiguration @@ -12,6 +8,12 @@ public void Configure(EntityTypeBuilder builder) builder.Property(e => e.Name).HasMaxLength(30).IsRequired(); builder.Property(e => e.Description).HasMaxLength(256); builder.Property(e => e.Value).IsRequired(); - builder.HasOne(e => e.Category).WithMany(c => c.Expenses).HasForeignKey(e => e.CategoryId); + + builder + .HasOne(e => e.Category) + .WithMany(c => c.Expenses) + .HasForeignKey(e => e.CategoryId) + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); } -} \ No newline at end of file +} diff --git a/src/Infrastructure/Contexts/ApplicationDbContext.cs b/src/Infrastructure/Contexts/ApplicationDbContext.cs index 6a59c92..d2e0fe9 100644 --- a/src/Infrastructure/Contexts/ApplicationDbContext.cs +++ b/src/Infrastructure/Contexts/ApplicationDbContext.cs @@ -1,13 +1,6 @@ -using Application.Contexts; -using Domain.Categories; -using Domain.Expenses; -using Domain.Users; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; - namespace Infrastructure.Contexts; -public class ApplicationDbContext : IdentityDbContext, IApplicationDbContext +public class ApplicationDbContext : IdentityDbContext, IApplicationDbContext { public ApplicationDbContext(DbContextOptions options) : base(options) { } diff --git a/src/Infrastructure/Extensions/MigrationExtension.cs b/src/Infrastructure/Extensions/MigrationExtension.cs index adf712e..a731afc 100644 --- a/src/Infrastructure/Extensions/MigrationExtension.cs +++ b/src/Infrastructure/Extensions/MigrationExtension.cs @@ -1,8 +1,3 @@ -using Infrastructure.Contexts; -using Microsoft.AspNetCore.Builder; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; - namespace Infrastructure.Extensions; public static class MigrationExtension @@ -11,6 +6,6 @@ public static void ApplyMigrations(this IApplicationBuilder app) { app.ApplicationServices.CreateScope() .ServiceProvider.GetRequiredService() - .Database.Migrate(); + .Database.MigrateAsync(); } } diff --git a/src/Infrastructure/GlobalUsings.cs b/src/Infrastructure/GlobalUsings.cs index 73eaa3d..3a4dd3c 100644 --- a/src/Infrastructure/GlobalUsings.cs +++ b/src/Infrastructure/GlobalUsings.cs @@ -1,12 +1,15 @@ +global using System.Text.Json; global using Application.Abstractions; -global using Application.Categories.PatchAsync; -global using Application.Categories.PostAsync; global using Application.Contexts; -global using Application.Expenses.PatchAsync; -global using Application.Expenses.PostAsync; +global using static Application.Features.DomainEvents.PatchedCategoryEvent; +global using static Application.Features.DomainEvents.PatchedExpenseEvent; +global using static Application.Features.DomainEvents.PostedCategoryEvent; +global using static Application.Features.DomainEvents.PostedExpenseEvent; +global using Application.Repositories; global using Domain.Cache; -global using Domain.Categories; -global using Domain.Expenses; +global using Domain.Entities; +global using Domain.Exceptions; +global using Domain.Identity; global using Domain.Persistence; global using Infrastructure.Cache; global using Infrastructure.Contexts; @@ -14,7 +17,10 @@ global using Infrastructure.Persistence; global using Infrastructure.Repositories; global using MassTransit; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Identity.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore; +global using Microsoft.EntityFrameworkCore.Metadata.Builders; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using StackExchange.Redis; diff --git a/src/Infrastructure/MessageBrokers/RabbitMQ/RabbitMQEventBus.cs b/src/Infrastructure/MessageBrokers/RabbitMQ/RabbitMQEventBus.cs index 5d0f7fb..7123588 100644 --- a/src/Infrastructure/MessageBrokers/RabbitMQ/RabbitMQEventBus.cs +++ b/src/Infrastructure/MessageBrokers/RabbitMQ/RabbitMQEventBus.cs @@ -1,6 +1,3 @@ -using Application.Abstractions; -using MassTransit; - namespace Infrastructure.MessageBrokers.RabbitMQ; public sealed class RabbitMQEventBus : IEventBus @@ -14,4 +11,4 @@ public RabbitMQEventBus(IPublishEndpoint publishEndpoint) public Task PublishAsync(T message, CancellationToken cancellationToken = default) where T : class => _publishEndpoint.Publish(message, cancellationToken); -} \ No newline at end of file +} diff --git a/src/Infrastructure/MessageBrokers/RabbitMQ/RabbitMQSettings.cs b/src/Infrastructure/MessageBrokers/RabbitMQ/RabbitMQSettings.cs deleted file mode 100644 index 628a9fa..0000000 --- a/src/Infrastructure/MessageBrokers/RabbitMQ/RabbitMQSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Infrastructure.MessageBrokers.RabbitMQ; - -public sealed class RabbitMQSettings -{ - public string Host { get; set; } = string.Empty; - public string Username { get; set; } = string.Empty; - public string Password { get; set; } = string.Empty; -} \ No newline at end of file diff --git a/src/Infrastructure/Migrations/20240727192023_categories_expenses.cs b/src/Infrastructure/Migrations/20240727192023_categories_expenses.cs deleted file mode 100644 index 97e97f4..0000000 --- a/src/Infrastructure/Migrations/20240727192023_categories_expenses.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Infrastructure.Migrations -{ - /// - public partial class categories_expenses : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Categories", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - Name = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), - Description = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Categories", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Expenses", - columns: table => new - { - Id = table.Column(type: "uniqueidentifier", nullable: false), - Name = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), - Description = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), - Value = table.Column(type: "float", nullable: false), - CategoryId = table.Column(type: "uniqueidentifier", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Expenses", x => x.Id); - table.ForeignKey( - name: "FK_Expenses_Categories_CategoryId", - column: x => x.CategoryId, - principalTable: "Categories", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Expenses_CategoryId", - table: "Expenses", - column: "CategoryId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Expenses"); - - migrationBuilder.DropTable( - name: "Categories"); - } - } -} diff --git a/src/Infrastructure/Migrations/20240727192023_categories_expenses.Designer.cs b/src/Infrastructure/Migrations/20241110225607_Initial.Designer.cs similarity index 94% rename from src/Infrastructure/Migrations/20240727192023_categories_expenses.Designer.cs rename to src/Infrastructure/Migrations/20241110225607_Initial.Designer.cs index 8271897..0143087 100644 --- a/src/Infrastructure/Migrations/20240727192023_categories_expenses.Designer.cs +++ b/src/Infrastructure/Migrations/20241110225607_Initial.Designer.cs @@ -12,8 +12,8 @@ namespace Infrastructure.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20240727192023_categories_expenses")] - partial class categories_expenses + [Migration("20241110225607_Initial")] + partial class Initial { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -25,7 +25,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("Domain.Categories.Category", b => + modelBuilder.Entity("Domain.Entities.Category", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -46,7 +46,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Categories"); }); - modelBuilder.Entity("Domain.Expenses.Expense", b => + modelBuilder.Entity("Domain.Entities.Expense", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -75,7 +75,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Expenses"); }); - modelBuilder.Entity("Domain.Users.User", b => + modelBuilder.Entity("Domain.Identity.ApplicationUser", b => { b.Property("Id") .HasColumnType("nvarchar(450)"); @@ -273,9 +273,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("Domain.Expenses.Expense", b => + modelBuilder.Entity("Domain.Entities.Expense", b => { - b.HasOne("Domain.Categories.Category", "Category") + b.HasOne("Domain.Entities.Category", "Category") .WithMany("Expenses") .HasForeignKey("CategoryId") .OnDelete(DeleteBehavior.Cascade) @@ -295,7 +295,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.HasOne("Domain.Users.User", null) + b.HasOne("Domain.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -304,7 +304,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.HasOne("Domain.Users.User", null) + b.HasOne("Domain.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -319,7 +319,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Domain.Users.User", null) + b.HasOne("Domain.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -328,14 +328,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.HasOne("Domain.Users.User", null) + b.HasOne("Domain.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("Domain.Categories.Category", b => + modelBuilder.Entity("Domain.Entities.Category", b => { b.Navigation("Expenses"); }); diff --git a/src/Infrastructure/Migrations/20240727150405_initial.cs b/src/Infrastructure/Migrations/20241110225607_Initial.cs similarity index 83% rename from src/Infrastructure/Migrations/20240727150405_initial.cs rename to src/Infrastructure/Migrations/20241110225607_Initial.cs index 4c0ae40..5761cb0 100644 --- a/src/Infrastructure/Migrations/20240727150405_initial.cs +++ b/src/Infrastructure/Migrations/20241110225607_Initial.cs @@ -6,7 +6,7 @@ namespace Infrastructure.Migrations { /// - public partial class initial : Migration + public partial class Initial : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -50,6 +50,19 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_AspNetUsers", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), + Description = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + migrationBuilder.CreateTable( name: "AspNetRoleClaims", columns: table => new @@ -156,6 +169,27 @@ protected override void Up(MigrationBuilder migrationBuilder) onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "Expenses", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), + Description = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + Value = table.Column(type: "float", nullable: false), + CategoryId = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Expenses", x => x.Id); + table.ForeignKey( + name: "FK_Expenses_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateIndex( name: "IX_AspNetRoleClaims_RoleId", table: "AspNetRoleClaims", @@ -194,6 +228,11 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "NormalizedUserName", unique: true, filter: "[NormalizedUserName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_Expenses_CategoryId", + table: "Expenses", + column: "CategoryId"); } /// @@ -214,11 +253,17 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.DropTable( name: "AspNetUserTokens"); + migrationBuilder.DropTable( + name: "Expenses"); + migrationBuilder.DropTable( name: "AspNetRoles"); migrationBuilder.DropTable( name: "AspNetUsers"); + + migrationBuilder.DropTable( + name: "Categories"); } } } diff --git a/src/Infrastructure/Migrations/20240727150405_initial.Designer.cs b/src/Infrastructure/Migrations/20241110231659_NonNullable.Designer.cs similarity index 78% rename from src/Infrastructure/Migrations/20240727150405_initial.Designer.cs rename to src/Infrastructure/Migrations/20241110231659_NonNullable.Designer.cs index d6e8a0a..265fa1f 100644 --- a/src/Infrastructure/Migrations/20240727150405_initial.Designer.cs +++ b/src/Infrastructure/Migrations/20241110231659_NonNullable.Designer.cs @@ -12,8 +12,8 @@ namespace Infrastructure.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20240727150405_initial")] - partial class initial + [Migration("20241110231659_NonNullable")] + partial class NonNullable { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -25,7 +25,57 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("Domain.Users.User", b => + modelBuilder.Entity("Domain.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Domain.Entities.Expense", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CategoryId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("Value") + .HasColumnType("float"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Expenses"); + }); + + modelBuilder.Entity("Domain.Identity.ApplicationUser", b => { b.Property("Id") .HasColumnType("nvarchar(450)"); @@ -223,6 +273,17 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); + modelBuilder.Entity("Domain.Entities.Expense", b => + { + b.HasOne("Domain.Entities.Category", "Category") + .WithMany("Expenses") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) @@ -234,7 +295,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.HasOne("Domain.Users.User", null) + b.HasOne("Domain.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -243,7 +304,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.HasOne("Domain.Users.User", null) + b.HasOne("Domain.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -258,7 +319,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Domain.Users.User", null) + b.HasOne("Domain.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -267,12 +328,17 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.HasOne("Domain.Users.User", null) + b.HasOne("Domain.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); + + modelBuilder.Entity("Domain.Entities.Category", b => + { + b.Navigation("Expenses"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Infrastructure/Migrations/20241110231659_NonNullable.cs b/src/Infrastructure/Migrations/20241110231659_NonNullable.cs new file mode 100644 index 0000000..a43bbeb --- /dev/null +++ b/src/Infrastructure/Migrations/20241110231659_NonNullable.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class NonNullable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 2fd08bd..6745ed1 100644 --- a/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,7 +22,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - modelBuilder.Entity("Domain.Categories.Category", b => + modelBuilder.Entity("Domain.Entities.Category", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -43,7 +43,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Categories"); }); - modelBuilder.Entity("Domain.Expenses.Expense", b => + modelBuilder.Entity("Domain.Entities.Expense", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -72,7 +72,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Expenses"); }); - modelBuilder.Entity("Domain.Users.User", b => + modelBuilder.Entity("Domain.Identity.ApplicationUser", b => { b.Property("Id") .HasColumnType("nvarchar(450)"); @@ -270,9 +270,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("Domain.Expenses.Expense", b => + modelBuilder.Entity("Domain.Entities.Expense", b => { - b.HasOne("Domain.Categories.Category", "Category") + b.HasOne("Domain.Entities.Category", "Category") .WithMany("Expenses") .HasForeignKey("CategoryId") .OnDelete(DeleteBehavior.Cascade) @@ -292,7 +292,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.HasOne("Domain.Users.User", null) + b.HasOne("Domain.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -301,7 +301,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.HasOne("Domain.Users.User", null) + b.HasOne("Domain.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -316,7 +316,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Domain.Users.User", null) + b.HasOne("Domain.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -325,14 +325,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.HasOne("Domain.Users.User", null) + b.HasOne("Domain.Identity.ApplicationUser", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); - modelBuilder.Entity("Domain.Categories.Category", b => + modelBuilder.Entity("Domain.Entities.Category", b => { b.Navigation("Expenses"); }); diff --git a/src/Infrastructure/Persistence/UnitOfWork.cs b/src/Infrastructure/Persistence/UnitOfWork.cs index 339312b..c17333c 100644 --- a/src/Infrastructure/Persistence/UnitOfWork.cs +++ b/src/Infrastructure/Persistence/UnitOfWork.cs @@ -1,6 +1,3 @@ -using Domain.Persistence; -using Infrastructure.Contexts; - namespace Infrastructure.Persistence; internal sealed class UnitOfWork : IUnitOfWork diff --git a/src/Infrastructure/Repositories/CategoriesRepository.cs b/src/Infrastructure/Repositories/CategoriesRepository.cs index 5b88e8b..3727c98 100644 --- a/src/Infrastructure/Repositories/CategoriesRepository.cs +++ b/src/Infrastructure/Repositories/CategoriesRepository.cs @@ -1,6 +1,3 @@ -using Application.Contexts; -using Domain.Categories; - namespace Infrastructure.Repositories; public sealed class CategoriesRepository : ICategoriesRepository diff --git a/src/Infrastructure/Repositories/ExpensesRepository.cs b/src/Infrastructure/Repositories/ExpensesRepository.cs index ab70ec2..8b9d745 100644 --- a/src/Infrastructure/Repositories/ExpensesRepository.cs +++ b/src/Infrastructure/Repositories/ExpensesRepository.cs @@ -1,6 +1,3 @@ -using Application.Contexts; -using Domain.Expenses; - namespace Infrastructure.Repositories; public class ExpensesRepository : IExpensesRepository @@ -33,4 +30,4 @@ await _context.Expenses.FindAsync(id, cancellation) _context.Expenses.Remove(foundExpense); } -} \ No newline at end of file +} diff --git a/src/Presentation/Endpoints/CategoriesEndpoints.cs b/src/Presentation/Endpoints/CategoriesEndpoints.cs index a2c4a37..31b4dcb 100644 --- a/src/Presentation/Endpoints/CategoriesEndpoints.cs +++ b/src/Presentation/Endpoints/CategoriesEndpoints.cs @@ -1,15 +1,3 @@ -using Application.Categories.DeleteAsync; -using Application.Categories.GetAllAsync; -using Application.Categories.GetAsync; -using Application.Categories.PatchAsync; -using Application.Categories.PostAsync; -using MediatR; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Logging; - namespace Presentation.Endpoints; public static class CategoriesEndpoints diff --git a/src/Presentation/Endpoints/ExpensesEndpoints.cs b/src/Presentation/Endpoints/ExpensesEndpoints.cs index 66ed647..edc39b2 100644 --- a/src/Presentation/Endpoints/ExpensesEndpoints.cs +++ b/src/Presentation/Endpoints/ExpensesEndpoints.cs @@ -1,15 +1,3 @@ -using Application.Expenses.DeleteAsync; -using Application.Expenses.GetAllAsync; -using Application.Expenses.GetAsync; -using Application.Expenses.PatchAsync; -using Application.Expenses.PostAsync; -using MediatR; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Logging; - namespace Presentation.Endpoints; public static class ExpensesEndpoints diff --git a/src/Presentation/GlobalUsings.cs b/src/Presentation/GlobalUsings.cs new file mode 100644 index 0000000..5df8ae1 --- /dev/null +++ b/src/Presentation/GlobalUsings.cs @@ -0,0 +1,8 @@ +global using Application.Features.Commands; +global using Application.Features.Queries; +global using MediatR; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.AspNetCore.Routing; +global using Microsoft.Extensions.Logging; diff --git a/src/WebApi/GlobalUsings.cs b/src/WebApi/GlobalUsings.cs new file mode 100644 index 0000000..950aeb8 --- /dev/null +++ b/src/WebApi/GlobalUsings.cs @@ -0,0 +1,8 @@ +global using Application.Extensions; +global using Domain.Identity; +global using Infrastructure.Contexts; +global using Infrastructure.Extensions; +global using Microsoft.AspNetCore.Authorization; +global using Microsoft.AspNetCore.Identity; +global using Presentation.Endpoints; +global using Serilog; diff --git a/src/WebApi/Program.cs b/src/WebApi/Program.cs index 95759f3..e17da38 100644 --- a/src/WebApi/Program.cs +++ b/src/WebApi/Program.cs @@ -1,12 +1,3 @@ -using Application.Extensions; -using Domain.Users; -using Infrastructure.Contexts; -using Infrastructure.Extensions; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; -using Presentation.Endpoints; -using Serilog; - var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); @@ -28,7 +19,7 @@ options.DefaultPolicy = policy; }); builder - .Services.AddIdentityCore() + .Services.AddIdentityCore() .AddEntityFrameworkStores() .AddApiEndpoints(); @@ -50,7 +41,7 @@ app.UseAuthentication(); app.UseAuthorization(); -app.MapIdentityApi(); +app.MapIdentityApi(); app.MapCategoriesEndpoints(); app.MapExpensesEndpoints(); diff --git a/tests/Application/UnitTests/Categories/DeleteAsync/DeleteCategoryCommandHandlerTests.cs b/tests/UnitTests/Features/Commands/DeleteCategoryCommandHandlerTests.cs similarity index 91% rename from tests/Application/UnitTests/Categories/DeleteAsync/DeleteCategoryCommandHandlerTests.cs rename to tests/UnitTests/Features/Commands/DeleteCategoryCommandHandlerTests.cs index c9abe89..c9472e0 100644 --- a/tests/Application/UnitTests/Categories/DeleteAsync/DeleteCategoryCommandHandlerTests.cs +++ b/tests/UnitTests/Features/Commands/DeleteCategoryCommandHandlerTests.cs @@ -1,8 +1,3 @@ -using Application.Categories.DeleteAsync; -using Domain.Categories; -using Domain.Persistence; -using Moq; - namespace Application.UnitTests.Categories.PostAsync; public sealed class DeleteCategoryCommandHandlerTests diff --git a/tests/Application/UnitTests/Categories/PatchAsync/PatchCategoryCommandHandlerTests.cs b/tests/UnitTests/Features/Commands/PatchCategoryCommandHandlerTests.cs similarity index 90% rename from tests/Application/UnitTests/Categories/PatchAsync/PatchCategoryCommandHandlerTests.cs rename to tests/UnitTests/Features/Commands/PatchCategoryCommandHandlerTests.cs index 127436a..ac73f21 100644 --- a/tests/Application/UnitTests/Categories/PatchAsync/PatchCategoryCommandHandlerTests.cs +++ b/tests/UnitTests/Features/Commands/PatchCategoryCommandHandlerTests.cs @@ -1,11 +1,3 @@ -using Application.Abstractions; -using Application.Categories.PatchAsync; -using Domain.Categories; -using Domain.Persistence; -using FluentValidation; -using FluentValidation.Results; -using Moq; - namespace Application.UnitTests.Categories.PostAsync; public sealed class PatchCategoryCommandHandlerTests diff --git a/tests/Application/UnitTests/Categories/PostAsync/PostCategoryCommandHandlerTests.cs b/tests/UnitTests/Features/Commands/PostCategoryCommandHandlerTests.cs similarity index 90% rename from tests/Application/UnitTests/Categories/PostAsync/PostCategoryCommandHandlerTests.cs rename to tests/UnitTests/Features/Commands/PostCategoryCommandHandlerTests.cs index e0fa60b..02cda6e 100644 --- a/tests/Application/UnitTests/Categories/PostAsync/PostCategoryCommandHandlerTests.cs +++ b/tests/UnitTests/Features/Commands/PostCategoryCommandHandlerTests.cs @@ -1,11 +1,3 @@ -using Application.Abstractions; -using Application.Categories.PostAsync; -using Domain.Categories; -using Domain.Persistence; -using FluentValidation; -using FluentValidation.Results; -using Moq; - namespace Application.UnitTests.Categories.PostAsync; public sealed class PostCategoryCommandHandlerTests diff --git a/tests/Application/UnitTests/Categories/GetAllAsync/GetAllCategoriesQueryHandlerTests.cs b/tests/UnitTests/Features/Queries/GetAllCategoriesQueryHandlerTests.cs similarity index 90% rename from tests/Application/UnitTests/Categories/GetAllAsync/GetAllCategoriesQueryHandlerTests.cs rename to tests/UnitTests/Features/Queries/GetAllCategoriesQueryHandlerTests.cs index 2e8a71d..a01bf62 100644 --- a/tests/Application/UnitTests/Categories/GetAllAsync/GetAllCategoriesQueryHandlerTests.cs +++ b/tests/UnitTests/Features/Queries/GetAllCategoriesQueryHandlerTests.cs @@ -1,14 +1,4 @@ -using Application.Categories.GetAllAsync; -using Application.Categories.Shared; -using Application.Contexts; -using Domain.Cache; -using Domain.Categories; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Moq; -using static Domain.Extensions.Collections.Collections; - -namespace Application.UnitTests.Categories.GetAllAsync; +namespace UnitTests.Features.Queries; public sealed class GetAllCategoriesQueryHandlerTests { @@ -39,7 +29,7 @@ public async Task GetAllCategoriesQueryHandler_ShouldReturnPagedListOfRequestedC var categories = new List { new(Guid.NewGuid(), "Name 1", "Description 1"), - new(Guid.NewGuid(), "Name 2", "Description 2") + new(Guid.NewGuid(), "Name 2", "Description 2"), }; _dbSet .As>() diff --git a/tests/Application/UnitTests/Categories/GetAsync/GetCategoryQueryHandlerTests.cs b/tests/UnitTests/Features/Queries/GetCategoryQueryHandlerTests.cs similarity index 92% rename from tests/Application/UnitTests/Categories/GetAsync/GetCategoryQueryHandlerTests.cs rename to tests/UnitTests/Features/Queries/GetCategoryQueryHandlerTests.cs index ed65f1f..5ed8945 100644 --- a/tests/Application/UnitTests/Categories/GetAsync/GetCategoryQueryHandlerTests.cs +++ b/tests/UnitTests/Features/Queries/GetCategoryQueryHandlerTests.cs @@ -1,10 +1,3 @@ -using Application.Categories.GetAsync; -using Application.Contexts; -using Domain.Categories; -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Moq; - namespace Application.UnitTests.Categories.GetAsync; public sealed class GetCategoryQueryHandlerTests diff --git a/tests/UnitTests/GlobalUsings.cs b/tests/UnitTests/GlobalUsings.cs new file mode 100644 index 0000000..2317b2b --- /dev/null +++ b/tests/UnitTests/GlobalUsings.cs @@ -0,0 +1,24 @@ +global using System.Linq.Expressions; +global using Application.Abstractions; +global using Application.Contexts; +global using Application.Features.Commands; +global using static Application.Features.Commands.DeleteCategoryCommand; +global using static Application.Features.Commands.PatchCategoryCommand; +global using static Application.Features.Commands.PostCategoryCommand; +global using Application.Features.DomainEvents; +global using Application.Features.Queries; +global using static Application.Features.Queries.GetAllCategoriesQuery; +global using static Application.Features.Queries.GetCategoryQuery; +global using Application.Features.Shared; +global using Application.Repositories; +global using Domain.Cache; +global using Domain.Entities; +global using static Domain.Extensions.Collections.Collections; +global using Domain.Persistence; +global using FluentAssertions; +global using FluentValidation; +global using FluentValidation.Results; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.EntityFrameworkCore.Query; +global using Moq; +global using UnitTests.Shared; diff --git a/tests/Application/UnitTests/Shared/DbSetMock.cs b/tests/UnitTests/Shared/DbSetMock.cs similarity index 91% rename from tests/Application/UnitTests/Shared/DbSetMock.cs rename to tests/UnitTests/Shared/DbSetMock.cs index b061160..30e4407 100644 --- a/tests/Application/UnitTests/Shared/DbSetMock.cs +++ b/tests/UnitTests/Shared/DbSetMock.cs @@ -1,9 +1,6 @@ -using System.Linq.Expressions; -using Microsoft.EntityFrameworkCore.Query; +namespace UnitTests.Shared; -namespace Application.UnitTests; - -public class TestAsyncQueryProvider : IAsyncQueryProvider +public sealed class TestAsyncQueryProvider : IAsyncQueryProvider { private readonly IQueryProvider _inner; @@ -66,4 +63,4 @@ public TestAsyncEnumerator(IEnumerator inner) public ValueTask DisposeAsync() => new(Task.Run(() => _inner.Dispose())); public ValueTask MoveNextAsync() => new(_inner.MoveNext()); -} \ No newline at end of file +} diff --git a/tests/Application/Application.Tests.csproj b/tests/UnitTests/UnitTests.csproj similarity index 79% rename from tests/Application/Application.Tests.csproj rename to tests/UnitTests/UnitTests.csproj index d01d531..35c3b20 100644 --- a/tests/Application/Application.Tests.csproj +++ b/tests/UnitTests/UnitTests.csproj @@ -1,4 +1,5 @@ + net8.0 enable @@ -10,10 +11,10 @@ - - + - + + @@ -23,7 +24,8 @@ - + +