From 6656c997e576c361e5686502be5af432857f7511 Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Fri, 26 Jul 2024 14:16:35 +0200 Subject: [PATCH 01/16] core --- .../Catalogs/Catalog.cs | 30 +++++++++++++++++++ .../Catalogs/ICatalogRepository.cs | 10 +++++++ .../Repositories/InMemoryCatalogRepository.cs | 14 +++++++++ 3 files changed, 54 insertions(+) create mode 100644 src/DotNetBoilerplate.Core/Catalogs/Catalog.cs create mode 100644 src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs create mode 100644 src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs diff --git a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs new file mode 100644 index 0000000..db8b1fa --- /dev/null +++ b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs @@ -0,0 +1,30 @@ + +namespace DotNetBoilerplate.Core.Catalogs; + +public sealed class Catalog +{ + private Catalog() + { } + public Guid Id { get; private set; } + public string Name { get; private set; } + public string Genre { get; private set; } + public string Description { get; private set; } + public Guid BookStoreId { get; private set; } + + public static Catalog Create( + string name, + string genre, + string description, + Guid bookStoreId + ) + { + return new Catalog + { + Id = Guid.NewGuid(), + Name = name, + Genre = genre, + Description = description, + BookStoreId = bookStoreId + }; + } +} diff --git a/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs new file mode 100644 index 0000000..f58bd72 --- /dev/null +++ b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs @@ -0,0 +1,10 @@ + + +namespace DotNetBoilerplate.Core.Catalogs; + +public interface ICatalogRepository +{ + Task AddAsync(Catalog catalog); + + +} diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs new file mode 100644 index 0000000..0a5c4ca --- /dev/null +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs @@ -0,0 +1,14 @@ +using DotNetBoilerplate.Core.Catalogs; + +namespace DotNetBoilerplate.Infrastructure.DAL.Repositories; + +internal sealed class InMemoryCatalogRepository : ICatalogRepository +{ + private readonly List _catalogs = []; + + public Task AddAsync(Catalog catalog) + { + _catalogs.Add(catalog); + return Task.CompletedTask; + } +} From dcf68d3dabe5d3632808a5f94fed804251614f03 Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Fri, 26 Jul 2024 17:40:29 +0200 Subject: [PATCH 02/16] added Creating Catalog --- .../Catalogs/CatalogsEndpoints.cs | 19 ++++++++ .../Catalogs/CreateCatalogEndpoint.cs | 46 +++++++++++++++++++ src/DotNetBoilerplate.Api/Program.cs | 2 + .../Catalogs/Create/CreateCatalogCommand.cs | 5 ++ .../Catalogs/Create/CreateCatalogHandler.cs | 33 +++++++++++++ .../Catalogs/Catalog.cs | 9 +++- .../UserCanNotCreateCatalogException.cs | 8 ++++ .../Catalogs/ICatalogRepository.cs | 2 +- .../DAL/Repositories/Extensions.cs | 2 + .../Repositories/InMemoryCatalogRepository.cs | 6 +++ 10 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs create mode 100644 src/DotNetBoilerplate.Api/Catalogs/CreateCatalogEndpoint.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogCommand.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs create mode 100644 src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotCreateCatalogException.cs diff --git a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs new file mode 100644 index 0000000..9d1fc35 --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs @@ -0,0 +1,19 @@ +using DotNetBoilerplate.Api.Catalogs; + +namespace DotNetBoilerplate.Api.Catalogs; + +internal static class CatalogsEndpoints +{ + public const string BasePath = "catalogs"; + public const string Tags = "Catalogs"; + + public static void MapCatalogsEndpoints(this WebApplication app) + { + var group = app.MapGroup(BasePath) + .WithTags(Tags); + + group + .MapEndpoint(); + } +} + diff --git a/src/DotNetBoilerplate.Api/Catalogs/CreateCatalogEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/CreateCatalogEndpoint.cs new file mode 100644 index 0000000..ba8e342 --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/CreateCatalogEndpoint.cs @@ -0,0 +1,46 @@ +using System.ComponentModel.DataAnnotations; +using DotNetBoilerplate.Application.Catalogs.Create; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +internal sealed class CreateCatalogEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapPost("", Handle) + .RequireAuthorization() + .WithSummary("Create catalog"); + } + private static async Task> Handle( + [FromBody] Request request, + [FromServices] ICommandDispatcher commandDispatcher, + CancellationToken ct + ) + { + var command = new CreateCatalogCommand( + request.Name, + request.Genre, + request.Description + ); + + var result = await commandDispatcher.DispatchAsync(command, ct); + + return TypedResults.Ok(new Response(result)); + } + + internal sealed record Response( + Guid Id + ); + + private sealed class Request + { + [Required] public string Name { get; init; } + + [Required] public string Genre { get; init; } + + [Required] public string Description { get; init; } + } +} diff --git a/src/DotNetBoilerplate.Api/Program.cs b/src/DotNetBoilerplate.Api/Program.cs index 623bed1..6255e51 100644 --- a/src/DotNetBoilerplate.Api/Program.cs +++ b/src/DotNetBoilerplate.Api/Program.cs @@ -2,6 +2,7 @@ using DotNetBoilerplate.Api.BookStores; using DotNetBoilerplate.Api.Books; using DotNetBoilerplate.Api.Users; +using DotNetBoilerplate.Api.Catalogs; using DotNetBoilerplate.Application; using DotNetBoilerplate.Core; using DotNetBoilerplate.Infrastructure; @@ -22,6 +23,7 @@ app.MapUsersEndpoints(); app.MapBookStoresEndpoints(); app.MapBookEndpoints(); +app.MapCatalogsEndpoints(); app.UseInfrastructure(); diff --git a/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogCommand.cs b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogCommand.cs new file mode 100644 index 0000000..63b10ac --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogCommand.cs @@ -0,0 +1,5 @@ +using DotNetBoilerplate.Shared.Abstractions.Commands; + +namespace DotNetBoilerplate.Application.Catalogs.Create; + +public sealed record CreateCatalogCommand(string Name, string Genre, string Description) : ICommand; \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs new file mode 100644 index 0000000..7a5bef6 --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs @@ -0,0 +1,33 @@ +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Core.BookStores; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using DotNetBoilerplate.Application.Exceptions; +using DotNetBoilerplate.Shared.Abstractions.Contexts; + +namespace DotNetBoilerplate.Application.Catalogs.Create; + +internal sealed class CreateCatalogHandler( + IContext context, + ICatalogRepository catalogRepository, + IBookStoreRepository bookStoreRepository +) : ICommandHandler +{ + public async Task HandleAsync(CreateCatalogCommand command) + { + var bookStore = await bookStoreRepository.GetByOwnerIdAsync(context.Identity.Id); + if (bookStore is null) + throw new BookStoreNotFoundException(); + + bool userCanNotCreateCatalog = await catalogRepository.UserCanNotAddCatalogAsync(bookStore.Id); + + var catalog = Catalog.Create( + command.Name, + command.Genre, + command.Description, + bookStore.Id, + userCanNotCreateCatalog + ); + await catalogRepository.AddAsync(catalog); + return catalog.Id; + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs index db8b1fa..dca5b33 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs @@ -1,4 +1,5 @@ - +using DotNetBoilerplate.Core.Catalogs.Exceptions; + namespace DotNetBoilerplate.Core.Catalogs; public sealed class Catalog @@ -15,9 +16,13 @@ public static Catalog Create( string name, string genre, string description, - Guid bookStoreId + Guid bookStoreId, + bool userCanNotCreateCatalog ) { + if (userCanNotCreateCatalog) + throw new UserCanNotCreateCatalogException(); + return new Catalog { Id = Guid.NewGuid(), diff --git a/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotCreateCatalogException.cs b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotCreateCatalogException.cs new file mode 100644 index 0000000..54c2cb1 --- /dev/null +++ b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotCreateCatalogException.cs @@ -0,0 +1,8 @@ +using DotNetBoilerplate.Shared.Abstractions.Exceptions; + +namespace DotNetBoilerplate.Core.Catalogs.Exceptions; + +internal sealed class UserCanNotCreateCatalogException() + : CustomException("User can not create catalog.") +{ +}; diff --git a/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs index f58bd72..8bee71a 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs @@ -6,5 +6,5 @@ public interface ICatalogRepository { Task AddAsync(Catalog catalog); - + Task UserCanNotAddCatalogAsync(Guid bookStoreId); } diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs index 76fc7eb..9b60410 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs @@ -2,6 +2,7 @@ using DotNetBoilerplate.Core.Books; using DotNetBoilerplate.Core.Users; using Microsoft.Extensions.DependencyInjection; +using DotNetBoilerplate.Core.Catalogs; namespace DotNetBoilerplate.Infrastructure.DAL.Repositories; @@ -12,6 +13,7 @@ public static IServiceCollection AddRepositories(this IServiceCollection service services.AddScoped(); services.AddScoped(); services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs index 0a5c4ca..4f62fd4 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs @@ -11,4 +11,10 @@ public Task AddAsync(Catalog catalog) _catalogs.Add(catalog); return Task.CompletedTask; } + + public Task UserCanNotAddCatalogAsync(Guid bookStoreId) + { + var count = _catalogs.Count(x => x.BookStoreId == bookStoreId); + return Task.FromResult(count >= 5); + } } From c0c0e0d40fc40f665823d9396e8165d293e50a3e Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Sun, 28 Jul 2024 15:14:21 +0200 Subject: [PATCH 03/16] Update catalog, fix create catalog --- .../Catalogs/CatalogsEndpoints.cs | 3 +- .../Catalogs/UpdateCatalogEndpoint.cs | 52 +++++++++++++++++++ .../Catalogs/Create/CreateCatalogHandler.cs | 1 + .../Catalogs/Update/UpdateCatalogCommand.cs | 5 ++ .../Catalogs/Update/UpdateCatalogHandler.cs | 29 +++++++++++ .../Exceptions/CatalogNotFoundException.cs | 6 +++ .../Catalogs/Catalog.cs | 20 ++++++- .../UserCanNotUpdateCatalogException.cs | 8 +++ .../Catalogs/ICatalogRepository.cs | 10 ++++ .../Repositories/InMemoryCatalogRepository.cs | 30 +++++++++++ 10 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 src/DotNetBoilerplate.Api/Catalogs/UpdateCatalogEndpoint.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogCommand.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogHandler.cs create mode 100644 src/DotNetBoilerplate.Application/Exceptions/CatalogNotFoundException.cs create mode 100644 src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotUpdateCatalogException.cs diff --git a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs index 9d1fc35..90f1274 100644 --- a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs +++ b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs @@ -13,7 +13,8 @@ public static void MapCatalogsEndpoints(this WebApplication app) .WithTags(Tags); group - .MapEndpoint(); + .MapEndpoint() + .MapEndpoint(); } } diff --git a/src/DotNetBoilerplate.Api/Catalogs/UpdateCatalogEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/UpdateCatalogEndpoint.cs new file mode 100644 index 0000000..8cef77f --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/UpdateCatalogEndpoint.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations; +using DotNetBoilerplate.Application.Catalogs.Update; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +public class UpdateCatalogEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapPut("{id:guid}", Handle) + .RequireAuthorization() + .WithSummary("Update a catalog"); + } + + private static async Task> Handle( + [FromRoute] Guid id, + [FromBody] Request request, + [FromServices] ICommandDispatcher commandDispatcher, + CancellationToken ct + ) + { + var command = new UpdateCatalogCommand( + id, + request.Name, + request.Genre, + request.Description + ); + + var result = await commandDispatcher.DispatchAsync(command, ct); + + return TypedResults.Ok(new Response(result)); + } + + internal sealed record Response( + Guid Id + ); + + private sealed class Request + { + // ReSharper disable once UnusedAutoPropertyAccessor.Local + [Required] public string Name { get; init; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + [Required] public string Genre { get; init; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + [Required] public string Description { get; init; } + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs index 7a5bef6..05cbe9c 100644 --- a/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs +++ b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs @@ -25,6 +25,7 @@ public async Task HandleAsync(CreateCatalogCommand command) command.Genre, command.Description, bookStore.Id, + context.Identity.Id, userCanNotCreateCatalog ); await catalogRepository.AddAsync(catalog); diff --git a/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogCommand.cs b/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogCommand.cs new file mode 100644 index 0000000..8b9be3e --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogCommand.cs @@ -0,0 +1,5 @@ +using DotNetBoilerplate.Shared.Abstractions.Commands; + +namespace DotNetBoilerplate.Application.Catalogs.Update; + +public sealed record UpdateCatalogCommand(Guid Id, string Name, string Genre, string Description) : ICommand; \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogHandler.cs new file mode 100644 index 0000000..feb36bb --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogHandler.cs @@ -0,0 +1,29 @@ +using DotNetBoilerplate.Application.Exceptions; +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using DotNetBoilerplate.Shared.Abstractions.Contexts; + +namespace DotNetBoilerplate.Application.Catalogs.Update; + +internal sealed class UpdateCatalogHandler( + IContext context, + ICatalogRepository catalogRepository +) : ICommandHandler +{ + public async Task HandleAsync(UpdateCatalogCommand command) + { + var catalog = await catalogRepository.GetByIdAsync(command.Id); + if (catalog is null) + throw new BookNotFoundException(); + + catalog.Update( + command.Name, + command.Genre, + command.Description, + context.Identity.Id + ); + + await catalogRepository.UpdateAsync(catalog); + return catalog.Id; + } +} diff --git a/src/DotNetBoilerplate.Application/Exceptions/CatalogNotFoundException.cs b/src/DotNetBoilerplate.Application/Exceptions/CatalogNotFoundException.cs new file mode 100644 index 0000000..9b9251c --- /dev/null +++ b/src/DotNetBoilerplate.Application/Exceptions/CatalogNotFoundException.cs @@ -0,0 +1,6 @@ +using DotNetBoilerplate.Shared.Abstractions.Exceptions; + +namespace DotNetBoilerplate.Application.Exceptions; + +internal sealed class CatalogNotFoundException() + : CustomException("Catalog not found"); diff --git a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs index dca5b33..ae1c3c3 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs @@ -11,12 +11,14 @@ private Catalog() public string Genre { get; private set; } public string Description { get; private set; } public Guid BookStoreId { get; private set; } + public Guid CreatedBy { get; private set; } public static Catalog Create( string name, string genre, string description, Guid bookStoreId, + Guid createdBy, bool userCanNotCreateCatalog ) { @@ -29,7 +31,23 @@ bool userCanNotCreateCatalog Name = name, Genre = genre, Description = description, - BookStoreId = bookStoreId + BookStoreId = bookStoreId, + CreatedBy = createdBy }; } + public void Update( + string name, + string genre, + string description, + Guid updater + ) + { + if (updater == Guid.Empty) + throw new UserCanNotUpdateCatalogException(); + + Name = name; + Genre = genre; + Description = description; + } + } diff --git a/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotUpdateCatalogException.cs b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotUpdateCatalogException.cs new file mode 100644 index 0000000..b6853c8 --- /dev/null +++ b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotUpdateCatalogException.cs @@ -0,0 +1,8 @@ +using DotNetBoilerplate.Shared.Abstractions.Exceptions; + +namespace DotNetBoilerplate.Core.Catalogs.Exceptions; + +internal sealed class UserCanNotUpdateCatalogException() + : CustomException("User can not update catalog.") +{ +}; diff --git a/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs index 8bee71a..27ba546 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs @@ -7,4 +7,14 @@ public interface ICatalogRepository Task AddAsync(Catalog catalog); Task UserCanNotAddCatalogAsync(Guid bookStoreId); + + Task UpdateAsync(Catalog catalog); + + Task> GetAll(); + + Task GetByIdAsync(Guid id); + + Task> GetAllInBookStore(Guid bookStoreId); + + Task DeleteAsync(Catalog catalog); } diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs index 4f62fd4..21ac34e 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs @@ -17,4 +17,34 @@ public Task UserCanNotAddCatalogAsync(Guid bookStoreId) var count = _catalogs.Count(x => x.BookStoreId == bookStoreId); return Task.FromResult(count >= 5); } + + public Task UpdateAsync(Catalog catalog) + { + var existingCatalogIndex = _catalogs.FindIndex(x => x.Id == catalog.Id); + _catalogs[existingCatalogIndex] = catalog; + + return Task.CompletedTask; + } + + public Task> GetAll() + { + return Task.FromResult(_catalogs.AsEnumerable()); + } + + public Task GetByIdAsync(Guid id) + { + return Task.FromResult(_catalogs.FirstOrDefault(x => x.Id == id)); + } + + public Task> GetAllInBookStore(Guid bookStoreId) + { + return Task.FromResult(_catalogs.FindAll(x => x.BookStoreId == bookStoreId) + .AsEnumerable()); + } + + public Task DeleteAsync(Catalog catalog) + { + _catalogs.Remove(catalog); + return Task.CompletedTask; + } } From c92c8204860c8d89c36f6ebc79d828203c328fa7 Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Sun, 28 Jul 2024 21:36:49 +0200 Subject: [PATCH 04/16] Get browse catalogs --- .../BrowseCatalogsByBookStoreIdEndpoint.cs | 32 +++++++++++++++++++ .../Catalogs/BrowseCatalogsEndpoint.cs | 28 ++++++++++++++++ .../Catalogs/CatalogsEndpoints.cs | 5 ++- .../Catalogs/GetCatalogByIdEndpoint.cs | 31 ++++++++++++++++++ .../BrowseCatalogsByBookStoreIdHandler.cs | 19 +++++++++++ .../BrowseCatalogsByBookStoreIdQuery.cs | 6 ++++ .../Catalogs/Browse/BrowseCatalogsHandler.cs | 19 +++++++++++ .../Catalogs/Browse/BrowseCatalogsQuery.cs | 6 ++++ .../Catalogs/DTO/CatalogDto.cs | 8 +++++ .../Catalogs/Get/GetCatalogByIdHandler.cs | 24 ++++++++++++++ .../Catalogs/Get/GetCatalogByIdQuery.cs | 6 ++++ 11 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsByBookStoreIdEndpoint.cs create mode 100644 src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsEndpoint.cs create mode 100644 src/DotNetBoilerplate.Api/Catalogs/GetCatalogByIdEndpoint.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdHandler.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdQuery.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsHandler.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsQuery.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/DTO/CatalogDto.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdHandler.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdQuery.cs diff --git a/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsByBookStoreIdEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsByBookStoreIdEndpoint.cs new file mode 100644 index 0000000..85c044c --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsByBookStoreIdEndpoint.cs @@ -0,0 +1,32 @@ +using DotNetBoilerplate.Application.Catalogs.Browse; +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Application.Catalogs.Get; +using DotNetBoilerplate.Shared.Abstractions.Queries; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +public class BrowseCatalogsByBookStoreIdEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapGet("bookstore/{id:guid}", Handle) + .WithSummary("Browse all Catalogs in Book Store"); + } + + private static async Task>, NotFound>> Handle( + [FromRoute] Guid id, + [FromServices] IQueryDispatcher queryDispatcher, + CancellationToken ct + ) + { + var query = new BrowseCatalogsByBookStoreIdQuery(id); + + var result = await queryDispatcher.QueryAsync(query, ct); + + if (result is null) return TypedResults.NotFound(); + + return TypedResults.Ok(result); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsEndpoint.cs new file mode 100644 index 0000000..016b69a --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsEndpoint.cs @@ -0,0 +1,28 @@ +using DotNetBoilerplate.Application.Catalogs.Browse; +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Shared.Abstractions.Queries; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +public class BrowseCatalogsEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapGet("", Handle) + .WithSummary("Browse all catalogs"); + } + + private static async Task>> Handle( + [FromServices] IQueryDispatcher queryDispatcher, + CancellationToken ct + ) + { + var query = new BrowseCatalogsQuery(); + + var result = await queryDispatcher.QueryAsync(query, ct); + + return TypedResults.Ok(result); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs index 90f1274..fd1b106 100644 --- a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs +++ b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs @@ -14,7 +14,10 @@ public static void MapCatalogsEndpoints(this WebApplication app) group .MapEndpoint() - .MapEndpoint(); + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint(); } } diff --git a/src/DotNetBoilerplate.Api/Catalogs/GetCatalogByIdEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/GetCatalogByIdEndpoint.cs new file mode 100644 index 0000000..0f6fcbc --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/GetCatalogByIdEndpoint.cs @@ -0,0 +1,31 @@ +using DotNetBoilerplate.Application.Catalogs.Get; +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Shared.Abstractions.Queries; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +public class GetCatalogByIdEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapGet("{id:guid}", Handle) + .WithSummary("Browse catalog by Id"); + } + + private static async Task, NotFound>> Handle( + [FromRoute] Guid id, + [FromServices] IQueryDispatcher queryDispatcher, + CancellationToken ct + ) + { + var query = new GetCatalogByIdQuery(id); + + var result = await queryDispatcher.QueryAsync(query, ct); + + if (result is null) return TypedResults.NotFound(); + + return TypedResults.Ok(result); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdHandler.cs new file mode 100644 index 0000000..5f173df --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdHandler.cs @@ -0,0 +1,19 @@ +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Shared.Abstractions.Queries; + +namespace DotNetBoilerplate.Application.Catalogs.Browse; + +internal sealed class BrowseBooksByBookStoreIdHandler( + ICatalogRepository catalogRepository +) : IQueryHandler> +{ + public async Task> HandleAsync(BrowseCatalogsByBookStoreIdQuery query) + { + var books = await catalogRepository.GetAllInBookStore(query.Id); + + return books + .Select(x => new CatalogDto(x.Id, x.Name, x.Genre, x.Description)) + .ToList(); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdQuery.cs b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdQuery.cs new file mode 100644 index 0000000..fcceaa5 --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdQuery.cs @@ -0,0 +1,6 @@ +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Shared.Abstractions.Queries; + +namespace DotNetBoilerplate.Application.Catalogs.Browse; + +public sealed record BrowseCatalogsByBookStoreIdQuery(Guid Id) : IQuery>; \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsHandler.cs new file mode 100644 index 0000000..24b5ccc --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsHandler.cs @@ -0,0 +1,19 @@ +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Shared.Abstractions.Queries; + +namespace DotNetBoilerplate.Application.Catalogs.Browse; + +internal sealed class BrowseCatalogsHandler( + ICatalogRepository catalogRepository +) : IQueryHandler> +{ + public async Task> HandleAsync(BrowseCatalogsQuery query) + { + var books = await catalogRepository.GetAll(); + + return books + .Select(x => new CatalogDto(x.Id, x.Name, x.Genre, x.Description)) + .ToList(); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsQuery.cs b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsQuery.cs new file mode 100644 index 0000000..5ab338a --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsQuery.cs @@ -0,0 +1,6 @@ +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Shared.Abstractions.Queries; + +namespace DotNetBoilerplate.Application.Catalogs.Browse; + +public sealed record BrowseCatalogsQuery : IQuery>; \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/DTO/CatalogDto.cs b/src/DotNetBoilerplate.Application/Catalogs/DTO/CatalogDto.cs new file mode 100644 index 0000000..c96a53b --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/DTO/CatalogDto.cs @@ -0,0 +1,8 @@ +namespace DotNetBoilerplate.Application.Catalogs.DTO; + +public sealed record CatalogDto( + Guid Id, + string Name, + string Genre, + string Description +); \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdHandler.cs new file mode 100644 index 0000000..5a3f96c --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdHandler.cs @@ -0,0 +1,24 @@ +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Shared.Abstractions.Queries; +using DotNetBoilerplate.Application.Exceptions; +namespace DotNetBoilerplate.Application.Catalogs.Get; + +internal sealed class GetBookStoreByIdHandler( + ICatalogRepository catalogRepository +) : IQueryHandler +{ + public async Task HandleAsync(GetCatalogByIdQuery query) + { + var catalog = await catalogRepository.GetByIdAsync(query.Id); + if (catalog is null) + return null; + + return new CatalogDto( + catalog.Id, + catalog.Name, + catalog.Genre, + catalog.Description + ); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdQuery.cs b/src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdQuery.cs new file mode 100644 index 0000000..cc7714e --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdQuery.cs @@ -0,0 +1,6 @@ +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Shared.Abstractions.Queries; + +namespace DotNetBoilerplate.Application.Catalogs.Get; + +public sealed record GetCatalogByIdQuery(Guid Id) : IQuery; \ No newline at end of file From 9b59ff697176fa405b1800335d2eee324ac17d3f Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Sun, 28 Jul 2024 22:06:15 +0200 Subject: [PATCH 05/16] Delete catalog --- .../Catalogs/CatalogsEndpoints.cs | 3 +- .../Catalogs/DeleteCatalogEndpoint.cs | 30 +++++++++++++++++++ .../Catalogs/Delete/DeleteCatalogCommand.cs | 5 ++++ .../Catalogs/Delete/DeleteCatalogHandler.cs | 24 +++++++++++++++ .../UserCanNotDeleteCatalogException.cs | 6 ++++ 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/DotNetBoilerplate.Api/Catalogs/DeleteCatalogEndpoint.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogCommand.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogHandler.cs create mode 100644 src/DotNetBoilerplate.Application/Exceptions/UserCanNotDeleteCatalogException.cs diff --git a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs index fd1b106..2c88068 100644 --- a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs +++ b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs @@ -17,7 +17,8 @@ public static void MapCatalogsEndpoints(this WebApplication app) .MapEndpoint() .MapEndpoint() .MapEndpoint() - .MapEndpoint(); + .MapEndpoint() + .MapEndpoint(); } } diff --git a/src/DotNetBoilerplate.Api/Catalogs/DeleteCatalogEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/DeleteCatalogEndpoint.cs new file mode 100644 index 0000000..09d7cb8 --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/DeleteCatalogEndpoint.cs @@ -0,0 +1,30 @@ +using DotNetBoilerplate.Application.Catalogs.Delete; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +internal sealed class DeleteCatalogEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapDelete("{id:guid}", Handle) + .RequireAuthorization() + .WithSummary("Delete catalog by Id"); + } + + private static async Task> Handle( + Guid id, + [FromServices] ICommandDispatcher commandDispatcher, + CancellationToken ct + ) + { + var command = new DeleteCatalogCommand(id); + + await commandDispatcher.DispatchAsync(command, ct); + + return TypedResults.Ok(new Response()); + } + internal sealed record Response(); +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogCommand.cs b/src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogCommand.cs new file mode 100644 index 0000000..3e6d1d4 --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogCommand.cs @@ -0,0 +1,5 @@ +using DotNetBoilerplate.Shared.Abstractions.Commands; + +namespace DotNetBoilerplate.Application.Catalogs.Delete; + +public sealed record DeleteCatalogCommand(Guid Id) : ICommand; diff --git a/src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogHandler.cs new file mode 100644 index 0000000..9e21bff --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogHandler.cs @@ -0,0 +1,24 @@ +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using DotNetBoilerplate.Shared.Abstractions.Contexts; +using DotNetBoilerplate.Application.Exceptions; + +namespace DotNetBoilerplate.Application.Catalogs.Delete; + +internal sealed class DeleteBookHandler( + IContext context, + ICatalogRepository catalogRepository + ) : ICommandHandler +{ + public async Task HandleAsync(DeleteCatalogCommand command) + { + var catalog = await catalogRepository.GetByIdAsync(command.Id); + + if (catalog is null) + throw new CatalogNotFoundException(); + if (catalog.CreatedBy != context.Identity.Id) + throw new UserCanNotDeleteBookException(); + + await catalogRepository.DeleteAsync(catalog); + } +} diff --git a/src/DotNetBoilerplate.Application/Exceptions/UserCanNotDeleteCatalogException.cs b/src/DotNetBoilerplate.Application/Exceptions/UserCanNotDeleteCatalogException.cs new file mode 100644 index 0000000..d2e575f --- /dev/null +++ b/src/DotNetBoilerplate.Application/Exceptions/UserCanNotDeleteCatalogException.cs @@ -0,0 +1,6 @@ +using DotNetBoilerplate.Shared.Abstractions.Exceptions; + +namespace DotNetBoilerplate.Application.Exceptions; + +internal sealed class UserCanNotDeleteCatalogException() + : CustomException("User does not have permission to delete this catalog."); From 14b5db90cce474f25e32d9df42e17307869a3001 Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Mon, 29 Jul 2024 11:08:10 +0200 Subject: [PATCH 06/16] add book to catalog --- src/DotNetBoilerplate.Core/Catalogs/Catalog.cs | 11 +++++++++-- .../Exceptions/BookAlreadyAddedToCatalogException.cs | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/DotNetBoilerplate.Core/Catalogs/Exceptions/BookAlreadyAddedToCatalogException.cs diff --git a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs index ae1c3c3..86a4e8f 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs @@ -1,4 +1,5 @@ -using DotNetBoilerplate.Core.Catalogs.Exceptions; +using DotNetBoilerplate.Core.Books; +using DotNetBoilerplate.Core.Catalogs.Exceptions; namespace DotNetBoilerplate.Core.Catalogs; @@ -10,6 +11,7 @@ private Catalog() public string Name { get; private set; } public string Genre { get; private set; } public string Description { get; private set; } + public List Books { get; private set; } public Guid BookStoreId { get; private set; } public Guid CreatedBy { get; private set; } @@ -49,5 +51,10 @@ Guid updater Genre = genre; Description = description; } - + public void AddBook(Book book) + { + if (Books.Any(x => x.Id == book.Id)) + throw new BookAlreadyAddedToCatalogException(); + Books.Add(book); + } } diff --git a/src/DotNetBoilerplate.Core/Catalogs/Exceptions/BookAlreadyAddedToCatalogException.cs b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/BookAlreadyAddedToCatalogException.cs new file mode 100644 index 0000000..9b32bbb --- /dev/null +++ b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/BookAlreadyAddedToCatalogException.cs @@ -0,0 +1,8 @@ +using DotNetBoilerplate.Shared.Abstractions.Exceptions; + +namespace DotNetBoilerplate.Core.Catalogs.Exceptions; + +internal sealed class BookAlreadyAddedToCatalogException() + : CustomException("Book is already added to a catalog.") +{ +}; From 5558d2551b45235e6a77fe77ad79c5e2759ca2ad Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Fri, 26 Jul 2024 14:16:35 +0200 Subject: [PATCH 07/16] core --- .../Catalogs/Catalog.cs | 30 +++++++++++++++++++ .../Catalogs/ICatalogRepository.cs | 10 +++++++ .../Repositories/InMemoryCatalogRepository.cs | 14 +++++++++ 3 files changed, 54 insertions(+) create mode 100644 src/DotNetBoilerplate.Core/Catalogs/Catalog.cs create mode 100644 src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs create mode 100644 src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs diff --git a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs new file mode 100644 index 0000000..db8b1fa --- /dev/null +++ b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs @@ -0,0 +1,30 @@ + +namespace DotNetBoilerplate.Core.Catalogs; + +public sealed class Catalog +{ + private Catalog() + { } + public Guid Id { get; private set; } + public string Name { get; private set; } + public string Genre { get; private set; } + public string Description { get; private set; } + public Guid BookStoreId { get; private set; } + + public static Catalog Create( + string name, + string genre, + string description, + Guid bookStoreId + ) + { + return new Catalog + { + Id = Guid.NewGuid(), + Name = name, + Genre = genre, + Description = description, + BookStoreId = bookStoreId + }; + } +} diff --git a/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs new file mode 100644 index 0000000..f58bd72 --- /dev/null +++ b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs @@ -0,0 +1,10 @@ + + +namespace DotNetBoilerplate.Core.Catalogs; + +public interface ICatalogRepository +{ + Task AddAsync(Catalog catalog); + + +} diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs new file mode 100644 index 0000000..0a5c4ca --- /dev/null +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs @@ -0,0 +1,14 @@ +using DotNetBoilerplate.Core.Catalogs; + +namespace DotNetBoilerplate.Infrastructure.DAL.Repositories; + +internal sealed class InMemoryCatalogRepository : ICatalogRepository +{ + private readonly List _catalogs = []; + + public Task AddAsync(Catalog catalog) + { + _catalogs.Add(catalog); + return Task.CompletedTask; + } +} From 078e2af5ff7945f83761df936f4baa216f57b728 Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Fri, 26 Jul 2024 17:40:29 +0200 Subject: [PATCH 08/16] added Creating Catalog --- .../Catalogs/CatalogsEndpoints.cs | 19 ++++++++ .../Catalogs/CreateCatalogEndpoint.cs | 46 +++++++++++++++++++ src/DotNetBoilerplate.Api/Program.cs | 2 + .../Catalogs/Create/CreateCatalogCommand.cs | 5 ++ .../Catalogs/Create/CreateCatalogHandler.cs | 33 +++++++++++++ .../Catalogs/Catalog.cs | 9 +++- .../UserCanNotCreateCatalogException.cs | 8 ++++ .../Catalogs/ICatalogRepository.cs | 2 +- .../DAL/Repositories/Extensions.cs | 2 + .../Repositories/InMemoryCatalogRepository.cs | 6 +++ 10 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs create mode 100644 src/DotNetBoilerplate.Api/Catalogs/CreateCatalogEndpoint.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogCommand.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs create mode 100644 src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotCreateCatalogException.cs diff --git a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs new file mode 100644 index 0000000..9d1fc35 --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs @@ -0,0 +1,19 @@ +using DotNetBoilerplate.Api.Catalogs; + +namespace DotNetBoilerplate.Api.Catalogs; + +internal static class CatalogsEndpoints +{ + public const string BasePath = "catalogs"; + public const string Tags = "Catalogs"; + + public static void MapCatalogsEndpoints(this WebApplication app) + { + var group = app.MapGroup(BasePath) + .WithTags(Tags); + + group + .MapEndpoint(); + } +} + diff --git a/src/DotNetBoilerplate.Api/Catalogs/CreateCatalogEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/CreateCatalogEndpoint.cs new file mode 100644 index 0000000..ba8e342 --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/CreateCatalogEndpoint.cs @@ -0,0 +1,46 @@ +using System.ComponentModel.DataAnnotations; +using DotNetBoilerplate.Application.Catalogs.Create; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +internal sealed class CreateCatalogEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapPost("", Handle) + .RequireAuthorization() + .WithSummary("Create catalog"); + } + private static async Task> Handle( + [FromBody] Request request, + [FromServices] ICommandDispatcher commandDispatcher, + CancellationToken ct + ) + { + var command = new CreateCatalogCommand( + request.Name, + request.Genre, + request.Description + ); + + var result = await commandDispatcher.DispatchAsync(command, ct); + + return TypedResults.Ok(new Response(result)); + } + + internal sealed record Response( + Guid Id + ); + + private sealed class Request + { + [Required] public string Name { get; init; } + + [Required] public string Genre { get; init; } + + [Required] public string Description { get; init; } + } +} diff --git a/src/DotNetBoilerplate.Api/Program.cs b/src/DotNetBoilerplate.Api/Program.cs index 3085d98..4c06737 100644 --- a/src/DotNetBoilerplate.Api/Program.cs +++ b/src/DotNetBoilerplate.Api/Program.cs @@ -3,6 +3,7 @@ using DotNetBoilerplate.Api.Books; using DotNetBoilerplate.Api.Reviews; using DotNetBoilerplate.Api.Users; +using DotNetBoilerplate.Api.Catalogs; using DotNetBoilerplate.Application; using DotNetBoilerplate.Core; using DotNetBoilerplate.Infrastructure; @@ -24,6 +25,7 @@ app.MapBookStoresEndpoints(); app.MapBookEndpoints(); app.MapReviewEndpoints(); +app.MapCatalogsEndpoints(); app.UseInfrastructure(); diff --git a/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogCommand.cs b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogCommand.cs new file mode 100644 index 0000000..63b10ac --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogCommand.cs @@ -0,0 +1,5 @@ +using DotNetBoilerplate.Shared.Abstractions.Commands; + +namespace DotNetBoilerplate.Application.Catalogs.Create; + +public sealed record CreateCatalogCommand(string Name, string Genre, string Description) : ICommand; \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs new file mode 100644 index 0000000..7a5bef6 --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs @@ -0,0 +1,33 @@ +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Core.BookStores; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using DotNetBoilerplate.Application.Exceptions; +using DotNetBoilerplate.Shared.Abstractions.Contexts; + +namespace DotNetBoilerplate.Application.Catalogs.Create; + +internal sealed class CreateCatalogHandler( + IContext context, + ICatalogRepository catalogRepository, + IBookStoreRepository bookStoreRepository +) : ICommandHandler +{ + public async Task HandleAsync(CreateCatalogCommand command) + { + var bookStore = await bookStoreRepository.GetByOwnerIdAsync(context.Identity.Id); + if (bookStore is null) + throw new BookStoreNotFoundException(); + + bool userCanNotCreateCatalog = await catalogRepository.UserCanNotAddCatalogAsync(bookStore.Id); + + var catalog = Catalog.Create( + command.Name, + command.Genre, + command.Description, + bookStore.Id, + userCanNotCreateCatalog + ); + await catalogRepository.AddAsync(catalog); + return catalog.Id; + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs index db8b1fa..dca5b33 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs @@ -1,4 +1,5 @@ - +using DotNetBoilerplate.Core.Catalogs.Exceptions; + namespace DotNetBoilerplate.Core.Catalogs; public sealed class Catalog @@ -15,9 +16,13 @@ public static Catalog Create( string name, string genre, string description, - Guid bookStoreId + Guid bookStoreId, + bool userCanNotCreateCatalog ) { + if (userCanNotCreateCatalog) + throw new UserCanNotCreateCatalogException(); + return new Catalog { Id = Guid.NewGuid(), diff --git a/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotCreateCatalogException.cs b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotCreateCatalogException.cs new file mode 100644 index 0000000..54c2cb1 --- /dev/null +++ b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotCreateCatalogException.cs @@ -0,0 +1,8 @@ +using DotNetBoilerplate.Shared.Abstractions.Exceptions; + +namespace DotNetBoilerplate.Core.Catalogs.Exceptions; + +internal sealed class UserCanNotCreateCatalogException() + : CustomException("User can not create catalog.") +{ +}; diff --git a/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs index f58bd72..8bee71a 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs @@ -6,5 +6,5 @@ public interface ICatalogRepository { Task AddAsync(Catalog catalog); - + Task UserCanNotAddCatalogAsync(Guid bookStoreId); } diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs index 5b87612..ba1df23 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs @@ -3,6 +3,7 @@ using DotNetBoilerplate.Core.Users; using DotNetBoilerplate.Core.Reviews; using Microsoft.Extensions.DependencyInjection; +using DotNetBoilerplate.Core.Catalogs; namespace DotNetBoilerplate.Infrastructure.DAL.Repositories; @@ -14,6 +15,7 @@ public static IServiceCollection AddRepositories(this IServiceCollection service services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddSingleton(); return services; } diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs index 0a5c4ca..4f62fd4 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs @@ -11,4 +11,10 @@ public Task AddAsync(Catalog catalog) _catalogs.Add(catalog); return Task.CompletedTask; } + + public Task UserCanNotAddCatalogAsync(Guid bookStoreId) + { + var count = _catalogs.Count(x => x.BookStoreId == bookStoreId); + return Task.FromResult(count >= 5); + } } From 9918b1f2f77947ce797cd3e2c1c944357ccee11d Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Sun, 28 Jul 2024 15:14:21 +0200 Subject: [PATCH 09/16] Update catalog, fix create catalog --- .../Catalogs/CatalogsEndpoints.cs | 3 +- .../Catalogs/UpdateCatalogEndpoint.cs | 52 +++++++++++++++++++ .../Catalogs/Create/CreateCatalogHandler.cs | 1 + .../Catalogs/Update/UpdateCatalogCommand.cs | 5 ++ .../Catalogs/Update/UpdateCatalogHandler.cs | 29 +++++++++++ .../Exceptions/CatalogNotFoundException.cs | 6 +++ .../Catalogs/Catalog.cs | 20 ++++++- .../UserCanNotUpdateCatalogException.cs | 8 +++ .../Catalogs/ICatalogRepository.cs | 10 ++++ .../Repositories/InMemoryCatalogRepository.cs | 30 +++++++++++ 10 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 src/DotNetBoilerplate.Api/Catalogs/UpdateCatalogEndpoint.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogCommand.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogHandler.cs create mode 100644 src/DotNetBoilerplate.Application/Exceptions/CatalogNotFoundException.cs create mode 100644 src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotUpdateCatalogException.cs diff --git a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs index 9d1fc35..90f1274 100644 --- a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs +++ b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs @@ -13,7 +13,8 @@ public static void MapCatalogsEndpoints(this WebApplication app) .WithTags(Tags); group - .MapEndpoint(); + .MapEndpoint() + .MapEndpoint(); } } diff --git a/src/DotNetBoilerplate.Api/Catalogs/UpdateCatalogEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/UpdateCatalogEndpoint.cs new file mode 100644 index 0000000..8cef77f --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/UpdateCatalogEndpoint.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations; +using DotNetBoilerplate.Application.Catalogs.Update; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +public class UpdateCatalogEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapPut("{id:guid}", Handle) + .RequireAuthorization() + .WithSummary("Update a catalog"); + } + + private static async Task> Handle( + [FromRoute] Guid id, + [FromBody] Request request, + [FromServices] ICommandDispatcher commandDispatcher, + CancellationToken ct + ) + { + var command = new UpdateCatalogCommand( + id, + request.Name, + request.Genre, + request.Description + ); + + var result = await commandDispatcher.DispatchAsync(command, ct); + + return TypedResults.Ok(new Response(result)); + } + + internal sealed record Response( + Guid Id + ); + + private sealed class Request + { + // ReSharper disable once UnusedAutoPropertyAccessor.Local + [Required] public string Name { get; init; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + [Required] public string Genre { get; init; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + [Required] public string Description { get; init; } + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs index 7a5bef6..05cbe9c 100644 --- a/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs +++ b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs @@ -25,6 +25,7 @@ public async Task HandleAsync(CreateCatalogCommand command) command.Genre, command.Description, bookStore.Id, + context.Identity.Id, userCanNotCreateCatalog ); await catalogRepository.AddAsync(catalog); diff --git a/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogCommand.cs b/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogCommand.cs new file mode 100644 index 0000000..8b9be3e --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogCommand.cs @@ -0,0 +1,5 @@ +using DotNetBoilerplate.Shared.Abstractions.Commands; + +namespace DotNetBoilerplate.Application.Catalogs.Update; + +public sealed record UpdateCatalogCommand(Guid Id, string Name, string Genre, string Description) : ICommand; \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogHandler.cs new file mode 100644 index 0000000..feb36bb --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogHandler.cs @@ -0,0 +1,29 @@ +using DotNetBoilerplate.Application.Exceptions; +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using DotNetBoilerplate.Shared.Abstractions.Contexts; + +namespace DotNetBoilerplate.Application.Catalogs.Update; + +internal sealed class UpdateCatalogHandler( + IContext context, + ICatalogRepository catalogRepository +) : ICommandHandler +{ + public async Task HandleAsync(UpdateCatalogCommand command) + { + var catalog = await catalogRepository.GetByIdAsync(command.Id); + if (catalog is null) + throw new BookNotFoundException(); + + catalog.Update( + command.Name, + command.Genre, + command.Description, + context.Identity.Id + ); + + await catalogRepository.UpdateAsync(catalog); + return catalog.Id; + } +} diff --git a/src/DotNetBoilerplate.Application/Exceptions/CatalogNotFoundException.cs b/src/DotNetBoilerplate.Application/Exceptions/CatalogNotFoundException.cs new file mode 100644 index 0000000..9b9251c --- /dev/null +++ b/src/DotNetBoilerplate.Application/Exceptions/CatalogNotFoundException.cs @@ -0,0 +1,6 @@ +using DotNetBoilerplate.Shared.Abstractions.Exceptions; + +namespace DotNetBoilerplate.Application.Exceptions; + +internal sealed class CatalogNotFoundException() + : CustomException("Catalog not found"); diff --git a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs index dca5b33..ae1c3c3 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs @@ -11,12 +11,14 @@ private Catalog() public string Genre { get; private set; } public string Description { get; private set; } public Guid BookStoreId { get; private set; } + public Guid CreatedBy { get; private set; } public static Catalog Create( string name, string genre, string description, Guid bookStoreId, + Guid createdBy, bool userCanNotCreateCatalog ) { @@ -29,7 +31,23 @@ bool userCanNotCreateCatalog Name = name, Genre = genre, Description = description, - BookStoreId = bookStoreId + BookStoreId = bookStoreId, + CreatedBy = createdBy }; } + public void Update( + string name, + string genre, + string description, + Guid updater + ) + { + if (updater == Guid.Empty) + throw new UserCanNotUpdateCatalogException(); + + Name = name; + Genre = genre; + Description = description; + } + } diff --git a/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotUpdateCatalogException.cs b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotUpdateCatalogException.cs new file mode 100644 index 0000000..b6853c8 --- /dev/null +++ b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotUpdateCatalogException.cs @@ -0,0 +1,8 @@ +using DotNetBoilerplate.Shared.Abstractions.Exceptions; + +namespace DotNetBoilerplate.Core.Catalogs.Exceptions; + +internal sealed class UserCanNotUpdateCatalogException() + : CustomException("User can not update catalog.") +{ +}; diff --git a/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs index 8bee71a..27ba546 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs @@ -7,4 +7,14 @@ public interface ICatalogRepository Task AddAsync(Catalog catalog); Task UserCanNotAddCatalogAsync(Guid bookStoreId); + + Task UpdateAsync(Catalog catalog); + + Task> GetAll(); + + Task GetByIdAsync(Guid id); + + Task> GetAllInBookStore(Guid bookStoreId); + + Task DeleteAsync(Catalog catalog); } diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs index 4f62fd4..21ac34e 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs @@ -17,4 +17,34 @@ public Task UserCanNotAddCatalogAsync(Guid bookStoreId) var count = _catalogs.Count(x => x.BookStoreId == bookStoreId); return Task.FromResult(count >= 5); } + + public Task UpdateAsync(Catalog catalog) + { + var existingCatalogIndex = _catalogs.FindIndex(x => x.Id == catalog.Id); + _catalogs[existingCatalogIndex] = catalog; + + return Task.CompletedTask; + } + + public Task> GetAll() + { + return Task.FromResult(_catalogs.AsEnumerable()); + } + + public Task GetByIdAsync(Guid id) + { + return Task.FromResult(_catalogs.FirstOrDefault(x => x.Id == id)); + } + + public Task> GetAllInBookStore(Guid bookStoreId) + { + return Task.FromResult(_catalogs.FindAll(x => x.BookStoreId == bookStoreId) + .AsEnumerable()); + } + + public Task DeleteAsync(Catalog catalog) + { + _catalogs.Remove(catalog); + return Task.CompletedTask; + } } From e6ddf509f4652f485d6480b9e1ae8a26b60cfe6a Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Sun, 28 Jul 2024 21:36:49 +0200 Subject: [PATCH 10/16] Get browse catalogs --- .../BrowseCatalogsByBookStoreIdEndpoint.cs | 32 +++++++++++++++++++ .../Catalogs/BrowseCatalogsEndpoint.cs | 28 ++++++++++++++++ .../Catalogs/CatalogsEndpoints.cs | 5 ++- .../Catalogs/GetCatalogByIdEndpoint.cs | 31 ++++++++++++++++++ .../BrowseCatalogsByBookStoreIdHandler.cs | 19 +++++++++++ .../BrowseCatalogsByBookStoreIdQuery.cs | 6 ++++ .../Catalogs/Browse/BrowseCatalogsHandler.cs | 19 +++++++++++ .../Catalogs/Browse/BrowseCatalogsQuery.cs | 6 ++++ .../Catalogs/DTO/CatalogDto.cs | 8 +++++ .../Catalogs/Get/GetCatalogByIdHandler.cs | 24 ++++++++++++++ .../Catalogs/Get/GetCatalogByIdQuery.cs | 6 ++++ 11 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsByBookStoreIdEndpoint.cs create mode 100644 src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsEndpoint.cs create mode 100644 src/DotNetBoilerplate.Api/Catalogs/GetCatalogByIdEndpoint.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdHandler.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdQuery.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsHandler.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsQuery.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/DTO/CatalogDto.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdHandler.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdQuery.cs diff --git a/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsByBookStoreIdEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsByBookStoreIdEndpoint.cs new file mode 100644 index 0000000..85c044c --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsByBookStoreIdEndpoint.cs @@ -0,0 +1,32 @@ +using DotNetBoilerplate.Application.Catalogs.Browse; +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Application.Catalogs.Get; +using DotNetBoilerplate.Shared.Abstractions.Queries; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +public class BrowseCatalogsByBookStoreIdEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapGet("bookstore/{id:guid}", Handle) + .WithSummary("Browse all Catalogs in Book Store"); + } + + private static async Task>, NotFound>> Handle( + [FromRoute] Guid id, + [FromServices] IQueryDispatcher queryDispatcher, + CancellationToken ct + ) + { + var query = new BrowseCatalogsByBookStoreIdQuery(id); + + var result = await queryDispatcher.QueryAsync(query, ct); + + if (result is null) return TypedResults.NotFound(); + + return TypedResults.Ok(result); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsEndpoint.cs new file mode 100644 index 0000000..016b69a --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsEndpoint.cs @@ -0,0 +1,28 @@ +using DotNetBoilerplate.Application.Catalogs.Browse; +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Shared.Abstractions.Queries; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +public class BrowseCatalogsEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapGet("", Handle) + .WithSummary("Browse all catalogs"); + } + + private static async Task>> Handle( + [FromServices] IQueryDispatcher queryDispatcher, + CancellationToken ct + ) + { + var query = new BrowseCatalogsQuery(); + + var result = await queryDispatcher.QueryAsync(query, ct); + + return TypedResults.Ok(result); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs index 90f1274..fd1b106 100644 --- a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs +++ b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs @@ -14,7 +14,10 @@ public static void MapCatalogsEndpoints(this WebApplication app) group .MapEndpoint() - .MapEndpoint(); + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint(); } } diff --git a/src/DotNetBoilerplate.Api/Catalogs/GetCatalogByIdEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/GetCatalogByIdEndpoint.cs new file mode 100644 index 0000000..0f6fcbc --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/GetCatalogByIdEndpoint.cs @@ -0,0 +1,31 @@ +using DotNetBoilerplate.Application.Catalogs.Get; +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Shared.Abstractions.Queries; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +public class GetCatalogByIdEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapGet("{id:guid}", Handle) + .WithSummary("Browse catalog by Id"); + } + + private static async Task, NotFound>> Handle( + [FromRoute] Guid id, + [FromServices] IQueryDispatcher queryDispatcher, + CancellationToken ct + ) + { + var query = new GetCatalogByIdQuery(id); + + var result = await queryDispatcher.QueryAsync(query, ct); + + if (result is null) return TypedResults.NotFound(); + + return TypedResults.Ok(result); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdHandler.cs new file mode 100644 index 0000000..5f173df --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdHandler.cs @@ -0,0 +1,19 @@ +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Shared.Abstractions.Queries; + +namespace DotNetBoilerplate.Application.Catalogs.Browse; + +internal sealed class BrowseBooksByBookStoreIdHandler( + ICatalogRepository catalogRepository +) : IQueryHandler> +{ + public async Task> HandleAsync(BrowseCatalogsByBookStoreIdQuery query) + { + var books = await catalogRepository.GetAllInBookStore(query.Id); + + return books + .Select(x => new CatalogDto(x.Id, x.Name, x.Genre, x.Description)) + .ToList(); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdQuery.cs b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdQuery.cs new file mode 100644 index 0000000..fcceaa5 --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdQuery.cs @@ -0,0 +1,6 @@ +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Shared.Abstractions.Queries; + +namespace DotNetBoilerplate.Application.Catalogs.Browse; + +public sealed record BrowseCatalogsByBookStoreIdQuery(Guid Id) : IQuery>; \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsHandler.cs new file mode 100644 index 0000000..24b5ccc --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsHandler.cs @@ -0,0 +1,19 @@ +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Shared.Abstractions.Queries; + +namespace DotNetBoilerplate.Application.Catalogs.Browse; + +internal sealed class BrowseCatalogsHandler( + ICatalogRepository catalogRepository +) : IQueryHandler> +{ + public async Task> HandleAsync(BrowseCatalogsQuery query) + { + var books = await catalogRepository.GetAll(); + + return books + .Select(x => new CatalogDto(x.Id, x.Name, x.Genre, x.Description)) + .ToList(); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsQuery.cs b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsQuery.cs new file mode 100644 index 0000000..5ab338a --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsQuery.cs @@ -0,0 +1,6 @@ +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Shared.Abstractions.Queries; + +namespace DotNetBoilerplate.Application.Catalogs.Browse; + +public sealed record BrowseCatalogsQuery : IQuery>; \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/DTO/CatalogDto.cs b/src/DotNetBoilerplate.Application/Catalogs/DTO/CatalogDto.cs new file mode 100644 index 0000000..c96a53b --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/DTO/CatalogDto.cs @@ -0,0 +1,8 @@ +namespace DotNetBoilerplate.Application.Catalogs.DTO; + +public sealed record CatalogDto( + Guid Id, + string Name, + string Genre, + string Description +); \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdHandler.cs new file mode 100644 index 0000000..5a3f96c --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdHandler.cs @@ -0,0 +1,24 @@ +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Shared.Abstractions.Queries; +using DotNetBoilerplate.Application.Exceptions; +namespace DotNetBoilerplate.Application.Catalogs.Get; + +internal sealed class GetBookStoreByIdHandler( + ICatalogRepository catalogRepository +) : IQueryHandler +{ + public async Task HandleAsync(GetCatalogByIdQuery query) + { + var catalog = await catalogRepository.GetByIdAsync(query.Id); + if (catalog is null) + return null; + + return new CatalogDto( + catalog.Id, + catalog.Name, + catalog.Genre, + catalog.Description + ); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdQuery.cs b/src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdQuery.cs new file mode 100644 index 0000000..cc7714e --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Get/GetCatalogByIdQuery.cs @@ -0,0 +1,6 @@ +using DotNetBoilerplate.Application.Catalogs.DTO; +using DotNetBoilerplate.Shared.Abstractions.Queries; + +namespace DotNetBoilerplate.Application.Catalogs.Get; + +public sealed record GetCatalogByIdQuery(Guid Id) : IQuery; \ No newline at end of file From 4a59b1b18129edd9fcd2b42d1b421a9b76b0360a Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Sun, 28 Jul 2024 22:06:15 +0200 Subject: [PATCH 11/16] Delete catalog --- .../Catalogs/CatalogsEndpoints.cs | 3 +- .../Catalogs/DeleteCatalogEndpoint.cs | 30 +++++++++++++++++++ .../Catalogs/Delete/DeleteCatalogCommand.cs | 5 ++++ .../Catalogs/Delete/DeleteCatalogHandler.cs | 24 +++++++++++++++ .../UserCanNotDeleteCatalogException.cs | 6 ++++ 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/DotNetBoilerplate.Api/Catalogs/DeleteCatalogEndpoint.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogCommand.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogHandler.cs create mode 100644 src/DotNetBoilerplate.Application/Exceptions/UserCanNotDeleteCatalogException.cs diff --git a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs index fd1b106..2c88068 100644 --- a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs +++ b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs @@ -17,7 +17,8 @@ public static void MapCatalogsEndpoints(this WebApplication app) .MapEndpoint() .MapEndpoint() .MapEndpoint() - .MapEndpoint(); + .MapEndpoint() + .MapEndpoint(); } } diff --git a/src/DotNetBoilerplate.Api/Catalogs/DeleteCatalogEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/DeleteCatalogEndpoint.cs new file mode 100644 index 0000000..09d7cb8 --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/DeleteCatalogEndpoint.cs @@ -0,0 +1,30 @@ +using DotNetBoilerplate.Application.Catalogs.Delete; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +internal sealed class DeleteCatalogEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapDelete("{id:guid}", Handle) + .RequireAuthorization() + .WithSummary("Delete catalog by Id"); + } + + private static async Task> Handle( + Guid id, + [FromServices] ICommandDispatcher commandDispatcher, + CancellationToken ct + ) + { + var command = new DeleteCatalogCommand(id); + + await commandDispatcher.DispatchAsync(command, ct); + + return TypedResults.Ok(new Response()); + } + internal sealed record Response(); +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogCommand.cs b/src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogCommand.cs new file mode 100644 index 0000000..3e6d1d4 --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogCommand.cs @@ -0,0 +1,5 @@ +using DotNetBoilerplate.Shared.Abstractions.Commands; + +namespace DotNetBoilerplate.Application.Catalogs.Delete; + +public sealed record DeleteCatalogCommand(Guid Id) : ICommand; diff --git a/src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogHandler.cs new file mode 100644 index 0000000..9e21bff --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/Delete/DeleteCatalogHandler.cs @@ -0,0 +1,24 @@ +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using DotNetBoilerplate.Shared.Abstractions.Contexts; +using DotNetBoilerplate.Application.Exceptions; + +namespace DotNetBoilerplate.Application.Catalogs.Delete; + +internal sealed class DeleteBookHandler( + IContext context, + ICatalogRepository catalogRepository + ) : ICommandHandler +{ + public async Task HandleAsync(DeleteCatalogCommand command) + { + var catalog = await catalogRepository.GetByIdAsync(command.Id); + + if (catalog is null) + throw new CatalogNotFoundException(); + if (catalog.CreatedBy != context.Identity.Id) + throw new UserCanNotDeleteBookException(); + + await catalogRepository.DeleteAsync(catalog); + } +} diff --git a/src/DotNetBoilerplate.Application/Exceptions/UserCanNotDeleteCatalogException.cs b/src/DotNetBoilerplate.Application/Exceptions/UserCanNotDeleteCatalogException.cs new file mode 100644 index 0000000..d2e575f --- /dev/null +++ b/src/DotNetBoilerplate.Application/Exceptions/UserCanNotDeleteCatalogException.cs @@ -0,0 +1,6 @@ +using DotNetBoilerplate.Shared.Abstractions.Exceptions; + +namespace DotNetBoilerplate.Application.Exceptions; + +internal sealed class UserCanNotDeleteCatalogException() + : CustomException("User does not have permission to delete this catalog."); From ed8695f3a567c9254ef0e3a3259fc5df90b46d54 Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Mon, 29 Jul 2024 11:08:10 +0200 Subject: [PATCH 12/16] add book to catalog --- src/DotNetBoilerplate.Core/Catalogs/Catalog.cs | 11 +++++++++-- .../Exceptions/BookAlreadyAddedToCatalogException.cs | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/DotNetBoilerplate.Core/Catalogs/Exceptions/BookAlreadyAddedToCatalogException.cs diff --git a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs index ae1c3c3..86a4e8f 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs @@ -1,4 +1,5 @@ -using DotNetBoilerplate.Core.Catalogs.Exceptions; +using DotNetBoilerplate.Core.Books; +using DotNetBoilerplate.Core.Catalogs.Exceptions; namespace DotNetBoilerplate.Core.Catalogs; @@ -10,6 +11,7 @@ private Catalog() public string Name { get; private set; } public string Genre { get; private set; } public string Description { get; private set; } + public List Books { get; private set; } public Guid BookStoreId { get; private set; } public Guid CreatedBy { get; private set; } @@ -49,5 +51,10 @@ Guid updater Genre = genre; Description = description; } - + public void AddBook(Book book) + { + if (Books.Any(x => x.Id == book.Id)) + throw new BookAlreadyAddedToCatalogException(); + Books.Add(book); + } } diff --git a/src/DotNetBoilerplate.Core/Catalogs/Exceptions/BookAlreadyAddedToCatalogException.cs b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/BookAlreadyAddedToCatalogException.cs new file mode 100644 index 0000000..9b32bbb --- /dev/null +++ b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/BookAlreadyAddedToCatalogException.cs @@ -0,0 +1,8 @@ +using DotNetBoilerplate.Shared.Abstractions.Exceptions; + +namespace DotNetBoilerplate.Core.Catalogs.Exceptions; + +internal sealed class BookAlreadyAddedToCatalogException() + : CustomException("Book is already added to a catalog.") +{ +}; From 1bb6984160d03355269cbbf14283b7e2f9c21768 Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Fri, 9 Aug 2024 11:12:56 +0200 Subject: [PATCH 13/16] catalog --- src/DotNetBoilerplate.Core/Catalogs/Catalog.cs | 10 ++++++++-- .../Write/CatalogWriteConfiguration.cs | 14 ++++++++++++++ .../Contexts/DotNetBoilerplateWriteDbContext.cs | 4 ++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 src/DotNetBoilerplate.Infrastructure/DAL/Configurations/Write/CatalogWriteConfiguration.cs diff --git a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs index 86a4e8f..f0ac7f3 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs @@ -14,6 +14,7 @@ private Catalog() public List Books { get; private set; } public Guid BookStoreId { get; private set; } public Guid CreatedBy { get; private set; } + public DateTime UpdatedAt { get; private set; } public static Catalog Create( string name, @@ -21,6 +22,7 @@ public static Catalog Create( string description, Guid bookStoreId, Guid createdBy, + DateTime updatedAt, bool userCanNotCreateCatalog ) { @@ -34,14 +36,17 @@ bool userCanNotCreateCatalog Genre = genre, Description = description, BookStoreId = bookStoreId, - CreatedBy = createdBy + CreatedBy = createdBy, + UpdatedAt = updatedAt }; } public void Update( string name, string genre, string description, - Guid updater + Guid updater, + DateTime updatedAt + ) { if (updater == Guid.Empty) @@ -50,6 +55,7 @@ Guid updater Name = name; Genre = genre; Description = description; + UpdatedAt = updatedAt; } public void AddBook(Book book) { diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Configurations/Write/CatalogWriteConfiguration.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Configurations/Write/CatalogWriteConfiguration.cs new file mode 100644 index 0000000..21b82d0 --- /dev/null +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Configurations/Write/CatalogWriteConfiguration.cs @@ -0,0 +1,14 @@ +using DotNetBoilerplate.Core.Catalogs; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace DotNetBoilerplate.Infrastructure.DAL.Configurations.Write; + +internal sealed class CatalogWriteConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Contexts/DotNetBoilerplateWriteDbContext.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Contexts/DotNetBoilerplateWriteDbContext.cs index 529972d..a9382be 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Contexts/DotNetBoilerplateWriteDbContext.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Contexts/DotNetBoilerplateWriteDbContext.cs @@ -2,6 +2,7 @@ using DotNetBoilerplate.Core.BookStores; using DotNetBoilerplate.Core.Users; using DotNetBoilerplate.Core.Reviews; +using DotNetBoilerplate.Core.Catalogs; using DotNetBoilerplate.Infrastructure.DAL.Configurations.Write; using DotNetBoilerplate.Shared.Abstractions.Outbox; using Microsoft.EntityFrameworkCore; @@ -20,6 +21,8 @@ internal sealed class DotNetBoilerplateWriteDbContext(DbContextOptions Reviews { get; set; } + public DbSet Catalogs { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema("dotNetBoilerplate"); @@ -29,5 +32,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfiguration(new BookStoreWriteConfiguration()); modelBuilder.ApplyConfiguration(new BookWriteConfiguration()); modelBuilder.ApplyConfiguration(new ReviewsWriteConfiguration()); + modelBuilder.ApplyConfiguration(new CatalogWriteConfiguration()); } } \ No newline at end of file From bb04bff11376a78f6f357278cdee4c766005f5f2 Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Mon, 12 Aug 2024 17:59:49 +0200 Subject: [PATCH 14/16] feat: add book to catalog, Browse books in catalog, Remove book from catalog --- .../Books/BrowseBooksEndpoint.cs | 2 +- .../Catalogs/AddBookToCatalogEndpoint.cs | 37 +++++++++++++++++++ .../Catalogs/BrowseBooksInCatalogEndpoint.cs | 30 +++++++++++++++ .../BrowseCatalogsByBookStoreIdEndpoint.cs | 32 ---------------- .../Catalogs/BrowseCatalogsEndpoint.cs | 12 +++--- .../Catalogs/CatalogsEndpoints.cs | 6 ++- .../Catalogs/DeleteCatalogEndpoint.cs | 5 +-- .../Catalogs/RemoveBookFromCatalogEndpoint.cs | 36 ++++++++++++++++++ .../AddBook/AddBookToCatalogCommand.cs | 5 +++ .../AddBook/AddBookToCatalogHandler.cs | 21 +++++++++++ .../BrowseCatalogsByBookStoreIdHandler.cs | 19 ---------- .../BrowseCatalogsByBookStoreIdQuery.cs | 6 --- .../Catalogs/Browse/BrowseCatalogsHandler.cs | 13 ++++--- .../Catalogs/Browse/BrowseCatalogsQuery.cs | 2 +- .../BrowseBooksInCatalogHandler.cs | 17 +++++++++ .../BrowseBooks/BrowseBooksInCatalogQuery.cs | 6 +++ .../Catalogs/Create/CreateCatalogHandler.cs | 5 ++- .../RemoveBookFromCatalogCommand.cs | 5 +++ .../RemoveBookFromCatalogHandler.cs | 29 +++++++++++++++ .../Catalogs/Update/UpdateCatalogHandler.cs | 8 +++- ...serCanNotRemoveBookFromCatalogException.cs | 6 +++ .../Catalogs/Catalog.cs | 17 +++++---- ...dateCatalogMoreThanOnceIn3DaysException.cs | 8 ++++ .../Catalogs/ICatalogRepository.cs | 10 ++++- .../Repositories/InMemoryCatalogRepository.cs | 28 +++++++++++--- .../Emails/Extensions.cs | 5 --- 26 files changed, 275 insertions(+), 95 deletions(-) create mode 100644 src/DotNetBoilerplate.Api/Catalogs/AddBookToCatalogEndpoint.cs create mode 100644 src/DotNetBoilerplate.Api/Catalogs/BrowseBooksInCatalogEndpoint.cs delete mode 100644 src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsByBookStoreIdEndpoint.cs create mode 100644 src/DotNetBoilerplate.Api/Catalogs/RemoveBookFromCatalogEndpoint.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/AddBook/AddBookToCatalogCommand.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/AddBook/AddBookToCatalogHandler.cs delete mode 100644 src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdHandler.cs delete mode 100644 src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdQuery.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/BrowseBooks/BrowseBooksInCatalogHandler.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/BrowseBooks/BrowseBooksInCatalogQuery.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/RemoveBook/RemoveBookFromCatalogCommand.cs create mode 100644 src/DotNetBoilerplate.Application/Catalogs/RemoveBook/RemoveBookFromCatalogHandler.cs create mode 100644 src/DotNetBoilerplate.Application/Exceptions/UserCanNotRemoveBookFromCatalogException.cs create mode 100644 src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotUpdateCatalogMoreThanOnceIn3DaysException.cs diff --git a/src/DotNetBoilerplate.Api/Books/BrowseBooksEndpoint.cs b/src/DotNetBoilerplate.Api/Books/BrowseBooksEndpoint.cs index 9b3d1dc..1e24c29 100644 --- a/src/DotNetBoilerplate.Api/Books/BrowseBooksEndpoint.cs +++ b/src/DotNetBoilerplate.Api/Books/BrowseBooksEndpoint.cs @@ -14,7 +14,7 @@ public static void Map(IEndpointRouteBuilder app) .WithSummary("Browse books with optional BookStoreId parameter"); } - private static async Task>, Ok, NotFound>> Handle( + private static async Task>, NotFound>> Handle( [FromQuery] Guid? bookStoreId, [FromServices] IQueryDispatcher queryDispatcher, CancellationToken ct diff --git a/src/DotNetBoilerplate.Api/Catalogs/AddBookToCatalogEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/AddBookToCatalogEndpoint.cs new file mode 100644 index 0000000..3cb278b --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/AddBookToCatalogEndpoint.cs @@ -0,0 +1,37 @@ +using DotNetBoilerplate.Application.Catalogs.AddBook; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +public class AddBookToCatalogEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapPost("{catalogId:guid}/books", Handle) + .RequireAuthorization() + .WithSummary("Add book to catalog"); + } + + private static async Task> Handle( + [FromRoute] Guid catalogId, + [FromBody] Request request, + [FromServices] ICommandDispatcher commandDispatcher, + CancellationToken ct + ) + { + var command = new AddBookToCatalogCommand(catalogId, request.BookId); + var result = await commandDispatcher.DispatchAsync(command, ct); + + return TypedResults.Ok(new Response(result)); + } + internal sealed record Response( + Guid Id + ); + + private class Request + { + public Guid BookId { get; init; } + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Api/Catalogs/BrowseBooksInCatalogEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/BrowseBooksInCatalogEndpoint.cs new file mode 100644 index 0000000..257cffe --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/BrowseBooksInCatalogEndpoint.cs @@ -0,0 +1,30 @@ +using DotNetBoilerplate.Application.Catalogs.BrowseBooks; +using DotNetBoilerplate.Application.Books.DTO; +using DotNetBoilerplate.Shared.Abstractions.Queries; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +public class BrowseBooksInCatalogEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapGet("{catalogId:guid}/books", Handle) + .WithSummary("Browse books with CatalogId parameter"); + } + + private static async Task>, NotFound>> Handle( + [FromRoute] Guid catalogId, + [FromServices] IQueryDispatcher queryDispatcher, + CancellationToken ct + ) + { + var query = new BrowseBooksInCatalogQuery(catalogId); + var result = await queryDispatcher.QueryAsync(query, ct); + + return result is null + ? TypedResults.NotFound() + : TypedResults.Ok(result); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsByBookStoreIdEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsByBookStoreIdEndpoint.cs deleted file mode 100644 index 85c044c..0000000 --- a/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsByBookStoreIdEndpoint.cs +++ /dev/null @@ -1,32 +0,0 @@ -using DotNetBoilerplate.Application.Catalogs.Browse; -using DotNetBoilerplate.Application.Catalogs.DTO; -using DotNetBoilerplate.Application.Catalogs.Get; -using DotNetBoilerplate.Shared.Abstractions.Queries; -using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.AspNetCore.Mvc; - -namespace DotNetBoilerplate.Api.Catalogs; - -public class BrowseCatalogsByBookStoreIdEndpoint : IEndpoint -{ - public static void Map(IEndpointRouteBuilder app) - { - app.MapGet("bookstore/{id:guid}", Handle) - .WithSummary("Browse all Catalogs in Book Store"); - } - - private static async Task>, NotFound>> Handle( - [FromRoute] Guid id, - [FromServices] IQueryDispatcher queryDispatcher, - CancellationToken ct - ) - { - var query = new BrowseCatalogsByBookStoreIdQuery(id); - - var result = await queryDispatcher.QueryAsync(query, ct); - - if (result is null) return TypedResults.NotFound(); - - return TypedResults.Ok(result); - } -} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsEndpoint.cs index 016b69a..386705c 100644 --- a/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsEndpoint.cs +++ b/src/DotNetBoilerplate.Api/Catalogs/BrowseCatalogsEndpoint.cs @@ -11,18 +11,20 @@ public class BrowseCatalogsEndpoint : IEndpoint public static void Map(IEndpointRouteBuilder app) { app.MapGet("", Handle) - .WithSummary("Browse all catalogs"); + .WithSummary("Browse catalogs with optional BookStoreId parameter"); } - private static async Task>> Handle( + private static async Task>, NotFound>> Handle( + [FromQuery] Guid? bookStoreId, [FromServices] IQueryDispatcher queryDispatcher, CancellationToken ct ) { - var query = new BrowseCatalogsQuery(); - + var query = new BrowseCatalogsQuery(bookStoreId); var result = await queryDispatcher.QueryAsync(query, ct); - return TypedResults.Ok(result); + return result is null + ? TypedResults.NotFound() + : TypedResults.Ok(result); } } \ No newline at end of file diff --git a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs index 2c88068..c616316 100644 --- a/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs +++ b/src/DotNetBoilerplate.Api/Catalogs/CatalogsEndpoints.cs @@ -17,8 +17,10 @@ public static void MapCatalogsEndpoints(this WebApplication app) .MapEndpoint() .MapEndpoint() .MapEndpoint() - .MapEndpoint() - .MapEndpoint(); + .MapEndpoint() + .MapEndpoint() + .MapEndpoint() + .MapEndpoint(); } } diff --git a/src/DotNetBoilerplate.Api/Catalogs/DeleteCatalogEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/DeleteCatalogEndpoint.cs index 09d7cb8..a488cbc 100644 --- a/src/DotNetBoilerplate.Api/Catalogs/DeleteCatalogEndpoint.cs +++ b/src/DotNetBoilerplate.Api/Catalogs/DeleteCatalogEndpoint.cs @@ -14,7 +14,7 @@ public static void Map(IEndpointRouteBuilder app) .WithSummary("Delete catalog by Id"); } - private static async Task> Handle( + private static async Task Handle( Guid id, [FromServices] ICommandDispatcher commandDispatcher, CancellationToken ct @@ -24,7 +24,6 @@ CancellationToken ct await commandDispatcher.DispatchAsync(command, ct); - return TypedResults.Ok(new Response()); + return TypedResults.NoContent(); } - internal sealed record Response(); } \ No newline at end of file diff --git a/src/DotNetBoilerplate.Api/Catalogs/RemoveBookFromCatalogEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/RemoveBookFromCatalogEndpoint.cs new file mode 100644 index 0000000..e021925 --- /dev/null +++ b/src/DotNetBoilerplate.Api/Catalogs/RemoveBookFromCatalogEndpoint.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; +using DotNetBoilerplate.Application.Catalogs.Delete; +using DotNetBoilerplate.Application.Catalogs.RemoveBook; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; + +namespace DotNetBoilerplate.Api.Catalogs; + +internal sealed class RemoveBookFromCatalogEndpoint : IEndpoint +{ + public static void Map(IEndpointRouteBuilder app) + { + app.MapDelete("{catalogId:guid}/books/{id:guid}", Handle) + .RequireAuthorization() + .WithSummary("Remove book from catalog by Id"); + } + + private static async Task Handle( + [FromRoute] Guid catalogId, + [FromBody] Request request, + [FromServices] ICommandDispatcher commandDispatcher, + CancellationToken ct + ) + { + var command = new RemoveBookFromCatalogCommand(catalogId, request.BookId); + + await commandDispatcher.DispatchAsync(command, ct); + + return TypedResults.NoContent(); + } + private sealed class Request + { + [Required] public Guid BookId { get; init; } + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/AddBook/AddBookToCatalogCommand.cs b/src/DotNetBoilerplate.Application/Catalogs/AddBook/AddBookToCatalogCommand.cs new file mode 100644 index 0000000..191b495 --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/AddBook/AddBookToCatalogCommand.cs @@ -0,0 +1,5 @@ +using DotNetBoilerplate.Shared.Abstractions.Commands; + +namespace DotNetBoilerplate.Application.Catalogs.AddBook; + +public sealed record AddBookToCatalogCommand(Guid CatalogId, Guid BookId) : ICommand; \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/AddBook/AddBookToCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/AddBook/AddBookToCatalogHandler.cs new file mode 100644 index 0000000..6d0812d --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/AddBook/AddBookToCatalogHandler.cs @@ -0,0 +1,21 @@ +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Core.Books; +using DotNetBoilerplate.Shared.Abstractions.Commands; + +namespace DotNetBoilerplate.Application.Catalogs.AddBook; + +internal sealed class AddBookToCatalogHandler( + ICatalogRepository catalogRepository, + IBookRepository bookRepository +) : ICommandHandler +{ + public async Task HandleAsync(AddBookToCatalogCommand command) + { + var catalog = await catalogRepository.GetByIdAsync(command.CatalogId); + var book = await bookRepository.GetByIdAsync(command.BookId); + + await catalogRepository.AddBookToCatalogAsync(book, catalog); + + return catalog.Id; + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdHandler.cs deleted file mode 100644 index 5f173df..0000000 --- a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdHandler.cs +++ /dev/null @@ -1,19 +0,0 @@ -using DotNetBoilerplate.Application.Catalogs.DTO; -using DotNetBoilerplate.Core.Catalogs; -using DotNetBoilerplate.Shared.Abstractions.Queries; - -namespace DotNetBoilerplate.Application.Catalogs.Browse; - -internal sealed class BrowseBooksByBookStoreIdHandler( - ICatalogRepository catalogRepository -) : IQueryHandler> -{ - public async Task> HandleAsync(BrowseCatalogsByBookStoreIdQuery query) - { - var books = await catalogRepository.GetAllInBookStore(query.Id); - - return books - .Select(x => new CatalogDto(x.Id, x.Name, x.Genre, x.Description)) - .ToList(); - } -} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdQuery.cs b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdQuery.cs deleted file mode 100644 index fcceaa5..0000000 --- a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsByBookStoreIdQuery.cs +++ /dev/null @@ -1,6 +0,0 @@ -using DotNetBoilerplate.Application.Catalogs.DTO; -using DotNetBoilerplate.Shared.Abstractions.Queries; - -namespace DotNetBoilerplate.Application.Catalogs.Browse; - -public sealed record BrowseCatalogsByBookStoreIdQuery(Guid Id) : IQuery>; \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsHandler.cs index 24b5ccc..646dbbd 100644 --- a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsHandler.cs +++ b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsHandler.cs @@ -10,10 +10,13 @@ ICatalogRepository catalogRepository { public async Task> HandleAsync(BrowseCatalogsQuery query) { - var books = await catalogRepository.GetAll(); - - return books - .Select(x => new CatalogDto(x.Id, x.Name, x.Genre, x.Description)) - .ToList(); + IEnumerable catalogs; + if (query.BookStoreId.HasValue) + { + catalogs = await catalogRepository.GetAllInBookStore(query.BookStoreId.Value); + return catalogs.Select(x => new CatalogDto(x.Id, x.Name, x.Genre, x.Description)); + } + catalogs = await catalogRepository.GetAll(); + return catalogs.Select(x => new CatalogDto(x.Id, x.Name, x.Genre, x.Description)); } } \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsQuery.cs b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsQuery.cs index 5ab338a..d8b3ee1 100644 --- a/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsQuery.cs +++ b/src/DotNetBoilerplate.Application/Catalogs/Browse/BrowseCatalogsQuery.cs @@ -3,4 +3,4 @@ namespace DotNetBoilerplate.Application.Catalogs.Browse; -public sealed record BrowseCatalogsQuery : IQuery>; \ No newline at end of file +public sealed record BrowseCatalogsQuery(Guid? BookStoreId = null) : IQuery>; \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/BrowseBooks/BrowseBooksInCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/BrowseBooks/BrowseBooksInCatalogHandler.cs new file mode 100644 index 0000000..62de677 --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/BrowseBooks/BrowseBooksInCatalogHandler.cs @@ -0,0 +1,17 @@ +using DotNetBoilerplate.Application.Books.DTO; +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Shared.Abstractions.Queries; + +namespace DotNetBoilerplate.Application.Catalogs.BrowseBooks; + +internal sealed class BrowseBooksInCatalogHandler( + ICatalogRepository catalogRepository +) : IQueryHandler> +{ + public async Task> HandleAsync(BrowseBooksInCatalogQuery query) + { + var books = await catalogRepository.GetBooksInCatalogAsync(query.CatalogId); + + return books.Select(x => new BookDto(x.Id, x.Title, x.Writer, x.Genre, x.Year, x.Description)); + } +} \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/BrowseBooks/BrowseBooksInCatalogQuery.cs b/src/DotNetBoilerplate.Application/Catalogs/BrowseBooks/BrowseBooksInCatalogQuery.cs new file mode 100644 index 0000000..9c20eba --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/BrowseBooks/BrowseBooksInCatalogQuery.cs @@ -0,0 +1,6 @@ +using DotNetBoilerplate.Application.Books.DTO; +using DotNetBoilerplate.Shared.Abstractions.Queries; + +namespace DotNetBoilerplate.Application.Catalogs.BrowseBooks; + +public sealed record BrowseBooksInCatalogQuery(Guid CatalogId) : IQuery>; \ No newline at end of file diff --git a/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs index 05cbe9c..535acf4 100644 --- a/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs +++ b/src/DotNetBoilerplate.Application/Catalogs/Create/CreateCatalogHandler.cs @@ -3,10 +3,12 @@ using DotNetBoilerplate.Shared.Abstractions.Commands; using DotNetBoilerplate.Application.Exceptions; using DotNetBoilerplate.Shared.Abstractions.Contexts; +using DotNetBoilerplate.Shared.Abstractions.Time; namespace DotNetBoilerplate.Application.Catalogs.Create; internal sealed class CreateCatalogHandler( + IClock clock, IContext context, ICatalogRepository catalogRepository, IBookStoreRepository bookStoreRepository @@ -19,13 +21,14 @@ public async Task HandleAsync(CreateCatalogCommand command) throw new BookStoreNotFoundException(); bool userCanNotCreateCatalog = await catalogRepository.UserCanNotAddCatalogAsync(bookStore.Id); - + var catalog = Catalog.Create( command.Name, command.Genre, command.Description, bookStore.Id, context.Identity.Id, + clock.Now(), userCanNotCreateCatalog ); await catalogRepository.AddAsync(catalog); diff --git a/src/DotNetBoilerplate.Application/Catalogs/RemoveBook/RemoveBookFromCatalogCommand.cs b/src/DotNetBoilerplate.Application/Catalogs/RemoveBook/RemoveBookFromCatalogCommand.cs new file mode 100644 index 0000000..eb13e3a --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/RemoveBook/RemoveBookFromCatalogCommand.cs @@ -0,0 +1,5 @@ +using DotNetBoilerplate.Shared.Abstractions.Commands; + +namespace DotNetBoilerplate.Application.Catalogs.RemoveBook; + +public sealed record RemoveBookFromCatalogCommand(Guid CatalogId, Guid BookId) : ICommand; diff --git a/src/DotNetBoilerplate.Application/Catalogs/RemoveBook/RemoveBookFromCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/RemoveBook/RemoveBookFromCatalogHandler.cs new file mode 100644 index 0000000..1b70a73 --- /dev/null +++ b/src/DotNetBoilerplate.Application/Catalogs/RemoveBook/RemoveBookFromCatalogHandler.cs @@ -0,0 +1,29 @@ +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Shared.Abstractions.Commands; +using DotNetBoilerplate.Shared.Abstractions.Contexts; +using DotNetBoilerplate.Application.Exceptions; +using DotNetBoilerplate.Core.Books; + +namespace DotNetBoilerplate.Application.Catalogs.RemoveBook; + +internal sealed class RemoveBookFromCatalogHandler( + IContext context, + ICatalogRepository catalogRepository, + IBookRepository bookRepository + ) : ICommandHandler +{ + public async Task HandleAsync(RemoveBookFromCatalogCommand command) + { + var catalog = await catalogRepository.GetByIdAsync(command.CatalogId); + var book = await bookRepository.GetByIdAsync(command.BookId); + + if (catalog is null) + throw new CatalogNotFoundException(); + if (book is null) + throw new BookNotFoundException(); + if (catalog.CreatedBy != context.Identity.Id) + throw new UserCanNotRemoveBookFromCatalogException(); + + await catalogRepository.RemoveBookFromCatalogAsync(book, catalog); + } +} diff --git a/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogHandler.cs index feb36bb..2f274fb 100644 --- a/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogHandler.cs +++ b/src/DotNetBoilerplate.Application/Catalogs/Update/UpdateCatalogHandler.cs @@ -2,10 +2,12 @@ using DotNetBoilerplate.Core.Catalogs; using DotNetBoilerplate.Shared.Abstractions.Commands; using DotNetBoilerplate.Shared.Abstractions.Contexts; +using DotNetBoilerplate.Shared.Abstractions.Time; namespace DotNetBoilerplate.Application.Catalogs.Update; internal sealed class UpdateCatalogHandler( + IClock clock, IContext context, ICatalogRepository catalogRepository ) : ICommandHandler @@ -16,11 +18,15 @@ public async Task HandleAsync(UpdateCatalogCommand command) if (catalog is null) throw new BookNotFoundException(); + bool userCanNotUpdateCatalog = await catalogRepository.UserCanNotUpdateCatalogAsync(catalog.UpdatedAt, clock.Now()); + catalog.Update( command.Name, command.Genre, command.Description, - context.Identity.Id + context.Identity.Id, + clock.Now(), + userCanNotUpdateCatalog ); await catalogRepository.UpdateAsync(catalog); diff --git a/src/DotNetBoilerplate.Application/Exceptions/UserCanNotRemoveBookFromCatalogException.cs b/src/DotNetBoilerplate.Application/Exceptions/UserCanNotRemoveBookFromCatalogException.cs new file mode 100644 index 0000000..489188f --- /dev/null +++ b/src/DotNetBoilerplate.Application/Exceptions/UserCanNotRemoveBookFromCatalogException.cs @@ -0,0 +1,6 @@ +using DotNetBoilerplate.Shared.Abstractions.Exceptions; + +namespace DotNetBoilerplate.Application.Exceptions; + +internal sealed class UserCanNotRemoveBookFromCatalogException() + : CustomException("User can not remove book from that catalog."); \ No newline at end of file diff --git a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs index f0ac7f3..5f10301 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs @@ -45,22 +45,25 @@ public void Update( string genre, string description, Guid updater, - DateTime updatedAt + DateTime updatedAt, + bool userCanNotUpdateCatalog ) { if (updater == Guid.Empty) throw new UserCanNotUpdateCatalogException(); + if (userCanNotUpdateCatalog) + throw new UserCanNotUpdateCatalogMoreThanOnceIn3DaysException(); Name = name; Genre = genre; Description = description; UpdatedAt = updatedAt; } - public void AddBook(Book book) - { - if (Books.Any(x => x.Id == book.Id)) - throw new BookAlreadyAddedToCatalogException(); - Books.Add(book); - } + // public void AddBook(Book book) + // { + // if (Books.Any(x => x.Id == book.Id)) + // throw new BookAlreadyAddedToCatalogException(); + // Books.Add(book); + // } } diff --git a/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotUpdateCatalogMoreThanOnceIn3DaysException.cs b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotUpdateCatalogMoreThanOnceIn3DaysException.cs new file mode 100644 index 0000000..fb7f977 --- /dev/null +++ b/src/DotNetBoilerplate.Core/Catalogs/Exceptions/UserCanNotUpdateCatalogMoreThanOnceIn3DaysException.cs @@ -0,0 +1,8 @@ +using DotNetBoilerplate.Shared.Abstractions.Exceptions; + +namespace DotNetBoilerplate.Core.Catalogs.Exceptions; + +internal sealed class UserCanNotUpdateCatalogMoreThanOnceIn3DaysException() + : CustomException("User can not update catalog more than once in 3 days.") +{ +}; diff --git a/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs index 27ba546..6fee6f0 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/ICatalogRepository.cs @@ -1,4 +1,4 @@ - +using DotNetBoilerplate.Core.Books; namespace DotNetBoilerplate.Core.Catalogs; @@ -10,6 +10,8 @@ public interface ICatalogRepository Task UpdateAsync(Catalog catalog); + Task UserCanNotUpdateCatalogAsync(DateTime lastUpdated, DateTime now); + Task> GetAll(); Task GetByIdAsync(Guid id); @@ -17,4 +19,10 @@ public interface ICatalogRepository Task> GetAllInBookStore(Guid bookStoreId); Task DeleteAsync(Catalog catalog); + + Task AddBookToCatalogAsync(Book book, Catalog catalog); + + Task> GetBooksInCatalogAsync(Guid catalogId); + + Task RemoveBookFromCatalogAsync(Book book, Catalog catalog); } diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs index 21ac34e..a0ff52b 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/InMemoryCatalogRepository.cs @@ -1,4 +1,5 @@ using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Core.Books; namespace DotNetBoilerplate.Infrastructure.DAL.Repositories; @@ -11,13 +12,11 @@ public Task AddAsync(Catalog catalog) _catalogs.Add(catalog); return Task.CompletedTask; } - public Task UserCanNotAddCatalogAsync(Guid bookStoreId) { var count = _catalogs.Count(x => x.BookStoreId == bookStoreId); return Task.FromResult(count >= 5); } - public Task UpdateAsync(Catalog catalog) { var existingCatalogIndex = _catalogs.FindIndex(x => x.Id == catalog.Id); @@ -25,26 +24,43 @@ public Task UpdateAsync(Catalog catalog) return Task.CompletedTask; } - + public Task UserCanNotUpdateCatalogAsync(DateTime lastUpdated, DateTime now) + { + return Task.FromResult((now - lastUpdated).TotalSeconds < 3); + } public Task> GetAll() { return Task.FromResult(_catalogs.AsEnumerable()); } - public Task GetByIdAsync(Guid id) { return Task.FromResult(_catalogs.FirstOrDefault(x => x.Id == id)); } - public Task> GetAllInBookStore(Guid bookStoreId) { return Task.FromResult(_catalogs.FindAll(x => x.BookStoreId == bookStoreId) .AsEnumerable()); } - public Task DeleteAsync(Catalog catalog) { _catalogs.Remove(catalog); return Task.CompletedTask; } + + + public Task AddBookToCatalogAsync(Book book, Catalog catalog) + { + _catalogs.FirstOrDefault(x => x.Id == catalog.Id).Books.Add(book); + return Task.CompletedTask; + } + public Task> GetBooksInCatalogAsync(Guid catalogId) + { + var catalog = _catalogs.FirstOrDefault(x => x.Id == catalogId); + return Task.FromResult(catalog.Books.AsEnumerable()); + } + public Task RemoveBookFromCatalogAsync(Book book, Catalog catalog) + { + _catalogs.FirstOrDefault(x => x.Id == catalog.Id).Books.Remove(book); + return Task.CompletedTask; + } } diff --git a/src/DotNetBoilerplate.Infrastructure/Emails/Extensions.cs b/src/DotNetBoilerplate.Infrastructure/Emails/Extensions.cs index a15eb6c..64f2c0b 100644 --- a/src/DotNetBoilerplate.Infrastructure/Emails/Extensions.cs +++ b/src/DotNetBoilerplate.Infrastructure/Emails/Extensions.cs @@ -21,11 +21,6 @@ public static IServiceCollection AddEmails(this IServiceCollection services, ICo services.AddHttpClient() .AddTransientHttpErrorPolicy(policyBuilder => policyBuilder.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)))) .AddTransientHttpErrorPolicy(policyBuilder => policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30))) - // .ConfigureHttpClient((sp, client) => - // { - // var options = sp.GetRequiredService>().Value; - // // Configure the HttpClient if needed - // }) .AddTypedClient((httpClient, sp) => { var options = sp.GetRequiredService>().Value; From 7d07b3f29a8e0cc9b7720cc121c1105f46804106 Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Mon, 12 Aug 2024 18:08:03 +0200 Subject: [PATCH 15/16] resolved some conflicts --- src/DotNetBoilerplate.Api/Program.cs | 3 --- .../DAL/Repositories/Extensions.cs | 4 ---- 2 files changed, 7 deletions(-) diff --git a/src/DotNetBoilerplate.Api/Program.cs b/src/DotNetBoilerplate.Api/Program.cs index 311d70a..4c06737 100644 --- a/src/DotNetBoilerplate.Api/Program.cs +++ b/src/DotNetBoilerplate.Api/Program.cs @@ -24,10 +24,7 @@ app.MapUsersEndpoints(); app.MapBookStoresEndpoints(); app.MapBookEndpoints(); -<<<<<<< HEAD app.MapReviewEndpoints(); -======= ->>>>>>> 14b5db90cce474f25e32d9df42e17307869a3001 app.MapCatalogsEndpoints(); app.UseInfrastructure(); diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs index f33e9e1..ba1df23 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs @@ -13,12 +13,8 @@ public static IServiceCollection AddRepositories(this IServiceCollection service { services.AddScoped(); services.AddScoped(); -<<<<<<< HEAD services.AddScoped(); services.AddScoped(); -======= - services.AddSingleton(); ->>>>>>> 14b5db90cce474f25e32d9df42e17307869a3001 services.AddSingleton(); return services; From 87bf9118ff3e33586628a91eaaca279c9e764377 Mon Sep 17 00:00:00 2001 From: iAttaquer Date: Tue, 13 Aug 2024 15:33:36 +0200 Subject: [PATCH 16/16] feat: added migration for catalogs --- .../Catalogs/RemoveBookFromCatalogEndpoint.cs | 7 +- .../AddBook/AddBookToCatalogHandler.cs | 5 + .../RemoveBookFromCatalogHandler.cs | 5 +- .../Catalogs/Catalog.cs | 6 +- .../Write/CatalogWriteConfiguration.cs | 44 ++++++++ .../DotNetBoilerplateWriteDbContext.cs | 1 + ....cs => 20240813124313_Initial.Designer.cs} | 99 ++++++++++++++++- ...4_Initial.cs => 20240813124313_Initial.cs} | 100 +++++++++++++++++- ...tBoilerplateWriteDbContextModelSnapshot.cs | 97 +++++++++++++++++ .../DAL/Repositories/Extensions.cs | 3 +- .../Repositories/PostgresCatalogRepository.cs | 72 +++++++++++++ 11 files changed, 428 insertions(+), 11 deletions(-) rename src/DotNetBoilerplate.Infrastructure/DAL/Migrations/{20240731074754_Initial.Designer.cs => 20240813124313_Initial.Designer.cs} (70%) rename src/DotNetBoilerplate.Infrastructure/DAL/Migrations/{20240731074754_Initial.cs => 20240813124313_Initial.cs} (66%) create mode 100644 src/DotNetBoilerplate.Infrastructure/DAL/Repositories/PostgresCatalogRepository.cs diff --git a/src/DotNetBoilerplate.Api/Catalogs/RemoveBookFromCatalogEndpoint.cs b/src/DotNetBoilerplate.Api/Catalogs/RemoveBookFromCatalogEndpoint.cs index e021925..5295385 100644 --- a/src/DotNetBoilerplate.Api/Catalogs/RemoveBookFromCatalogEndpoint.cs +++ b/src/DotNetBoilerplate.Api/Catalogs/RemoveBookFromCatalogEndpoint.cs @@ -11,19 +11,18 @@ internal sealed class RemoveBookFromCatalogEndpoint : IEndpoint { public static void Map(IEndpointRouteBuilder app) { - app.MapDelete("{catalogId:guid}/books/{id:guid}", Handle) + app.MapDelete("{catalogId:guid}/books/{bookId:guid}", Handle) .RequireAuthorization() .WithSummary("Remove book from catalog by Id"); } private static async Task Handle( - [FromRoute] Guid catalogId, - [FromBody] Request request, + Guid catalogId, Guid bookId, [FromServices] ICommandDispatcher commandDispatcher, CancellationToken ct ) { - var command = new RemoveBookFromCatalogCommand(catalogId, request.BookId); + var command = new RemoveBookFromCatalogCommand(catalogId, bookId); await commandDispatcher.DispatchAsync(command, ct); diff --git a/src/DotNetBoilerplate.Application/Catalogs/AddBook/AddBookToCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/AddBook/AddBookToCatalogHandler.cs index 6d0812d..6c1c774 100644 --- a/src/DotNetBoilerplate.Application/Catalogs/AddBook/AddBookToCatalogHandler.cs +++ b/src/DotNetBoilerplate.Application/Catalogs/AddBook/AddBookToCatalogHandler.cs @@ -1,6 +1,7 @@ using DotNetBoilerplate.Core.Catalogs; using DotNetBoilerplate.Core.Books; using DotNetBoilerplate.Shared.Abstractions.Commands; +using DotNetBoilerplate.Application.Exceptions; namespace DotNetBoilerplate.Application.Catalogs.AddBook; @@ -12,7 +13,11 @@ IBookRepository bookRepository public async Task HandleAsync(AddBookToCatalogCommand command) { var catalog = await catalogRepository.GetByIdAsync(command.CatalogId); + if (catalog is null) + throw new CatalogNotFoundException(); var book = await bookRepository.GetByIdAsync(command.BookId); + if (book is null) + throw new BookNotFoundException(); await catalogRepository.AddBookToCatalogAsync(book, catalog); diff --git a/src/DotNetBoilerplate.Application/Catalogs/RemoveBook/RemoveBookFromCatalogHandler.cs b/src/DotNetBoilerplate.Application/Catalogs/RemoveBook/RemoveBookFromCatalogHandler.cs index 1b70a73..da0134f 100644 --- a/src/DotNetBoilerplate.Application/Catalogs/RemoveBook/RemoveBookFromCatalogHandler.cs +++ b/src/DotNetBoilerplate.Application/Catalogs/RemoveBook/RemoveBookFromCatalogHandler.cs @@ -15,12 +15,13 @@ IBookRepository bookRepository public async Task HandleAsync(RemoveBookFromCatalogCommand command) { var catalog = await catalogRepository.GetByIdAsync(command.CatalogId); - var book = await bookRepository.GetByIdAsync(command.BookId); - if (catalog is null) throw new CatalogNotFoundException(); + + var book = await bookRepository.GetByIdAsync(command.BookId); if (book is null) throw new BookNotFoundException(); + if (catalog.CreatedBy != context.Identity.Id) throw new UserCanNotRemoveBookFromCatalogException(); diff --git a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs index f0880be..15c18fa 100644 --- a/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs +++ b/src/DotNetBoilerplate.Core/Catalogs/Catalog.cs @@ -1,5 +1,6 @@ using DotNetBoilerplate.Core.Books; using DotNetBoilerplate.Core.Catalogs.Exceptions; +using DotNetBoilerplate.Core.Users; namespace DotNetBoilerplate.Core.Catalogs; @@ -11,9 +12,9 @@ private Catalog() public string Name { get; private set; } public string Genre { get; private set; } public string Description { get; private set; } - public List Books { get; private set; } + public List Books { get; set; } public Guid BookStoreId { get; private set; } - public Guid CreatedBy { get; private set; } + public UserId CreatedBy { get; private set; } public DateTime UpdatedAt { get; private set; } public static Catalog Create( @@ -35,6 +36,7 @@ bool userCanNotCreateCatalog Name = name, Genre = genre, Description = description, + Books = new List(), BookStoreId = bookStoreId, CreatedBy = createdBy, UpdatedAt = updatedAt diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Configurations/Write/CatalogWriteConfiguration.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Configurations/Write/CatalogWriteConfiguration.cs index 21b82d0..4230e9e 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Configurations/Write/CatalogWriteConfiguration.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Configurations/Write/CatalogWriteConfiguration.cs @@ -1,4 +1,7 @@ +using DotNetBoilerplate.Core.Books; +using DotNetBoilerplate.Core.BookStores; using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Core.Users; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -10,5 +13,46 @@ public void Configure(EntityTypeBuilder builder) { builder.HasKey(x => x.Id); + builder.Property(x => x.Name) + .IsRequired() + .HasMaxLength(30); + + builder.Property(x => x.Genre) + .IsRequired() + .HasMaxLength(20); + + builder.Property(x => x.Description) + .HasMaxLength(1000); + + // builder.Property(x => x.Books) + // .HasConversion() + // .IsRequired(); + + builder.Property(x => x.BookStoreId) + .IsRequired(); + + builder.Property(x => x.CreatedBy) + .HasConversion(x => x.Value, x => new UserId(x)) + .IsRequired(); + + builder.Property(x => x.UpdatedAt) + .IsRequired(); + + builder + .HasOne() + .WithMany() + .HasForeignKey(x => x.BookStoreId) + .IsRequired(); + + builder + .HasOne() + .WithMany() + .HasForeignKey(x => x.CreatedBy) + .IsRequired(); + + builder + .HasMany() + .WithMany() + .UsingEntity(j => j.ToTable("CatalogBooks")); } } \ No newline at end of file diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Contexts/DotNetBoilerplateWriteDbContext.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Contexts/DotNetBoilerplateWriteDbContext.cs index a9382be..164670e 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Contexts/DotNetBoilerplateWriteDbContext.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Contexts/DotNetBoilerplateWriteDbContext.cs @@ -23,6 +23,7 @@ internal sealed class DotNetBoilerplateWriteDbContext(DbContextOptions Catalogs { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema("dotNetBoilerplate"); diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Migrations/20240731074754_Initial.Designer.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Migrations/20240813124313_Initial.Designer.cs similarity index 70% rename from src/DotNetBoilerplate.Infrastructure/DAL/Migrations/20240731074754_Initial.Designer.cs rename to src/DotNetBoilerplate.Infrastructure/DAL/Migrations/20240813124313_Initial.Designer.cs index 85e0f52..7d97a75 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Migrations/20240731074754_Initial.Designer.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Migrations/20240813124313_Initial.Designer.cs @@ -12,7 +12,7 @@ namespace DotNetBoilerplate.Infrastructure.DAL.Migrations { [DbContext(typeof(DotNetBoilerplateWriteDbContext))] - [Migration("20240731074754_Initial")] + [Migration("20240813124313_Initial")] partial class Initial { /// @@ -26,6 +26,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("BookCatalog", b => + { + b.Property("BookId") + .HasColumnType("uuid"); + + b.Property("CatalogId") + .HasColumnType("uuid"); + + b.HasKey("BookId", "CatalogId"); + + b.HasIndex("CatalogId"); + + b.ToTable("CatalogBooks", "dotNetBoilerplate"); + }); + modelBuilder.Entity("DotNetBoilerplate.Core.BookStores.BookStore", b => { b.Property("Id") @@ -64,6 +79,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("BookStoreId") .HasColumnType("uuid"); + b.Property("CatalogId") + .HasColumnType("uuid"); + b.Property("CreatedBy") .HasColumnType("uuid"); @@ -93,11 +111,51 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("BookStoreId"); + b.HasIndex("CatalogId"); + b.HasIndex("CreatedBy"); b.ToTable("Books", "dotNetBoilerplate"); }); + modelBuilder.Entity("DotNetBoilerplate.Core.Catalogs.Catalog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BookStoreId") + .HasColumnType("uuid"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Genre") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("BookStoreId"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Catalogs", "dotNetBoilerplate"); + }); + modelBuilder.Entity("DotNetBoilerplate.Core.Reviews.Review", b => { b.Property("Id") @@ -196,6 +254,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("OutboxMessages", "dotNetBoilerplate"); }); + modelBuilder.Entity("BookCatalog", b => + { + b.HasOne("DotNetBoilerplate.Core.Books.Book", null) + .WithMany() + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DotNetBoilerplate.Core.Catalogs.Catalog", null) + .WithMany() + .HasForeignKey("CatalogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("DotNetBoilerplate.Core.BookStores.BookStore", b => { b.HasOne("DotNetBoilerplate.Core.Users.User", null) @@ -204,6 +277,25 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) }); modelBuilder.Entity("DotNetBoilerplate.Core.Books.Book", b => + { + b.HasOne("DotNetBoilerplate.Core.BookStores.BookStore", null) + .WithMany() + .HasForeignKey("BookStoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DotNetBoilerplate.Core.Catalogs.Catalog", null) + .WithMany("Books") + .HasForeignKey("CatalogId"); + + b.HasOne("DotNetBoilerplate.Core.Users.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DotNetBoilerplate.Core.Catalogs.Catalog", b => { b.HasOne("DotNetBoilerplate.Core.BookStores.BookStore", null) .WithMany() @@ -232,6 +324,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); + + modelBuilder.Entity("DotNetBoilerplate.Core.Catalogs.Catalog", b => + { + b.Navigation("Books"); + }); #pragma warning restore 612, 618 } } diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Migrations/20240731074754_Initial.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Migrations/20240813124313_Initial.cs similarity index 66% rename from src/DotNetBoilerplate.Infrastructure/DAL/Migrations/20240731074754_Initial.cs rename to src/DotNetBoilerplate.Infrastructure/DAL/Migrations/20240813124313_Initial.cs index af5bb1d..ae66f98 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Migrations/20240731074754_Initial.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Migrations/20240813124313_Initial.cs @@ -70,6 +70,38 @@ protected override void Up(MigrationBuilder migrationBuilder) principalColumn: "Id"); }); + migrationBuilder.CreateTable( + name: "Catalogs", + schema: "dotNetBoilerplate", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(30)", maxLength: 30, nullable: false), + Genre = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + BookStoreId = table.Column(type: "uuid", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Catalogs", x => x.Id); + table.ForeignKey( + name: "FK_Catalogs_BookStores_BookStoreId", + column: x => x.BookStoreId, + principalSchema: "dotNetBoilerplate", + principalTable: "BookStores", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Catalogs_Users_CreatedBy", + column: x => x.CreatedBy, + principalSchema: "dotNetBoilerplate", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "Books", schema: "dotNetBoilerplate", @@ -82,7 +114,8 @@ protected override void Up(MigrationBuilder migrationBuilder) Year = table.Column(type: "integer", nullable: false), Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), BookStoreId = table.Column(type: "uuid", nullable: false), - CreatedBy = table.Column(type: "uuid", nullable: false) + CreatedBy = table.Column(type: "uuid", nullable: false), + CatalogId = table.Column(type: "uuid", nullable: true) }, constraints: table => { @@ -94,6 +127,12 @@ protected override void Up(MigrationBuilder migrationBuilder) principalTable: "BookStores", principalColumn: "Id", onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Books_Catalogs_CatalogId", + column: x => x.CatalogId, + principalSchema: "dotNetBoilerplate", + principalTable: "Catalogs", + principalColumn: "Id"); table.ForeignKey( name: "FK_Books_Users_CreatedBy", column: x => x.CreatedBy, @@ -103,6 +142,33 @@ protected override void Up(MigrationBuilder migrationBuilder) onDelete: ReferentialAction.Cascade); }); + migrationBuilder.CreateTable( + name: "CatalogBooks", + schema: "dotNetBoilerplate", + columns: table => new + { + BookId = table.Column(type: "uuid", nullable: false), + CatalogId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CatalogBooks", x => new { x.BookId, x.CatalogId }); + table.ForeignKey( + name: "FK_CatalogBooks_Books_BookId", + column: x => x.BookId, + principalSchema: "dotNetBoilerplate", + principalTable: "Books", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_CatalogBooks_Catalogs_CatalogId", + column: x => x.CatalogId, + principalSchema: "dotNetBoilerplate", + principalTable: "Catalogs", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "Reviews", schema: "dotNetBoilerplate", @@ -140,6 +206,12 @@ protected override void Up(MigrationBuilder migrationBuilder) table: "Books", column: "BookStoreId"); + migrationBuilder.CreateIndex( + name: "IX_Books_CatalogId", + schema: "dotNetBoilerplate", + table: "Books", + column: "CatalogId"); + migrationBuilder.CreateIndex( name: "IX_Books_CreatedBy", schema: "dotNetBoilerplate", @@ -153,6 +225,24 @@ protected override void Up(MigrationBuilder migrationBuilder) column: "OwnerId", unique: true); + migrationBuilder.CreateIndex( + name: "IX_CatalogBooks_CatalogId", + schema: "dotNetBoilerplate", + table: "CatalogBooks", + column: "CatalogId"); + + migrationBuilder.CreateIndex( + name: "IX_Catalogs_BookStoreId", + schema: "dotNetBoilerplate", + table: "Catalogs", + column: "BookStoreId"); + + migrationBuilder.CreateIndex( + name: "IX_Catalogs_CreatedBy", + schema: "dotNetBoilerplate", + table: "Catalogs", + column: "CreatedBy"); + migrationBuilder.CreateIndex( name: "IX_Reviews_BookId_CreatedBy", schema: "dotNetBoilerplate", @@ -184,6 +274,10 @@ protected override void Up(MigrationBuilder migrationBuilder) /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropTable( + name: "CatalogBooks", + schema: "dotNetBoilerplate"); + migrationBuilder.DropTable( name: "OutboxMessages", schema: "dotNetBoilerplate"); @@ -196,6 +290,10 @@ protected override void Down(MigrationBuilder migrationBuilder) name: "Books", schema: "dotNetBoilerplate"); + migrationBuilder.DropTable( + name: "Catalogs", + schema: "dotNetBoilerplate"); + migrationBuilder.DropTable( name: "BookStores", schema: "dotNetBoilerplate"); diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Migrations/DotNetBoilerplateWriteDbContextModelSnapshot.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Migrations/DotNetBoilerplateWriteDbContextModelSnapshot.cs index 3066c83..089ab94 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Migrations/DotNetBoilerplateWriteDbContextModelSnapshot.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Migrations/DotNetBoilerplateWriteDbContextModelSnapshot.cs @@ -23,6 +23,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("BookCatalog", b => + { + b.Property("BookId") + .HasColumnType("uuid"); + + b.Property("CatalogId") + .HasColumnType("uuid"); + + b.HasKey("BookId", "CatalogId"); + + b.HasIndex("CatalogId"); + + b.ToTable("CatalogBooks", "dotNetBoilerplate"); + }); + modelBuilder.Entity("DotNetBoilerplate.Core.BookStores.BookStore", b => { b.Property("Id") @@ -61,6 +76,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("BookStoreId") .HasColumnType("uuid"); + b.Property("CatalogId") + .HasColumnType("uuid"); + b.Property("CreatedBy") .HasColumnType("uuid"); @@ -90,11 +108,51 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("BookStoreId"); + b.HasIndex("CatalogId"); + b.HasIndex("CreatedBy"); b.ToTable("Books", "dotNetBoilerplate"); }); + modelBuilder.Entity("DotNetBoilerplate.Core.Catalogs.Catalog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BookStoreId") + .HasColumnType("uuid"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Genre") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("BookStoreId"); + + b.HasIndex("CreatedBy"); + + b.ToTable("Catalogs", "dotNetBoilerplate"); + }); + modelBuilder.Entity("DotNetBoilerplate.Core.Reviews.Review", b => { b.Property("Id") @@ -193,6 +251,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("OutboxMessages", "dotNetBoilerplate"); }); + modelBuilder.Entity("BookCatalog", b => + { + b.HasOne("DotNetBoilerplate.Core.Books.Book", null) + .WithMany() + .HasForeignKey("BookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DotNetBoilerplate.Core.Catalogs.Catalog", null) + .WithMany() + .HasForeignKey("CatalogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + modelBuilder.Entity("DotNetBoilerplate.Core.BookStores.BookStore", b => { b.HasOne("DotNetBoilerplate.Core.Users.User", null) @@ -201,6 +274,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) }); modelBuilder.Entity("DotNetBoilerplate.Core.Books.Book", b => + { + b.HasOne("DotNetBoilerplate.Core.BookStores.BookStore", null) + .WithMany() + .HasForeignKey("BookStoreId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DotNetBoilerplate.Core.Catalogs.Catalog", null) + .WithMany("Books") + .HasForeignKey("CatalogId"); + + b.HasOne("DotNetBoilerplate.Core.Users.User", null) + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DotNetBoilerplate.Core.Catalogs.Catalog", b => { b.HasOne("DotNetBoilerplate.Core.BookStores.BookStore", null) .WithMany() @@ -229,6 +321,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); }); + + modelBuilder.Entity("DotNetBoilerplate.Core.Catalogs.Catalog", b => + { + b.Navigation("Books"); + }); #pragma warning restore 612, 618 } } diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs index ba1df23..576a21e 100644 --- a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/Extensions.cs @@ -15,7 +15,8 @@ public static IServiceCollection AddRepositories(this IServiceCollection service services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddSingleton(); + // services.AddSingleton(); + services.AddScoped(); return services; } diff --git a/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/PostgresCatalogRepository.cs b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/PostgresCatalogRepository.cs new file mode 100644 index 0000000..f9b3200 --- /dev/null +++ b/src/DotNetBoilerplate.Infrastructure/DAL/Repositories/PostgresCatalogRepository.cs @@ -0,0 +1,72 @@ + +using DotNetBoilerplate.Core.Books; +using DotNetBoilerplate.Core.Catalogs; +using DotNetBoilerplate.Infrastructure.DAL.Contexts; +using Microsoft.EntityFrameworkCore; + +namespace DotNetBoilerplate.Infrastructure.DAL.Repositories; + +internal sealed class PostgresCatalogRepository(DotNetBoilerplateWriteDbContext dbContext) : ICatalogRepository +{ + public async Task AddAsync(Catalog catalog) + { + await dbContext.Catalogs.AddAsync(catalog); + } + public async Task UserCanNotAddCatalogAsync(Guid bookStoreId) + { + return await dbContext.Catalogs.CountAsync(x => x.BookStoreId == bookStoreId) >= 5; + } + public async Task UpdateAsync(Catalog catalog) + { + dbContext.Catalogs.Update(catalog); + await dbContext.SaveChangesAsync(); + } + public Task UserCanNotUpdateCatalogAsync(DateTime lastUpdated, DateTime now) + { + return Task.FromResult((now - lastUpdated).TotalSeconds < 3); + } + public async Task> GetAll() + { + return await dbContext.Catalogs.ToListAsync(); + } + public async Task GetByIdAsync(Guid id) + { + return await dbContext.Catalogs.FirstOrDefaultAsync(x => x.Id == id); + } + public async Task> GetAllInBookStore(Guid bookStoreId) + { + return await dbContext.Catalogs.Where(x => x.BookStoreId == bookStoreId).ToListAsync(); + } + public async Task DeleteAsync(Catalog catalog) + { + dbContext.Catalogs.Remove(catalog); + await dbContext.SaveChangesAsync(); + } + + + public async Task AddBookToCatalogAsync(Book book, Catalog catalog) + { + var existingCatalog = await dbContext.Catalogs.Include(c => c.Books).FirstOrDefaultAsync(x => x.Id == catalog.Id); + // if (existingCatalog.Books == null) + // { + // existingCatalog.Books = new List(); + // } + existingCatalog.Books.Add(book); + await dbContext.SaveChangesAsync(); + } + public async Task> GetBooksInCatalogAsync(Guid catalogId) + { + var catalog = await dbContext.Catalogs + .Include(c => c.Books) + .FirstOrDefaultAsync(x => x.Id == catalogId); + return catalog.Books.ToList(); + } + public async Task RemoveBookFromCatalogAsync(Book book, Catalog catalog) + { + var existingCatalog = await dbContext.Catalogs + .Include(c => c.Books) + .FirstOrDefaultAsync(x => x.Id == catalog.Id); + existingCatalog.Books.Remove(book); + await dbContext.SaveChangesAsync(); + } +} \ No newline at end of file