diff --git a/.template.config/dotnetcli.host.json b/.template.config/dotnetcli.host.json index 9ab3548..9aac2e0 100644 --- a/.template.config/dotnetcli.host.json +++ b/.template.config/dotnetcli.host.json @@ -2,6 +2,12 @@ "symbolInfo": { "UseOracle": { "longName": "use-oracle" + }, + "EnableContainerSupport": { + "longName": "enable-container-support" + }, + "UseController": { + "longName": "use-controller" } } } \ No newline at end of file diff --git a/.template.config/ide.host.json b/.template.config/ide.host.json index 9405d54..eb6e3e9 100644 --- a/.template.config/ide.host.json +++ b/.template.config/ide.host.json @@ -6,12 +6,28 @@ { "id": "UseOracle", "name": { - "text": "Use Oracle" + "text": "Use oracle" }, "description": { - "text": "Use Oracle for database (default is LocalDB)." + "text": "Use oracle for database (default is LocalDB)." }, "isVisible": true + }, + { + "id": "EnableContainerSupport", + "name": { + "text": "Enable container support" + }, + "description": { + "text": "Enable container support and add Docker file for linux." + }, + "isVisible": true + }, + { + "id": "UseController", + "name": { + "text": "Use controller instead of minimals APIs." + } } ] } \ No newline at end of file diff --git a/.template.config/template.json b/.template.config/template.json index 00940e3..053a712 100644 --- a/.template.config/template.json +++ b/.template.config/template.json @@ -31,6 +31,18 @@ "type": "computed", "value": "(!UseOracle)" }, + "UseController": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "true", + "description": "Use controller instead of minimals APIs." + }, + "EnableContainerSupport": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Enable container support (Docker for linux)." + }, "fcaRepositoryUrl": { "type": "generated", "generator": "constant", @@ -44,7 +56,7 @@ "generator": "constant", "replaces": "fcaPackageVersion", "parameters": { - "value": "8.0.8" + "value": "8.0.1" } } }, @@ -93,6 +105,19 @@ "appsettings.Oracle.json": "appsettings.json", "TestDatabase.Oracle.cs": "TestDatabase.cs" } + }, + { + "condition": "(!UseController)", + "exclude": [ + "src/API/Controllers" + ] + }, + { + "condition": "(!EnableContainerSupport)", + "exclude": [ + "src/API/Dockerfile", + ".dockerignore" + ] } ] } diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..ecd91fe --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,8 @@ + + + net8.0 + true + enable + enable + + \ No newline at end of file diff --git a/FastCleanArchitecture.nuspec b/FastCleanArchitecture.nuspec index 64f62c3..2926f76 100644 --- a/FastCleanArchitecture.nuspec +++ b/FastCleanArchitecture.nuspec @@ -1,33 +1,35 @@ - + - Fast.Clean.Architecture.Solution.Template - 8.0.1 - Fast Clean Architecture Solution Template - christiandr - Fast Clean Architecture Solution Template for .NET 8. - - A Clean Architecture Solution Template for creating apps using Web API only with DotNet. - + Fast.Clean.Architecture.Solution.Template + 8.0.1 + Fast Clean Architecture Solution Template + christiandr + Fast Clean Architecture Solution Template for .NET 8. + + A Clean Architecture Solution Template for create apps. + + + - https://github.com/christianrd/FastCleanArchitecture - + https://github.com/christianrd/FastCleanArchitecture + - MIT - false - fast-clean-architecture clean-architecture project template csharp dotnet - icon.png - README.md + MIT + false + fast-clean-architecture clean-architecture project template csharp dotnet + icon.png + README.md - - - - + + + + - - - - - + + + + + \ No newline at end of file diff --git a/README.md b/README.md index d9b0a7f..2107491 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ Starting is quick and easy-just install the .NET template (detailed instructions The following prerequisites are required to build and run the solution: - [.NET 8.0 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) (latest version) -- [Node.js](https://nodejs.org/) (latest LTS, only required if you are using Angular or React) The easiest way to get started is to install the [.NET template](https://www.nuget.org/packages/Fast.Clean.Architecture.Solution.Template): ``` @@ -85,6 +84,8 @@ The template includes a full CI/CD pipeline. The pipeline is responsible for bui * [ASP.NET Core 8](https://docs.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core) * [Entity Framework Core 8](https://docs.microsoft.com/en-us/ef/core/) * [MediatR](https://github.com/jbogard/MediatR) +* [Maspter](https://github.com/MapsterMapper/Mapster) +* [FluentResult](https://github.com/altmann/FluentResults) * [FluentValidation](https://fluentvalidation.net/) * [NUnit](https://nunit.org/), [FluentAssertions](https://fluentassertions.com/), [Moq](https://github.com/devlooped/moq) & [Respawn](https://github.com/jbogard/Respawn) diff --git a/fastCleanArchitecture.sln b/fastCleanArchitecture.sln index 3a96ac0..b47c5bb 100644 --- a/fastCleanArchitecture.sln +++ b/fastCleanArchitecture.sln @@ -19,10 +19,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "src\Infra EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "API", "src\API\API.csproj", "{3A7EAD0E-2F46-4022-9119-96593FFEC72D}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{C7FBE7FF-00ED-4E1B-B015-CC61AE8E0FD3}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C7FBE7FF-00ED-4E1B-B015-CC61AE8E0FD3}" ProjectSection(SolutionItems) = preProject .gitignore = .gitignore FastCleanArchitecture.nuspec = FastCleanArchitecture.nuspec + README-template.md = README-template.md README.md = README.md EndProjectSection EndProject diff --git a/src/API/API.csproj b/src/API/API.csproj index 77031d3..cb385fc 100644 --- a/src/API/API.csproj +++ b/src/API/API.csproj @@ -5,8 +5,10 @@ enable enable f5e32e1e-4b42-4557-9635-b9a0c84f3591 + Linux ..\.. + FastCleanArchitecture $(CompanyName) $(CompanyName.Replace(" ", "")).$(MSBuildProjectName.Replace(" ", "")) @@ -14,7 +16,9 @@ + + all diff --git a/src/API/Controllers/BaseController.cs b/src/API/Controllers/ApiControllerBase.cs similarity index 55% rename from src/API/Controllers/BaseController.cs rename to src/API/Controllers/ApiControllerBase.cs index e01a552..742241c 100644 --- a/src/API/Controllers/BaseController.cs +++ b/src/API/Controllers/ApiControllerBase.cs @@ -4,9 +4,9 @@ namespace FastCleanArchitecture.API.Controllers; [ApiController] -public abstract class BaseController : ControllerBase +public abstract class ApiControllerBase : ControllerBase { protected readonly ISender Sender; - public BaseController(ISender Sender) => this.Sender = Sender; -} \ No newline at end of file + public ApiControllerBase(ISender Sender) => this.Sender = Sender; +} diff --git a/src/API/Controllers/TodoItems/TodoItemsController.cs b/src/API/Controllers/TodoItems/TodoItemsController.cs new file mode 100644 index 0000000..7704de1 --- /dev/null +++ b/src/API/Controllers/TodoItems/TodoItemsController.cs @@ -0,0 +1,36 @@ +using FastCleanArchitecture.Application.TodoItems.Commands.CreateTodoItem; +using FastCleanArchitecture.Application.TodoItems.Commands.DeleteTodoItem; +using FastCleanArchitecture.Application.TodoItems.Commands.UpdateTodoItem; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace FastCleanArchitecture.API.Controllers.TodoItems; + +[Route("api/[controller]")] +public sealed class TodoItemsController(ISender sender) : ApiControllerBase(sender) +{ + [HttpPost] + public async Task CreateTodoItem([FromBody] CreateTodoItemCommand request) + { + await Sender.Send(request); + + return NoContent(); + } + + [HttpPut("{Id}")] + public async Task UpdateTodoItem(UpdateTodoItemCommand request) + { + var result = await Sender.Send(request); + if (result.IsFailed) + return NotFound("Item not found."); + + return NoContent(); + } + + [HttpDelete("{Id}")] + public async Task DeleteTodoItem([FromRoute] DeleteTodoItemCommand request) + { + await Sender.Send(request); + return NoContent(); + } +} diff --git a/src/API/Controllers/TodoLists/TodoListsController.cs b/src/API/Controllers/TodoLists/TodoListsController.cs new file mode 100644 index 0000000..26bb0c1 --- /dev/null +++ b/src/API/Controllers/TodoLists/TodoListsController.cs @@ -0,0 +1,46 @@ +using FastCleanArchitecture.Application.TodoLists.Commands.Create; +using FastCleanArchitecture.Application.TodoLists.Commands.Delete; +using FastCleanArchitecture.Application.TodoLists.Commands.Update; +using FastCleanArchitecture.Application.TodoLists.Queries.GetTodos; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace FastCleanArchitecture.API.Controllers.TodoLists; + +[Route("api/[controller]")] +public sealed class TodoListsController(ISender Sender) : ApiControllerBase(Sender) +{ + [HttpGet(Name = nameof(GetTodoLists))] + public async Task GetTodoLists() + { + var result = await Sender.Send(new GetTodosQuery()); + return Ok(result.ValueOrDefault); + } + + [HttpPost] + public async Task CreateTodoList(CreateTodoListCommand request) + { + var result = await Sender.Send(request); + if (result.IsFailed) + return BadRequest(result.Errors); + + return CreatedAtRoute(nameof(GetTodoLists), result.Value); + } + + [HttpPut("{Id}")] + public async Task UpdateTodoList(UpdateTodoListCommand request) + { + var result = await Sender.Send(request); + if (result.IsFailed) + return BadRequest(result.Errors); + + return NoContent(); + } + + [HttpDelete("{Id}")] + public async Task DeleteTodoList([FromRoute] DeleteTodoListCommand request) + { + await Sender.Send(request); + return NoContent(); + } +} diff --git a/src/API/Controllers/TodoListsController.cs b/src/API/Controllers/TodoListsController.cs deleted file mode 100644 index 0b5c2d0..0000000 --- a/src/API/Controllers/TodoListsController.cs +++ /dev/null @@ -1,19 +0,0 @@ -using FastCleanArchitecture.Application.TodoLists.Commands.CreatTodoList; -using MediatR; -using Microsoft.AspNetCore.Mvc; - -namespace FastCleanArchitecture.API.Controllers; - -[Route("api/[controller]")] -public sealed class TodoListsController(ISender Sender) : BaseController(Sender) -{ - [HttpPost] - public async Task CreateTodoList(CreateTodoListCommand request) - { - var result = await Sender.Send(request); - if (result.IsFailed) - return BadRequest(result.Errors); - - return NoContent(); - } -} diff --git a/src/API/Program.cs b/src/API/Program.cs index 672354a..a03a876 100644 --- a/src/API/Program.cs +++ b/src/API/Program.cs @@ -2,8 +2,9 @@ using FastCleanArchitecture.Infrastructure; var builder = WebApplication.CreateBuilder(args); - +#if (UseController) builder.Services.AddControllers(); +#endif builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -22,8 +23,10 @@ app.UseHttpsRedirection(); +#if (UseController) app.MapControllers(); +#endif await app.UseInfrastructureAsync(); -app.Run(); \ No newline at end of file +app.Run(); diff --git a/src/Application/.templating.config/dotnetcli.host.json b/src/Application/.templating.config/dotnetcli.host.json new file mode 100644 index 0000000..d4447a8 --- /dev/null +++ b/src/Application/.templating.config/dotnetcli.host.json @@ -0,0 +1,16 @@ +{ + "symbolInfo": { + "featureName": { + "longName": "feature-name", + "shortName": "fn" + }, + "returnType": { + "longName": "return-type", + "shortName": "rt" + }, + "useCaseType": { + "longName": "usecase-type", + "shortName": "ut" + } + } +} \ No newline at end of file diff --git a/src/Application/.templating.config/icon.png b/src/Application/.templating.config/icon.png new file mode 100644 index 0000000..68e954e Binary files /dev/null and b/src/Application/.templating.config/icon.png differ diff --git a/src/Application/.templating.config/template.json b/src/Application/.templating.config/template.json new file mode 100644 index 0000000..3635e40 --- /dev/null +++ b/src/Application/.templating.config/template.json @@ -0,0 +1,78 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "christiandr", + "classifications": [ + "Fast Clean Architecture" + ], + "name": "Fast Clean Architecture Solution Use Case", + "description": "Create a new use case (query or command)", + "identity": "Fast.Clean.Architecture.Solution.UseCase.CSharp", + "groupIdentity": "Fast.Clean.Architecture.Solution.UseCase", + "shortName": "fast-ca-usecase", + "tags": { + "language": "C#", + "type": "item" + }, + "sourceName": "UpdateTodoItemDetail", + "preferNameDirectory": false, + "symbols": { + "DefaultNamespace": { + "type": "bind", + "binding": "msbuild:RootNamespace", + "replaces": "FastCleanArchitecture.Application", + "defaultValue": "FastCleanArchitecture.Application" + }, + "featureName": { + "type": "parameter", + "datatype": "string", + "isRequired": true, + "replaces": "TodoItems", + "fileRename": "TodoItems" + }, + "useCaseType": { + "type": "parameter", + "datatype": "choice", + "isRequired": true, + "choices": [ + { + "choice": "command", + "description": "Create a new command" + }, + { + "choice": "query", + "description": "Create a new query" + } + ], + "description": "The type of use case to create" + }, + "createCommand": { + "type": "computed", + "value": "(useCaseType == \"command\")" + }, + "createQuery": { + "type": "computed", + "value": "(useCaseType == \"query\")" + }, + "returnType": { + "type": "parameter", + "datatype": "string", + "isRequired": false, + "replaces": "object", + "defaultValue": "object" + } + }, + "sources": [ + { + "modifiers": [ + { + "condition": "(createCommand)", + "exclude": [ "TodoItems/Queries/**/*" ] + }, + { + "condition": "(createQuery)", + "exclude": [ "TodoItems/Commands/**/*" ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj index ba890d0..95ab0f7 100644 --- a/src/Application/Application.csproj +++ b/src/Application/Application.csproj @@ -12,7 +12,10 @@ - + + + + diff --git a/src/Application/Common/Behaviors/LoggingBehavior.cs b/src/Application/Common/Behaviors/LoggingBehavior.cs index 4f25596..4bfdac3 100644 --- a/src/Application/Common/Behaviors/LoggingBehavior.cs +++ b/src/Application/Common/Behaviors/LoggingBehavior.cs @@ -20,18 +20,18 @@ public async Task Handle(TRequest request, RequestHandlerDelegate : IPipelineBehavior where TRequest : notnull +{ + private readonly Stopwatch _timer; + private readonly ILogger _logger; + + public PerformanceBehavior(Stopwatch timer, ILogger logger) + { + _timer = timer; + _logger = logger; + } + + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + _timer.Start(); + + var response = await next(); + + _timer.Stop(); + + var elapsedMilliseconds = _timer.ElapsedMilliseconds; + + if (elapsedMilliseconds > 500) + { + var requestName = typeof(TRequest).Name; + + _logger.LogWarning("FastCleanArchitecture Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@Request}", + requestName, elapsedMilliseconds, request); + } + + return response; + } +} diff --git a/src/Application/Common/Behaviors/ValidationBehavior.cs b/src/Application/Common/Behaviors/ValidationBehavior.cs index 2161066..aaa84b7 100644 --- a/src/Application/Common/Behaviors/ValidationBehavior.cs +++ b/src/Application/Common/Behaviors/ValidationBehavior.cs @@ -21,10 +21,11 @@ public async Task Handle(TRequest request, RequestHandlerDelegate(request); + var validationResults = await Task.WhenAll( + _validators.Select(v => v.ValidateAsync(context, cancellationToken))); - var validationErrors = _validators - .Select(validator => validator.Validate(context)) - .Where(validationResult => validationResult.Errors.Any()) + var validationErrors = validationResults + .Where(vr => vr.Errors.Any()) .SelectMany(validationResult => validationResult.Errors) .Select(validationFailure => new ValidationError( validationFailure.PropertyName, @@ -36,4 +37,4 @@ public async Task Handle(TRequest request, RequestHandlerDelegate.NewConfig() + .Map(dest => dest.Priority, src => (int)src.Priority); + + TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); + } +} diff --git a/src/Application/Common/Models/LookupDto.cs b/src/Application/Common/Models/LookupDto.cs new file mode 100644 index 0000000..df48646 --- /dev/null +++ b/src/Application/Common/Models/LookupDto.cs @@ -0,0 +1,7 @@ +namespace FastCleanArchitecture.Application.Common.Models; + +public sealed class LookupDto +{ + public int Id { get; set; } + public string? Title { get; set; } +} diff --git a/src/Application/DependencyInjection.cs b/src/Application/DependencyInjection.cs index 03217dc..472200e 100644 --- a/src/Application/DependencyInjection.cs +++ b/src/Application/DependencyInjection.cs @@ -1,5 +1,8 @@ using FastCleanArchitecture.Application.Common.Behaviors; +using FastCleanArchitecture.Application.Common.Mappings; using FluentValidation; +using Mapster; +using MediatR; using Microsoft.Extensions.DependencyInjection; using System.Diagnostics.CodeAnalysis; @@ -15,9 +18,14 @@ public static IServiceCollection AddApplication(this IServiceCollection services conf.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly); conf.AddOpenBehavior(typeof(LoggingBehavior<,>)); conf.AddOpenBehavior(typeof(ValidationBehavior<,>)); + conf.AddBehavior(typeof(IPipelineBehavior<,>), typeof(PerformanceBehavior<,>)); }); + services.AddMapster(); + MapsterConfig.Configure(); + services.AddValidatorsFromAssembly(typeof(DependencyInjection).Assembly); + return services; } -} \ No newline at end of file +} diff --git a/src/Application/TodoItems/Commands/CreateTodoItem/CreateTodoItem.cs b/src/Application/TodoItems/Commands/CreateTodoItem/CreateTodoItem.cs new file mode 100644 index 0000000..c22c679 --- /dev/null +++ b/src/Application/TodoItems/Commands/CreateTodoItem/CreateTodoItem.cs @@ -0,0 +1,34 @@ +using FastCleanArchitecture.Application.Common.Messaging; +using FastCleanArchitecture.Domain.Common; +using FastCleanArchitecture.Domain.TodoItems; +using FluentResults; + +namespace FastCleanArchitecture.Application.TodoItems.Commands.CreateTodoItem; + +public record CreateTodoItemCommand : ICommand +{ + public Guid ListId { get; set; } + public string? Title { get; set; } +} + +internal sealed class CreateTodoItemCommandCommandHandler : ICommandHandler +{ + private readonly ITodoItemRepository _repository; + private readonly IUnitOfWork _unitOfWork; + + public CreateTodoItemCommandCommandHandler(ITodoItemRepository repository, IUnitOfWork unitOfWork) + { + _repository = repository; + _unitOfWork = unitOfWork; + } + + public async Task> Handle(CreateTodoItemCommand request, CancellationToken cancellationToken) + { + var entity = TodoItem.Create(request.ListId, request.Title); + + _repository.Add(entity); + await _unitOfWork.SaveChangesAsync(); + + return entity.Id; + } +} diff --git a/src/Application/TodoItems/Commands/CreateTodoItem/CreateTodoItemCommandValidator.cs b/src/Application/TodoItems/Commands/CreateTodoItem/CreateTodoItemCommandValidator.cs new file mode 100644 index 0000000..bf32b6b --- /dev/null +++ b/src/Application/TodoItems/Commands/CreateTodoItem/CreateTodoItemCommandValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; + +namespace FastCleanArchitecture.Application.TodoItems.Commands.CreateTodoItem; + +public class CreateTodoItemCommandValidator : AbstractValidator +{ + public CreateTodoItemCommandValidator() + { + RuleFor(v => v.Title) + .MaximumLength(200) + .NotEmpty(); + } +} diff --git a/src/Application/TodoItems/Commands/DeleteTodoItem/DeleteTodoItem.cs b/src/Application/TodoItems/Commands/DeleteTodoItem/DeleteTodoItem.cs new file mode 100644 index 0000000..0148cf4 --- /dev/null +++ b/src/Application/TodoItems/Commands/DeleteTodoItem/DeleteTodoItem.cs @@ -0,0 +1,34 @@ +using FastCleanArchitecture.Application.Common.Messaging; +using FastCleanArchitecture.Domain.Common; +using FastCleanArchitecture.Domain.TodoItems; +using FluentResults; +using MediatR; + +namespace FastCleanArchitecture.Application.TodoItems.Commands.DeleteTodoItem; + +public record DeleteTodoItemCommand(Guid Id) : ICommand; + +internal sealed class DeleteTodoItemCommandHandler : ICommandHandler +{ + private readonly ITodoItemRepository _repository; + private readonly IUnitOfWork _unitOfWork; + + public DeleteTodoItemCommandHandler(ITodoItemRepository repository, IUnitOfWork unitOfWork) + { + _repository = repository; + _unitOfWork = unitOfWork; + } + + public async Task Handle(DeleteTodoItemCommand request, CancellationToken cancellationToken) + { + var entity = await _repository.GetByIdAsync(request.Id, cancellationToken); + + if (entity is null) + return Result.Ok(); + + _repository.Remove(entity); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Ok(); + } +} diff --git a/src/Application/TodoItems/Commands/UpdateTodoItem/UpdateTodoItem.cs b/src/Application/TodoItems/Commands/UpdateTodoItem/UpdateTodoItem.cs new file mode 100644 index 0000000..d6b8e17 --- /dev/null +++ b/src/Application/TodoItems/Commands/UpdateTodoItem/UpdateTodoItem.cs @@ -0,0 +1,46 @@ +using FastCleanArchitecture.Application.Common.Messaging; +using FastCleanArchitecture.Domain.Common; +using FastCleanArchitecture.Domain.TodoItems; +using FluentResults; +using Microsoft.AspNetCore.Mvc; + +namespace FastCleanArchitecture.Application.TodoItems.Commands.UpdateTodoItem; + +public sealed record UpdateTodoItemCommand : ICommand +{ + [FromRoute] + public Guid Id { get; init; } + + [FromBody] + public BodyItemRequest Body { get; set; } = new BodyItemRequest(); + + public record BodyItemRequest + { + public string? Title { get; init; } + public bool Done { get; init; } + } +} + +internal sealed class UpdateTodoItemCommandHandler : ICommandHandler +{ + private readonly ITodoItemRepository _repository; + private readonly IUnitOfWork _unitOfWork; + + public UpdateTodoItemCommandHandler(ITodoItemRepository repository, IUnitOfWork unitOfWork) + { + _repository = repository; + _unitOfWork = unitOfWork; + } + + public async Task Handle(UpdateTodoItemCommand request, CancellationToken cancellationToken) + { + var item = await _repository.GetByIdAsync(request.Id, cancellationToken); + + if (item is null) return Result.Fail("Item not found."); + + _repository.Update(TodoItem.Update(request.Body.Title, request.Body.Done, item)); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Ok(); + } +} diff --git a/src/Application/TodoItems/Commands/UpdateTodoItem/UpdateTodoItemCommandValidator.cs b/src/Application/TodoItems/Commands/UpdateTodoItem/UpdateTodoItemCommandValidator.cs new file mode 100644 index 0000000..3d8df2b --- /dev/null +++ b/src/Application/TodoItems/Commands/UpdateTodoItem/UpdateTodoItemCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; + +namespace FastCleanArchitecture.Application.TodoItems.Commands.UpdateTodoItem; + +public class UpdateTodoItemCommandValidator : AbstractValidator +{ + public UpdateTodoItemCommandValidator() + { + RuleFor(v => v.Body.Title) + .MaximumLength(200) + .NotEmpty() + .OverridePropertyName(x => x.Body.Title); + } +} diff --git a/src/Application/TodoItems/Commands/UpdateTodoItemDetail/UpdateTodoItemDetail.cs b/src/Application/TodoItems/Commands/UpdateTodoItemDetail/UpdateTodoItemDetail.cs new file mode 100644 index 0000000..d11d6be --- /dev/null +++ b/src/Application/TodoItems/Commands/UpdateTodoItemDetail/UpdateTodoItemDetail.cs @@ -0,0 +1,51 @@ +using FastCleanArchitecture.Application.Common.Messaging; +using FastCleanArchitecture.Domain.Common; +using FastCleanArchitecture.Domain.TodoItems; +using FastCleanArchitecture.Domain.TodoItems.Enums; +using FluentResults; +using Microsoft.AspNetCore.Mvc; +using static FastCleanArchitecture.Application.TodoItems.Commands.UpdateTodoItem.UpdateTodoItemCommand; + +namespace FastCleanArchitecture.Application.TodoItems.Commands.UpdateTodoItemDetail; + +public sealed record UpdateTodoItemDetailCommand : ICommand +{ + [FromRoute] + public Guid Id { get; init; } + + [FromBody] + public BodyItemDetailRequest Body { get; set; } = new BodyItemDetailRequest(); + + public record BodyItemDetailRequest + { + public Guid ListId { get; init; } + + public PriorityLevel Priority { get; init; } + + public string? Note { get; init; } + } +} + +internal sealed class UpdateTodoItemDetailCommandHandler : ICommandHandler +{ + private readonly ITodoItemRepository _todoItemRepository; + private readonly IUnitOfWork _unitOfWork; + + public UpdateTodoItemDetailCommandHandler(ITodoItemRepository todoItemRepository, IUnitOfWork unitOfWork) + { + _todoItemRepository = todoItemRepository; + _unitOfWork = unitOfWork; + } + + public async Task Handle(UpdateTodoItemDetailCommand request, CancellationToken cancellationToken) + { + var item = await _todoItemRepository.GetByIdAsync(request.Id, cancellationToken); + + if (item is null) return Result.Fail("Item not found."); + + _todoItemRepository.Update(TodoItem.UpdateDetail(request.Body.ListId, request.Body.Priority, request.Body.Note, item)); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Ok(); + } +} diff --git a/src/Application/TodoItems/EventHandlers/TodoItemCreatedEventHandler.cs b/src/Application/TodoItems/EventHandlers/TodoItemCreatedEventHandler.cs new file mode 100644 index 0000000..e0d3b82 --- /dev/null +++ b/src/Application/TodoItems/EventHandlers/TodoItemCreatedEventHandler.cs @@ -0,0 +1,15 @@ +using FastCleanArchitecture.Domain.TodoItems.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace FastCleanArchitecture.Application.TodoItems.EventHandlers; + +internal sealed class TodoItemCreatedEventHandler(ILogger logger) + : INotificationHandler +{ + public Task Handle(TodoItemCreatedEvent notification, CancellationToken cancellationToken) + { + logger.LogInformation("FastCleanAchitecture Domain Event: {DomainEvent}", notification.GetType().Name); + return Task.CompletedTask; + } +} diff --git a/src/Application/TodoLists/Commands/CreatTodoList/CreateTodoList.cs b/src/Application/TodoLists/Commands/Create/CreateTodoList.cs similarity index 98% rename from src/Application/TodoLists/Commands/CreatTodoList/CreateTodoList.cs rename to src/Application/TodoLists/Commands/Create/CreateTodoList.cs index abc7bc0..5f8b825 100644 --- a/src/Application/TodoLists/Commands/CreatTodoList/CreateTodoList.cs +++ b/src/Application/TodoLists/Commands/Create/CreateTodoList.cs @@ -3,7 +3,7 @@ using FastCleanArchitecture.Domain.TodoLists; using FluentResults; -namespace FastCleanArchitecture.Application.TodoLists.Commands.CreatTodoList; +namespace FastCleanArchitecture.Application.TodoLists.Commands.Create; public record CreateTodoListCommand : ICommand { diff --git a/src/Application/TodoLists/Commands/CreatTodoList/CreateTodoListCommandValidator.cs b/src/Application/TodoLists/Commands/Create/CreateTodoListCommandValidator.cs similarity index 94% rename from src/Application/TodoLists/Commands/CreatTodoList/CreateTodoListCommandValidator.cs rename to src/Application/TodoLists/Commands/Create/CreateTodoListCommandValidator.cs index 9fff607..bbef5fc 100644 --- a/src/Application/TodoLists/Commands/CreatTodoList/CreateTodoListCommandValidator.cs +++ b/src/Application/TodoLists/Commands/Create/CreateTodoListCommandValidator.cs @@ -1,7 +1,7 @@ using FastCleanArchitecture.Domain.TodoLists; using FluentValidation; -namespace FastCleanArchitecture.Application.TodoLists.Commands.CreatTodoList; +namespace FastCleanArchitecture.Application.TodoLists.Commands.Create; public class CreateTodoListCommandValidator : AbstractValidator { @@ -23,6 +23,6 @@ public async Task BeUniqueTitle(string title, CancellationToken cancellati { var entity = await _todoListRepository.GetByTitleAsync(title, cancellationToken); - return entity is not null; + return entity is null; } -} \ No newline at end of file +} diff --git a/src/Application/TodoLists/Commands/Delete/DeleteTodoList.cs b/src/Application/TodoLists/Commands/Delete/DeleteTodoList.cs new file mode 100644 index 0000000..42d1c65 --- /dev/null +++ b/src/Application/TodoLists/Commands/Delete/DeleteTodoList.cs @@ -0,0 +1,32 @@ +using FastCleanArchitecture.Application.Common.Messaging; +using FastCleanArchitecture.Domain.Common; +using FastCleanArchitecture.Domain.TodoLists; +using FluentResults; + +namespace FastCleanArchitecture.Application.TodoLists.Commands.Delete; + +public record DeleteTodoListCommand(Guid Id) : ICommand; + +internal sealed class DeleteTodoListCommandHandler : ICommandHandler +{ + private readonly ITodoListRepository _todoListRepository; + private readonly IUnitOfWork _unitOfWork; + + public DeleteTodoListCommandHandler(ITodoListRepository todoListRepository, IUnitOfWork unitOfWork) + { + _todoListRepository = todoListRepository; + _unitOfWork = unitOfWork; + } + + public async Task Handle(DeleteTodoListCommand request, CancellationToken cancellationToken) + { + var entity = await _todoListRepository.GetByIdAsync(request.Id, cancellationToken); + if (entity is null) + return Result.Ok(); + + _todoListRepository.Remove(entity); + await _unitOfWork.SaveChangesAsync(); + + return Result.Ok(); + } +} diff --git a/src/Application/TodoLists/Commands/Purge/PurgeTodoLists.cs b/src/Application/TodoLists/Commands/Purge/PurgeTodoLists.cs new file mode 100644 index 0000000..c0934e9 --- /dev/null +++ b/src/Application/TodoLists/Commands/Purge/PurgeTodoLists.cs @@ -0,0 +1,29 @@ +using FastCleanArchitecture.Application.Common.Messaging; +using FastCleanArchitecture.Domain.Common; +using FastCleanArchitecture.Domain.TodoLists; +using FluentResults; + +namespace FastCleanArchitecture.Application.TodoLists.Commands.Purge; + +public record PurgeTodoListsCommand : ICommand; + +internal sealed class PurgeTodoListsCommandHandler : ICommandHandler +{ + private readonly ITodoListRepository _todoListRepository; + private readonly IUnitOfWork _unitOfWork; + + public PurgeTodoListsCommandHandler(ITodoListRepository todoListRepository, IUnitOfWork unitOfWork) + { + _todoListRepository = todoListRepository; + _unitOfWork = unitOfWork; + } + + public async Task Handle(PurgeTodoListsCommand request, CancellationToken cancellationToken) + { + _todoListRepository.RemoveRange(await _todoListRepository.GetAllAsync(cancellationToken)); + + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Ok(); + } +} diff --git a/src/Application/TodoLists/Commands/Update/UpdateTodoList.cs b/src/Application/TodoLists/Commands/Update/UpdateTodoList.cs new file mode 100644 index 0000000..b3460a5 --- /dev/null +++ b/src/Application/TodoLists/Commands/Update/UpdateTodoList.cs @@ -0,0 +1,45 @@ +using FastCleanArchitecture.Application.Common.Messaging; +using FastCleanArchitecture.Domain.Common; +using FastCleanArchitecture.Domain.TodoLists; +using FluentResults; +using Microsoft.AspNetCore.Mvc; + +namespace FastCleanArchitecture.Application.TodoLists.Commands.Update; + +public record UpdateTodoListCommand : ICommand +{ + [FromRoute] + public Guid Id { get; init; } + + [FromBody] + public BodyListRequest Body { get; set; } = new BodyListRequest(); + + public record BodyListRequest + { + public string? Title { get; init; } + } +} + +internal sealed class UpdateTodoListCommandHandler : ICommandHandler +{ + private readonly ITodoListRepository _repository; + private readonly IUnitOfWork _unitOfWork; + + public UpdateTodoListCommandHandler(ITodoListRepository repository, IUnitOfWork unitOfWork) + { + _repository = repository; + _unitOfWork = unitOfWork; + } + + public async Task Handle(UpdateTodoListCommand request, CancellationToken cancellationToken) + { + var entity = await _repository.GetByIdAsync(request.Id, cancellationToken); + if (entity is null) + return Result.Fail("Todo list not found."); + + _repository.Update(TodoList.UpdateTitle(request.Body.Title!, entity)); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Ok(); + } +} diff --git a/src/Application/TodoLists/Commands/Update/UpdateTodoListCommandValidator.cs b/src/Application/TodoLists/Commands/Update/UpdateTodoListCommandValidator.cs new file mode 100644 index 0000000..3d27624 --- /dev/null +++ b/src/Application/TodoLists/Commands/Update/UpdateTodoListCommandValidator.cs @@ -0,0 +1,28 @@ +using FastCleanArchitecture.Domain.TodoLists; +using FluentValidation; + +namespace FastCleanArchitecture.Application.TodoLists.Commands.Update; + +public class UpdateTodoListCommandValidator : AbstractValidator +{ + private readonly ITodoListRepository _todoListRepository; + + public UpdateTodoListCommandValidator(ITodoListRepository todoListRepository) + { + _todoListRepository = todoListRepository; + + RuleFor(v => v.Body.Title) + .NotEmpty() + .MaximumLength(200) + .MustAsync(BeUniqueTitle) + .WithMessage("'{PropertyName}' must be unique.") + .WithErrorCode("Unique") + .OverridePropertyName(x => x.Body.Title); + } + + public async Task BeUniqueTitle(UpdateTodoListCommand request, string title, CancellationToken cancellationToken) + { + var entity = await _todoListRepository.GetByIdAsync(request.Id, cancellationToken); + return entity is not null && entity!.Title!.Equals(title, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/src/Application/TodoLists/Queries/GetTodos/GetTodos.cs b/src/Application/TodoLists/Queries/GetTodos/GetTodos.cs new file mode 100644 index 0000000..441ac50 --- /dev/null +++ b/src/Application/TodoLists/Queries/GetTodos/GetTodos.cs @@ -0,0 +1,28 @@ +using FastCleanArchitecture.Application.Common.Messaging; +using FastCleanArchitecture.Application.Common.Models; +using FastCleanArchitecture.Domain.TodoItems.Enums; +using FastCleanArchitecture.Domain.TodoLists; +using FluentResults; +using Mapster; + +namespace FastCleanArchitecture.Application.TodoLists.Queries.GetTodos; + +public record GetTodosQuery : IQuery; + +internal sealed class GetTodosQueryHandler(ITodoListRepository todoListRepository) : IQueryHandler +{ + public async Task> Handle(GetTodosQuery request, CancellationToken cancellationToken) + { + var todoLists = await todoListRepository.GetAllAsync(cancellationToken); + + return new TodosVm + { + PriorityLevels = Enum.GetValues(typeof(PriorityLevel)) + .Cast() + .Select(priorityLevel => new LookupDto { Id = (int)priorityLevel, Title = priorityLevel.ToString() }) + .ToList(), + + Lists = todoLists.Adapt>() + }; + } +} diff --git a/src/Application/TodoLists/Queries/GetTodos/TodoItemDto.cs b/src/Application/TodoLists/Queries/GetTodos/TodoItemDto.cs new file mode 100644 index 0000000..8864311 --- /dev/null +++ b/src/Application/TodoLists/Queries/GetTodos/TodoItemDto.cs @@ -0,0 +1,10 @@ +namespace FastCleanArchitecture.Application.TodoLists.Queries.GetTodos; + +public sealed class TodoItemDto +{ + public Guid Id { get; set; } + public string? Title { get; set; } + public bool Done { get; set; } + public int Priority { get; set; } + public string? Note { get; set; } +} diff --git a/src/Application/TodoLists/Queries/GetTodos/TodoListDto.cs b/src/Application/TodoLists/Queries/GetTodos/TodoListDto.cs new file mode 100644 index 0000000..c2fb6c5 --- /dev/null +++ b/src/Application/TodoLists/Queries/GetTodos/TodoListDto.cs @@ -0,0 +1,12 @@ +namespace FastCleanArchitecture.Application.TodoLists.Queries.GetTodos; + +public sealed class TodoListDto +{ + public Guid Id { get; set; } + + public string? Title { get; set; } + + public string? Colour { get; set; } + + public IReadOnlyCollection Items { get; set; } = []; +} diff --git a/src/Application/TodoLists/Queries/GetTodos/TodosVm.cs b/src/Application/TodoLists/Queries/GetTodos/TodosVm.cs new file mode 100644 index 0000000..bd791a5 --- /dev/null +++ b/src/Application/TodoLists/Queries/GetTodos/TodosVm.cs @@ -0,0 +1,9 @@ +using FastCleanArchitecture.Application.Common.Models; + +namespace FastCleanArchitecture.Application.TodoLists.Queries.GetTodos; + +public sealed class TodosVm +{ + public IReadOnlyCollection PriorityLevels { get; set; } = []; + public IReadOnlyCollection Lists { get; set; } = []; +} diff --git a/src/Domain/Common/BaseAuditableEntity.cs b/src/Domain/Common/BaseAuditableEntity.cs index ab1d102..1c5ddb1 100644 --- a/src/Domain/Common/BaseAuditableEntity.cs +++ b/src/Domain/Common/BaseAuditableEntity.cs @@ -1,9 +1,17 @@ namespace FastCleanArchitecture.Domain.Common; -public abstract class BaseAuditableEntity(Guid id) : BaseEntity(id) +public abstract class BaseAuditableEntity : BaseEntity { + protected BaseAuditableEntity(Guid id) : base(id) + { + } + + protected BaseAuditableEntity() : base() + { + } + public DateTimeOffset CreatedAtUtc { get; protected set; } = DateTimeOffset.UtcNow; public string? CreatedBy { get; protected set; } public DateTimeOffset ModifiedAtUtc { get; protected set; } public string? ModifiedBy { get; protected set; } -} \ No newline at end of file +} diff --git a/src/Domain/TodoItems/Events/TodoItemCreatedEvent.cs b/src/Domain/TodoItems/Events/TodoItemCreatedEvent.cs new file mode 100644 index 0000000..34ee042 --- /dev/null +++ b/src/Domain/TodoItems/Events/TodoItemCreatedEvent.cs @@ -0,0 +1,5 @@ +using FastCleanArchitecture.Domain.Common; + +namespace FastCleanArchitecture.Domain.TodoItems.Events; + +public sealed record TodoItemCreatedEvent(TodoItem Item) : IDomainEvent; diff --git a/src/Domain/TodoItems/ITodoItemRepository.cs b/src/Domain/TodoItems/ITodoItemRepository.cs index 6b567dd..f9c87b1 100644 --- a/src/Domain/TodoItems/ITodoItemRepository.cs +++ b/src/Domain/TodoItems/ITodoItemRepository.cs @@ -2,5 +2,29 @@ public interface ITodoItemRepository { - void Add(TodoItem item); -} \ No newline at end of file + /// + /// Get Item by id. + /// + /// + /// + /// + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + + /// + /// Add item to the list. + /// + /// + void Add(TodoItem todoItem); + + /// + /// Delete a item. + /// + /// + void Remove(TodoItem todoItem); + + /// + /// Update a item. + /// + /// + void Update(TodoItem todoItem); +} diff --git a/src/Domain/TodoItems/TodoItem.cs b/src/Domain/TodoItems/TodoItem.cs index 035ef65..20ae6a6 100644 --- a/src/Domain/TodoItems/TodoItem.cs +++ b/src/Domain/TodoItems/TodoItem.cs @@ -1,5 +1,6 @@ using FastCleanArchitecture.Domain.Common; using FastCleanArchitecture.Domain.TodoItems.Enums; +using FastCleanArchitecture.Domain.TodoItems.Events; namespace FastCleanArchitecture.Domain.TodoItems; @@ -15,6 +16,9 @@ private TodoItem(Guid id, Guid listId, string? title, string? note, PriorityLeve CreatedBy = createdBy; } + private TodoItem() + { } + public Guid ListId { get; private set; } public string? Title { get; private set; } @@ -25,8 +29,29 @@ private TodoItem(Guid id, Guid listId, string? title, string? note, PriorityLeve public DateTime? Reminder { get; private set; } + public bool Done { get; private set; } + public static TodoItem Create(Guid listId, string? title, PriorityLevel priority = PriorityLevel.None, string? note = null, DateTime? reminder = null, string? createdBy = null) { - return new TodoItem(Guid.NewGuid(), listId, title, note, priority, reminder, createdBy); + var item = new TodoItem(Guid.NewGuid(), listId, title, note, priority, reminder, createdBy); + + item.AddDomainEvent(new TodoItemCreatedEvent(item)); + + return item; + } + + public static TodoItem Update(string? title, bool Done, TodoItem todoItem) + { + todoItem.Title = title; + todoItem.Done = Done; + return todoItem; + } + + public static TodoItem UpdateDetail(Guid listId, PriorityLevel priority, string? note, TodoItem todoItem) + { + todoItem.ListId = listId; + todoItem.Priority = priority; + todoItem.Note = note; + return todoItem; } -} \ No newline at end of file +} diff --git a/src/Domain/TodoLists/ITodoListRepository.cs b/src/Domain/TodoLists/ITodoListRepository.cs index 5c1c588..2a2ef0c 100644 --- a/src/Domain/TodoLists/ITodoListRepository.cs +++ b/src/Domain/TodoLists/ITodoListRepository.cs @@ -2,6 +2,14 @@ public interface ITodoListRepository { + /// + /// Get a todo list by Id. + /// + /// + /// + /// + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + /// /// Get a todo list by title. /// @@ -10,6 +18,13 @@ public interface ITodoListRepository /// Task GetByTitleAsync(string title, CancellationToken cancellationToken = default); + /// + /// Get all todo lists. + /// + /// + /// + Task> GetAllAsync(CancellationToken cancellationToken = default); + /// /// Add a todo list. /// diff --git a/src/Domain/TodoLists/TodoList.cs b/src/Domain/TodoLists/TodoList.cs index 2819133..2b91fbc 100644 --- a/src/Domain/TodoLists/TodoList.cs +++ b/src/Domain/TodoLists/TodoList.cs @@ -6,22 +6,32 @@ namespace FastCleanArchitecture.Domain.TodoLists; public sealed class TodoList : BaseAuditableEntity { - private TodoList(Guid id, string? title, Colour? colour, IList? items) : base(id) + private TodoList(Guid id, string? title, Colour? colour, IList items) : base(id) { Title = title; Colour = colour ?? Colour.Grey; - Items = items ?? []; + Items = items; } + private TodoList() + { } + public string? Title { get; private set; } - public Colour Colour { get; private set; } - public IList Items { get; private set; } + public Colour Colour { get; private set; } = Colour.Grey; + public IList Items { get; private set; } = []; public static TodoList Create( string? title, Colour? colour = null, - IList? todoItems = null) + List? todoItems = null) + { + return new TodoList(Guid.NewGuid(), title, colour, todoItems ?? new List()); + } + + public static TodoList UpdateTitle(string title, TodoList todoList) { - return new TodoList(Guid.NewGuid(), title, colour, todoItems); + todoList.Title = title; + todoList.ModifiedAtUtc = DateTime.UtcNow; + return todoList; } } diff --git a/src/Infrastructure/Data/ApplicationDbContextInitialiser.cs b/src/Infrastructure/Data/ApplicationDbContextInitialiser.cs index 302db69..79f7189 100644 --- a/src/Infrastructure/Data/ApplicationDbContextInitialiser.cs +++ b/src/Infrastructure/Data/ApplicationDbContextInitialiser.cs @@ -78,4 +78,4 @@ public async Task TrySeedAsync() await _context.SaveChangesAsync(); } } -} \ No newline at end of file +} diff --git a/src/Infrastructure/Data/Configurations/TodoListConfiguration.cs b/src/Infrastructure/Data/Configurations/TodoListConfiguration.cs index 7290e7c..bc09dd5 100644 --- a/src/Infrastructure/Data/Configurations/TodoListConfiguration.cs +++ b/src/Infrastructure/Data/Configurations/TodoListConfiguration.cs @@ -15,6 +15,11 @@ public void Configure(EntityTypeBuilder builder) .HasMaxLength(200) .IsRequired(); + builder.HasMany(t => t.Items) + .WithOne() + .HasForeignKey(x => x.ListId) + .OnDelete(DeleteBehavior.Cascade); + builder.OwnsOne(b => b.Colour); } -} \ No newline at end of file +} diff --git a/src/Infrastructure/Data/Migrations/20240910005539_Migrations.Designer.cs b/src/Infrastructure/Data/Migrations/20240912040604_Migrations.Designer.cs similarity index 92% rename from src/Infrastructure/Data/Migrations/20240910005539_Migrations.Designer.cs rename to src/Infrastructure/Data/Migrations/20240912040604_Migrations.Designer.cs index 8ad7b74..be490de 100644 --- a/src/Infrastructure/Data/Migrations/20240910005539_Migrations.Designer.cs +++ b/src/Infrastructure/Data/Migrations/20240912040604_Migrations.Designer.cs @@ -9,10 +9,10 @@ #nullable disable -namespace FastCleanArchitecture.Infrastructure.Migrations +namespace FastCleanArchitecture.Infrastructure.Data.Migrations { [DbContext(typeof(ApplicationDbContext))] - [Migration("20240910005539_Migrations")] + [Migration("20240912040604_Migrations")] partial class Migrations { /// @@ -37,6 +37,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("CreatedBy") .HasColumnType("nvarchar(max)"); + b.Property("Done") + .HasColumnType("bit"); + b.Property("ListId") .HasColumnType("uniqueidentifier"); @@ -60,12 +63,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(200) .HasColumnType("nvarchar(200)"); - b.Property("TodoListId") - .HasColumnType("uniqueidentifier"); - b.HasKey("Id"); - b.HasIndex("TodoListId"); + b.HasIndex("ListId"); b.ToTable("TodoItems", (string)null); }); @@ -102,7 +102,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.HasOne("FastCleanArchitecture.Domain.TodoLists.TodoList", null) .WithMany("Items") - .HasForeignKey("TodoListId"); + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("FastCleanArchitecture.Domain.TodoLists.TodoList", b => diff --git a/src/Infrastructure/Data/Migrations/20240910005539_Migrations.cs b/src/Infrastructure/Data/Migrations/20240912040604_Migrations.cs similarity index 87% rename from src/Infrastructure/Data/Migrations/20240910005539_Migrations.cs rename to src/Infrastructure/Data/Migrations/20240912040604_Migrations.cs index 81af085..3f10766 100644 --- a/src/Infrastructure/Data/Migrations/20240910005539_Migrations.cs +++ b/src/Infrastructure/Data/Migrations/20240912040604_Migrations.cs @@ -3,7 +3,7 @@ #nullable disable -namespace FastCleanArchitecture.Infrastructure.Migrations +namespace FastCleanArchitecture.Infrastructure.Data.Migrations { /// public partial class Migrations : Migration @@ -38,7 +38,7 @@ protected override void Up(MigrationBuilder migrationBuilder) Note = table.Column(type: "nvarchar(max)", nullable: true), Priority = table.Column(type: "int", nullable: false), Reminder = table.Column(type: "datetime2", nullable: true), - TodoListId = table.Column(type: "uniqueidentifier", nullable: true), + Done = table.Column(type: "bit", nullable: false), CreatedAtUtc = table.Column(type: "datetimeoffset", nullable: false), CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), ModifiedAtUtc = table.Column(type: "datetimeoffset", nullable: false), @@ -48,16 +48,17 @@ protected override void Up(MigrationBuilder migrationBuilder) { table.PrimaryKey("PK_TodoItems", x => x.Id); table.ForeignKey( - name: "FK_TodoItems_TodoLists_TodoListId", - column: x => x.TodoListId, + name: "FK_TodoItems_TodoLists_ListId", + column: x => x.ListId, principalTable: "TodoLists", - principalColumn: "Id"); + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateIndex( - name: "IX_TodoItems_TodoListId", + name: "IX_TodoItems_ListId", table: "TodoItems", - column: "TodoListId"); + column: "ListId"); } /// diff --git a/src/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs index 5a663fd..6d60fd3 100644 --- a/src/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -3,12 +3,12 @@ using FastCleanArchitecture.Infrastructure.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Oracle.EntityFrameworkCore.Metadata; #nullable disable -namespace FastCleanArchitecture.Infrastructure.Migrations +namespace FastCleanArchitecture.Infrastructure.Data.Migrations { [DbContext(typeof(ApplicationDbContext))] partial class ApplicationDbContextModelSnapshot : ModelSnapshot @@ -20,49 +20,49 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasAnnotation("ProductVersion", "8.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 128); - OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("FastCleanArchitecture.Domain.TodoItems.TodoItem", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("RAW(16)"); + .HasColumnType("uniqueidentifier"); b.Property("CreatedAtUtc") - .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + .HasColumnType("datetimeoffset"); b.Property("CreatedBy") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("nvarchar(max)"); + + b.Property("Done") + .HasColumnType("bit"); b.Property("ListId") - .HasColumnType("RAW(16)"); + .HasColumnType("uniqueidentifier"); b.Property("ModifiedAtUtc") - .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + .HasColumnType("datetimeoffset"); b.Property("ModifiedBy") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("nvarchar(max)"); b.Property("Note") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("nvarchar(max)"); b.Property("Priority") - .HasColumnType("NUMBER(10)"); + .HasColumnType("int"); b.Property("Reminder") - .HasColumnType("TIMESTAMP(7)"); + .HasColumnType("datetime2"); b.Property("Title") .IsRequired() .HasMaxLength(200) - .HasColumnType("NVARCHAR2(200)"); - - b.Property("TodoListId") - .HasColumnType("RAW(16)"); + .HasColumnType("nvarchar(200)"); b.HasKey("Id"); - b.HasIndex("TodoListId"); + b.HasIndex("ListId"); b.ToTable("TodoItems", (string)null); }); @@ -71,24 +71,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("RAW(16)"); + .HasColumnType("uniqueidentifier"); b.Property("CreatedAtUtc") - .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + .HasColumnType("datetimeoffset"); b.Property("CreatedBy") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("nvarchar(max)"); b.Property("ModifiedAtUtc") - .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + .HasColumnType("datetimeoffset"); b.Property("ModifiedBy") - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("nvarchar(max)"); b.Property("Title") .IsRequired() .HasMaxLength(200) - .HasColumnType("NVARCHAR2(200)"); + .HasColumnType("nvarchar(200)"); b.HasKey("Id"); @@ -99,7 +99,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.HasOne("FastCleanArchitecture.Domain.TodoLists.TodoList", null) .WithMany("Items") - .HasForeignKey("TodoListId"); + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); modelBuilder.Entity("FastCleanArchitecture.Domain.TodoLists.TodoList", b => @@ -107,11 +109,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.OwnsOne("FastCleanArchitecture.Domain.TodoLists.ValueObjects.Colour", "Colour", b1 => { b1.Property("TodoListId") - .HasColumnType("RAW(16)"); + .HasColumnType("uniqueidentifier"); b1.Property("Code") .IsRequired() - .HasColumnType("NVARCHAR2(2000)"); + .HasColumnType("nvarchar(max)"); b1.HasKey("TodoListId"); diff --git a/src/Infrastructure/Data/OracleMigrations/20240910180110_InitialMigrations.Designer.cs b/src/Infrastructure/Data/OracleMigrations/20240910180110_InitialMigrations.Designer.cs deleted file mode 100644 index 662fd9e..0000000 --- a/src/Infrastructure/Data/OracleMigrations/20240910180110_InitialMigrations.Designer.cs +++ /dev/null @@ -1,138 +0,0 @@ -// -using System; -using FastCleanArchitecture.Infrastructure.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Oracle.EntityFrameworkCore.Metadata; - -#nullable disable - -namespace FastCleanArchitecture.Infrastructure.Data.OracleMigrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20240910180110_InitialMigrations")] - partial class InitialMigrations - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("FastCleanArchitecture.Domain.TodoItems.TodoItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("RAW(16)"); - - b.Property("CreatedAtUtc") - .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); - - b.Property("CreatedBy") - .HasColumnType("NVARCHAR2(2000)"); - - b.Property("ListId") - .HasColumnType("RAW(16)"); - - b.Property("ModifiedAtUtc") - .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); - - b.Property("ModifiedBy") - .HasColumnType("NVARCHAR2(2000)"); - - b.Property("Note") - .HasColumnType("NVARCHAR2(2000)"); - - b.Property("Priority") - .HasColumnType("NUMBER(10)"); - - b.Property("Reminder") - .HasColumnType("TIMESTAMP(7)"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("NVARCHAR2(200)"); - - b.Property("TodoListId") - .HasColumnType("RAW(16)"); - - b.HasKey("Id"); - - b.HasIndex("TodoListId"); - - b.ToTable("TodoItems", (string)null); - }); - - modelBuilder.Entity("FastCleanArchitecture.Domain.TodoLists.TodoList", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("RAW(16)"); - - b.Property("CreatedAtUtc") - .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); - - b.Property("CreatedBy") - .HasColumnType("NVARCHAR2(2000)"); - - b.Property("ModifiedAtUtc") - .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); - - b.Property("ModifiedBy") - .HasColumnType("NVARCHAR2(2000)"); - - b.Property("Title") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("NVARCHAR2(200)"); - - b.HasKey("Id"); - - b.ToTable("TodoLists", (string)null); - }); - - modelBuilder.Entity("FastCleanArchitecture.Domain.TodoItems.TodoItem", b => - { - b.HasOne("FastCleanArchitecture.Domain.TodoLists.TodoList", null) - .WithMany("Items") - .HasForeignKey("TodoListId"); - }); - - modelBuilder.Entity("FastCleanArchitecture.Domain.TodoLists.TodoList", b => - { - b.OwnsOne("FastCleanArchitecture.Domain.TodoLists.ValueObjects.Colour", "Colour", b1 => - { - b1.Property("TodoListId") - .HasColumnType("RAW(16)"); - - b1.Property("Code") - .IsRequired() - .HasColumnType("NVARCHAR2(2000)"); - - b1.HasKey("TodoListId"); - - b1.ToTable("TodoLists"); - - b1.WithOwner() - .HasForeignKey("TodoListId"); - }); - - b.Navigation("Colour") - .IsRequired(); - }); - - modelBuilder.Entity("FastCleanArchitecture.Domain.TodoLists.TodoList", b => - { - b.Navigation("Items"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Infrastructure/Data/OracleMigrations/20240910180110_InitialMigrations.cs b/src/Infrastructure/Data/OracleMigrations/20240910180110_InitialMigrations.cs deleted file mode 100644 index 2a3f54f..0000000 --- a/src/Infrastructure/Data/OracleMigrations/20240910180110_InitialMigrations.cs +++ /dev/null @@ -1,329 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FastCleanArchitecture.Infrastructure.Data.OracleMigrations -{ - /// - public partial class InitialMigrations : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "Title", - table: "TodoLists", - type: "NVARCHAR2(200)", - maxLength: 200, - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(200)", - oldMaxLength: 200); - - migrationBuilder.AlterColumn( - name: "ModifiedBy", - table: "TodoLists", - type: "NVARCHAR2(2000)", - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(max)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ModifiedAtUtc", - table: "TodoLists", - type: "TIMESTAMP(7) WITH TIME ZONE", - nullable: false, - oldClrType: typeof(DateTimeOffset), - oldType: "datetimeoffset"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "TodoLists", - type: "NVARCHAR2(2000)", - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(max)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAtUtc", - table: "TodoLists", - type: "TIMESTAMP(7) WITH TIME ZONE", - nullable: false, - oldClrType: typeof(DateTimeOffset), - oldType: "datetimeoffset"); - - migrationBuilder.AlterColumn( - name: "Colour_Code", - table: "TodoLists", - type: "NVARCHAR2(2000)", - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(max)"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "TodoLists", - type: "RAW(16)", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uniqueidentifier"); - - migrationBuilder.AlterColumn( - name: "TodoListId", - table: "TodoItems", - type: "RAW(16)", - nullable: true, - oldClrType: typeof(Guid), - oldType: "uniqueidentifier", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Title", - table: "TodoItems", - type: "NVARCHAR2(200)", - maxLength: 200, - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(200)", - oldMaxLength: 200); - - migrationBuilder.AlterColumn( - name: "Reminder", - table: "TodoItems", - type: "TIMESTAMP(7)", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "datetime2", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Priority", - table: "TodoItems", - type: "NUMBER(10)", - nullable: false, - oldClrType: typeof(int), - oldType: "int"); - - migrationBuilder.AlterColumn( - name: "Note", - table: "TodoItems", - type: "NVARCHAR2(2000)", - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(max)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ModifiedBy", - table: "TodoItems", - type: "NVARCHAR2(2000)", - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(max)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ModifiedAtUtc", - table: "TodoItems", - type: "TIMESTAMP(7) WITH TIME ZONE", - nullable: false, - oldClrType: typeof(DateTimeOffset), - oldType: "datetimeoffset"); - - migrationBuilder.AlterColumn( - name: "ListId", - table: "TodoItems", - type: "RAW(16)", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uniqueidentifier"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "TodoItems", - type: "NVARCHAR2(2000)", - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(max)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAtUtc", - table: "TodoItems", - type: "TIMESTAMP(7) WITH TIME ZONE", - nullable: false, - oldClrType: typeof(DateTimeOffset), - oldType: "datetimeoffset"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "TodoItems", - type: "RAW(16)", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uniqueidentifier"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "Title", - table: "TodoLists", - type: "nvarchar(200)", - maxLength: 200, - nullable: false, - oldClrType: typeof(string), - oldType: "NVARCHAR2(200)", - oldMaxLength: 200); - - migrationBuilder.AlterColumn( - name: "ModifiedBy", - table: "TodoLists", - type: "nvarchar(max)", - nullable: true, - oldClrType: typeof(string), - oldType: "NVARCHAR2(2000)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ModifiedAtUtc", - table: "TodoLists", - type: "datetimeoffset", - nullable: false, - oldClrType: typeof(DateTimeOffset), - oldType: "TIMESTAMP(7) WITH TIME ZONE"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "TodoLists", - type: "nvarchar(max)", - nullable: true, - oldClrType: typeof(string), - oldType: "NVARCHAR2(2000)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAtUtc", - table: "TodoLists", - type: "datetimeoffset", - nullable: false, - oldClrType: typeof(DateTimeOffset), - oldType: "TIMESTAMP(7) WITH TIME ZONE"); - - migrationBuilder.AlterColumn( - name: "Colour_Code", - table: "TodoLists", - type: "nvarchar(max)", - nullable: false, - oldClrType: typeof(string), - oldType: "NVARCHAR2(2000)"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "TodoLists", - type: "uniqueidentifier", - nullable: false, - oldClrType: typeof(Guid), - oldType: "RAW(16)"); - - migrationBuilder.AlterColumn( - name: "TodoListId", - table: "TodoItems", - type: "uniqueidentifier", - nullable: true, - oldClrType: typeof(Guid), - oldType: "RAW(16)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Title", - table: "TodoItems", - type: "nvarchar(200)", - maxLength: 200, - nullable: false, - oldClrType: typeof(string), - oldType: "NVARCHAR2(200)", - oldMaxLength: 200); - - migrationBuilder.AlterColumn( - name: "Reminder", - table: "TodoItems", - type: "datetime2", - nullable: true, - oldClrType: typeof(DateTime), - oldType: "TIMESTAMP(7)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "Priority", - table: "TodoItems", - type: "int", - nullable: false, - oldClrType: typeof(int), - oldType: "NUMBER(10)"); - - migrationBuilder.AlterColumn( - name: "Note", - table: "TodoItems", - type: "nvarchar(max)", - nullable: true, - oldClrType: typeof(string), - oldType: "NVARCHAR2(2000)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ModifiedBy", - table: "TodoItems", - type: "nvarchar(max)", - nullable: true, - oldClrType: typeof(string), - oldType: "NVARCHAR2(2000)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "ModifiedAtUtc", - table: "TodoItems", - type: "datetimeoffset", - nullable: false, - oldClrType: typeof(DateTimeOffset), - oldType: "TIMESTAMP(7) WITH TIME ZONE"); - - migrationBuilder.AlterColumn( - name: "ListId", - table: "TodoItems", - type: "uniqueidentifier", - nullable: false, - oldClrType: typeof(Guid), - oldType: "RAW(16)"); - - migrationBuilder.AlterColumn( - name: "CreatedBy", - table: "TodoItems", - type: "nvarchar(max)", - nullable: true, - oldClrType: typeof(string), - oldType: "NVARCHAR2(2000)", - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "CreatedAtUtc", - table: "TodoItems", - type: "datetimeoffset", - nullable: false, - oldClrType: typeof(DateTimeOffset), - oldType: "TIMESTAMP(7) WITH TIME ZONE"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "TodoItems", - type: "uniqueidentifier", - nullable: false, - oldClrType: typeof(Guid), - oldType: "RAW(16)"); - } - } -} diff --git a/src/Infrastructure/Data/Repositories/TodoItemRepository.cs b/src/Infrastructure/Data/Repositories/TodoItemRepository.cs new file mode 100644 index 0000000..7744d3d --- /dev/null +++ b/src/Infrastructure/Data/Repositories/TodoItemRepository.cs @@ -0,0 +1,10 @@ +using FastCleanArchitecture.Domain.TodoItems; + +namespace FastCleanArchitecture.Infrastructure.Data.Repositories; + +internal sealed class TodoItemRepository : BaseRepository, ITodoItemRepository +{ + public TodoItemRepository(ApplicationDbContext context) : base(context) + { + } +} diff --git a/src/Infrastructure/Data/Repositories/TodoListRepository.cs b/src/Infrastructure/Data/Repositories/TodoListRepository.cs index bcfdd8c..dc91bd1 100644 --- a/src/Infrastructure/Data/Repositories/TodoListRepository.cs +++ b/src/Infrastructure/Data/Repositories/TodoListRepository.cs @@ -1,4 +1,5 @@ -using FastCleanArchitecture.Domain.TodoLists; +using FastCleanArchitecture.Domain.TodoItems; +using FastCleanArchitecture.Domain.TodoLists; using Microsoft.EntityFrameworkCore; namespace FastCleanArchitecture.Infrastructure.Data.Repositories; @@ -9,8 +10,14 @@ public TodoListRepository(ApplicationDbContext context) : base(context) { } + public async Task> GetAllAsync(CancellationToken cancellationToken = default) + => await Context.Set() + .Include(t => t.Items) + .OrderBy(x => x.Title) + .ToListAsync(cancellationToken); + public async Task GetByTitleAsync(string title, CancellationToken cancellationToken = default) { - return await Context.Set().FirstOrDefaultAsync(x => x.Title!.Equals(title, StringComparison.CurrentCultureIgnoreCase)); + return await Context.Set().FirstOrDefaultAsync(x => x.Title! == title); } -} \ No newline at end of file +} diff --git a/src/Infrastructure/DependencyInjection.cs b/src/Infrastructure/DependencyInjection.cs index ae0b940..c2feffe 100644 --- a/src/Infrastructure/DependencyInjection.cs +++ b/src/Infrastructure/DependencyInjection.cs @@ -1,4 +1,5 @@ using FastCleanArchitecture.Domain.Common; +using FastCleanArchitecture.Domain.TodoItems; using FastCleanArchitecture.Domain.TodoLists; using FastCleanArchitecture.Infrastructure.Data; using FastCleanArchitecture.Infrastructure.Data.Repositories; @@ -30,6 +31,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(sp => sp.GetRequiredService()); return services; diff --git a/templates/fca-use-case/FeatureName/Commands/FastCleanArchitectureUseCase/FastCleanArchitectureUseCase.cs b/templates/fca-use-case/FeatureName/Commands/FastCleanArchitectureUseCase/FastCleanArchitectureUseCase.cs index a2a00a9..c5166b0 100644 --- a/templates/fca-use-case/FeatureName/Commands/FastCleanArchitectureUseCase/FastCleanArchitectureUseCase.cs +++ b/templates/fca-use-case/FeatureName/Commands/FastCleanArchitectureUseCase/FastCleanArchitectureUseCase.cs @@ -2,14 +2,14 @@ namespace FastCleanArchitecture.Application.FeatureName.Commands.FastCleanArchitectureUseCase; -public record FastCleanArchitectureUseCaseCommand : ICommand +public sealed record FastCleanArchitectureUseCaseCommand : ICommand { } -internal sealed class FastArchitectureUseCaseCommandHandler : ICommandHandler +internal sealed class FastCleanArchitectureUseCaseCommandHandler : ICommandHandler { - public async Task Handle(FastCleanArchitectureUseCaseCommand request, CancellationToken cancellationToken) + public async Task> Handle(FastCleanArchitectureUseCaseCommand request, CancellationToken cancellationToken) { throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/templates/fca-use-case/FeatureName/Queries/FastCleanArchitectureUseCase/FastCleanArchitectureUseCase.cs b/templates/fca-use-case/FeatureName/Queries/FastCleanArchitectureUseCase/FastCleanArchitectureUseCase.cs index ba4945e..0b0c8d6 100644 --- a/templates/fca-use-case/FeatureName/Queries/FastCleanArchitectureUseCase/FastCleanArchitectureUseCase.cs +++ b/templates/fca-use-case/FeatureName/Queries/FastCleanArchitectureUseCase/FastCleanArchitectureUseCase.cs @@ -2,14 +2,14 @@ namespace FastCleanArchitecture.Application.FeatureName.Queries.FastCleanArchitectureUseCase; -public record FastCleanArchitectureUseCaseQuery : IQuery +public sealed record FastCleanArchitectureUseCaseQuery : IQuery { } internal sealed class FastCleanArchitectureUseCaseQueryHandler : IQueryHandler { - public async Task Handle(FastCleanArchitectureUseCaseQuery request, CancellationToken cancellationToken) + public async Task> Handle(FastCleanArchitectureUseCaseQuery request, CancellationToken cancellationToken) { throw new NotImplementedException(); } -} \ No newline at end of file +}