From 1bcbcf110cfcf0422e180f38b15ece2c23e72d24 Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Fri, 15 Nov 2024 15:54:59 +0100 Subject: [PATCH 01/14] Add Brand domain model with events and exception handling Introduced the `Brand` class in the `FSH.Starter.WebApi.Catalog.Domain` namespace, inheriting from `AuditableEntity` and implementing `IAggregateRoot`. Added properties for `Name` and `Description`, and methods for creating and updating brand instances. Queued domain events `BrandCreated` and `BrandUpdated` during these operations. Added `BrandCreated` and `BrandUpdated` classes in the `FSH.Starter.WebApi.Catalog.Domain.Events` namespace, both inheriting from `DomainEvent` and including a property for the `Brand` instance. Added `BrandNotFoundException` class in the `FSH.Starter.WebApi.Catalog.Domain.Exceptions` namespace, inheriting from `NotFoundException` to handle cases where a brand with a specified ID is not found. --- .../modules/Catalog/Catalog.Domain/Brand.cs | 48 +++++++++++++++++++ .../Catalog.Domain/Events/BrandCreated.cs | 7 +++ .../Catalog.Domain/Events/BrandUpdated.cs | 7 +++ .../Exceptions/BrandNotFoundException.cs | 10 ++++ 4 files changed, 72 insertions(+) create mode 100644 src/api/modules/Catalog/Catalog.Domain/Brand.cs create mode 100644 src/api/modules/Catalog/Catalog.Domain/Events/BrandCreated.cs create mode 100644 src/api/modules/Catalog/Catalog.Domain/Events/BrandUpdated.cs create mode 100644 src/api/modules/Catalog/Catalog.Domain/Exceptions/BrandNotFoundException.cs diff --git a/src/api/modules/Catalog/Catalog.Domain/Brand.cs b/src/api/modules/Catalog/Catalog.Domain/Brand.cs new file mode 100644 index 000000000..ce0b9997a --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Domain/Brand.cs @@ -0,0 +1,48 @@ +using FSH.Framework.Core.Domain; +using FSH.Framework.Core.Domain.Contracts; +using FSH.Starter.WebApi.Catalog.Domain.Events; + +namespace FSH.Starter.WebApi.Catalog.Domain; +public class Brand : AuditableEntity, IAggregateRoot +{ + public string Name { get; private set; } = default!; + public string? Description { get; private set; } + + public static Brand Create(string name, string? description) + { + var brand = new Brand + { + Name = name, + Description = description + }; + + brand.QueueDomainEvent(new BrandCreated() { Brand = brand }); + + return brand; + } + + public Brand Update(string? name, string? description) + { + if (name is not null && Name?.Equals(name, StringComparison.OrdinalIgnoreCase) is not true) Name = name; + if (description is not null && Description?.Equals(description, StringComparison.OrdinalIgnoreCase) is not true) Description = description; + + this.QueueDomainEvent(new BrandUpdated() { Brand = this }); + + return this; + } + + public static Brand Update(Guid id, string name, string? description) + { + var brand = new Brand + { + Id = id, + Name = name, + Description = description + }; + + brand.QueueDomainEvent(new BrandUpdated() { Brand = brand }); + + return brand; + } +} + diff --git a/src/api/modules/Catalog/Catalog.Domain/Events/BrandCreated.cs b/src/api/modules/Catalog/Catalog.Domain/Events/BrandCreated.cs new file mode 100644 index 000000000..a1ae4abeb --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Domain/Events/BrandCreated.cs @@ -0,0 +1,7 @@ +using FSH.Framework.Core.Domain.Events; + +namespace FSH.Starter.WebApi.Catalog.Domain.Events; +public sealed record BrandCreated : DomainEvent +{ + public Brand? Brand { get; set; } +} diff --git a/src/api/modules/Catalog/Catalog.Domain/Events/BrandUpdated.cs b/src/api/modules/Catalog/Catalog.Domain/Events/BrandUpdated.cs new file mode 100644 index 000000000..4446dcf07 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Domain/Events/BrandUpdated.cs @@ -0,0 +1,7 @@ +using FSH.Framework.Core.Domain.Events; + +namespace FSH.Starter.WebApi.Catalog.Domain.Events; +public sealed record BrandUpdated : DomainEvent +{ + public Brand? Brand { get; set; } +} diff --git a/src/api/modules/Catalog/Catalog.Domain/Exceptions/BrandNotFoundException.cs b/src/api/modules/Catalog/Catalog.Domain/Exceptions/BrandNotFoundException.cs new file mode 100644 index 000000000..84a40a1b8 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Domain/Exceptions/BrandNotFoundException.cs @@ -0,0 +1,10 @@ +using FSH.Framework.Core.Exceptions; + +namespace FSH.Starter.WebApi.Catalog.Domain.Exceptions; +public sealed class BrandNotFoundException : NotFoundException +{ + public BrandNotFoundException(Guid id) + : base($"brand with id {id} not found") + { + } +} From 1c818bb98f9bb03a00af70ae04a0a7ee0484fba7 Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Sat, 16 Nov 2024 10:25:03 +0100 Subject: [PATCH 02/14] Add brand management commands and handlers Added commands and handlers for brand management: - CreateBrandCommand, CreateBrandCommandValidator, CreateBrandHandler, and CreateBrandResponse for creating brands. - DeleteBrandCommand and DeleteBrandHandler for deleting brands. - BrandCreatedEventHandler for handling brand creation events. - BrandResponse and GetBrandHandler for retrieving brand details. - GetBrandRequest for brand retrieval requests. - SearchBrandSpecs, SearchBrandsCommand, and SearchBrandsHandler for searching brands with pagination. - UpdateBrandCommand, UpdateBrandCommandValidator, UpdateBrandHandler, and UpdateBrandResponse for updating brand details. --- .../Brands/Create/v1/CreateBrandCommand.cs | 8 ++++++ .../Create/v1/CreateBrandCommandValidator.cs | 11 ++++++++ .../Brands/Create/v1/CreateBrandHandler.cs | 21 ++++++++++++++ .../Brands/Create/v1/CreateBrandResponse.cs | 4 +++ .../Brands/Delete/v1/DeleteBrandCommand.cs | 5 ++++ .../Brands/Delete/v1/DeleteBrandHandler.cs | 22 +++++++++++++++ .../EventHandlers/BrandCreatedEventHandler.cs | 16 +++++++++++ .../Brands/Get/v1/BrandResponse.cs | 2 ++ .../Brands/Get/v1/GetBrandHandler.cs | 28 +++++++++++++++++++ .../Brands/Get/v1/GetBrandRequest.cs | 8 ++++++ .../Brands/Search/v1/SearchBrandSpecs.cs | 15 ++++++++++ .../Brands/Search/v1/SearchBrandsCommand.cs | 11 ++++++++ .../Brands/Search/v1/SearchBrandsHandler.cs | 24 ++++++++++++++++ .../Brands/Update/v1/UpdateBrandCommand.cs | 7 +++++ .../Update/v1/UpdateBrandCommandValidator.cs | 11 ++++++++ .../Brands/Update/v1/UpdateBrandHandler.cs | 24 ++++++++++++++++ .../Brands/Update/v1/UpdateBrandResponse.cs | 2 ++ 17 files changed, 219 insertions(+) create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandCommand.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandCommandValidator.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandHandler.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandResponse.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Delete/v1/DeleteBrandCommand.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Delete/v1/DeleteBrandHandler.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/EventHandlers/BrandCreatedEventHandler.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Get/v1/BrandResponse.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Get/v1/GetBrandHandler.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Get/v1/GetBrandRequest.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Search/v1/SearchBrandSpecs.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Search/v1/SearchBrandsCommand.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Search/v1/SearchBrandsHandler.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandCommand.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandCommandValidator.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandHandler.cs create mode 100644 src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandResponse.cs diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandCommand.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandCommand.cs new file mode 100644 index 000000000..0c2559f9a --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandCommand.cs @@ -0,0 +1,8 @@ +using System.ComponentModel; +using MediatR; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Create.v1; +public sealed record CreateBrandCommand( + [property: DefaultValue("Sample Brand")] string? Name, + [property: DefaultValue("Descriptive Description")] string? Description = null) : IRequest; + diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandCommandValidator.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandCommandValidator.cs new file mode 100644 index 000000000..0d2e69533 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandCommandValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Create.v1; +public class CreateBrandCommandValidator : AbstractValidator +{ + public CreateBrandCommandValidator() + { + RuleFor(b => b.Name).NotEmpty().MinimumLength(2).MaximumLength(100); + RuleFor(b => b.Description).MaximumLength(1000); + } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandHandler.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandHandler.cs new file mode 100644 index 000000000..15b4b7633 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandHandler.cs @@ -0,0 +1,21 @@ +using FSH.Framework.Core.Persistence; +using FSH.Starter.WebApi.Catalog.Domain; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Create.v1; +public sealed class CreateBrandHandler( + ILogger logger, + [FromKeyedServices("catalog:brands")] IRepository repository) + : IRequestHandler +{ + public async Task Handle(CreateBrandCommand request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + var brand = Brand.Create(request.Name!, request.Description); + await repository.AddAsync(brand, cancellationToken); + logger.LogInformation("brand created {BrandId}", brand.Id); + return new CreateBrandResponse(brand.Id); + } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandResponse.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandResponse.cs new file mode 100644 index 000000000..11e63834b --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Create/v1/CreateBrandResponse.cs @@ -0,0 +1,4 @@ +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Create.v1; + +public sealed record CreateBrandResponse(Guid? Id); + diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Delete/v1/DeleteBrandCommand.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Delete/v1/DeleteBrandCommand.cs new file mode 100644 index 000000000..0e11b2414 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Delete/v1/DeleteBrandCommand.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Delete.v1; +public sealed record DeleteBrandCommand( + Guid Id) : IRequest; diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Delete/v1/DeleteBrandHandler.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Delete/v1/DeleteBrandHandler.cs new file mode 100644 index 000000000..d4afe86ef --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Delete/v1/DeleteBrandHandler.cs @@ -0,0 +1,22 @@ +using FSH.Framework.Core.Persistence; +using FSH.Starter.WebApi.Catalog.Domain; +using FSH.Starter.WebApi.Catalog.Domain.Exceptions; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Delete.v1; +public sealed class DeleteBrandHandler( + ILogger logger, + [FromKeyedServices("catalog:brands")] IRepository repository) + : IRequestHandler +{ + public async Task Handle(DeleteBrandCommand request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + var brand = await repository.GetByIdAsync(request.Id, cancellationToken); + _ = brand ?? throw new BrandNotFoundException(request.Id); + await repository.DeleteAsync(brand, cancellationToken); + logger.LogInformation("Brand with id : {BrandId} deleted", brand.Id); + } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/EventHandlers/BrandCreatedEventHandler.cs b/src/api/modules/Catalog/Catalog.Application/Brands/EventHandlers/BrandCreatedEventHandler.cs new file mode 100644 index 000000000..777526b76 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/EventHandlers/BrandCreatedEventHandler.cs @@ -0,0 +1,16 @@ +using FSH.Starter.WebApi.Catalog.Domain.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.EventHandlers; + +public class BrandCreatedEventHandler(ILogger logger) : INotificationHandler +{ + public async Task Handle(BrandCreated notification, + CancellationToken cancellationToken) + { + logger.LogInformation("handling brand created domain event.."); + await Task.FromResult(notification); + logger.LogInformation("finished handling brand created domain event.."); + } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Get/v1/BrandResponse.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Get/v1/BrandResponse.cs new file mode 100644 index 000000000..726030b24 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Get/v1/BrandResponse.cs @@ -0,0 +1,2 @@ +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Get.v1; +public sealed record BrandResponse(Guid? Id, string Name, string? Description); diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Get/v1/GetBrandHandler.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Get/v1/GetBrandHandler.cs new file mode 100644 index 000000000..7848a10d6 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Get/v1/GetBrandHandler.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.DependencyInjection; +using FSH.Starter.WebApi.Catalog.Domain.Exceptions; +using FSH.Framework.Core.Persistence; +using FSH.Framework.Core.Caching; +using FSH.Starter.WebApi.Catalog.Domain; +using MediatR; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Get.v1; +public sealed class GetBrandHandler( + [FromKeyedServices("catalog:brands")] IReadRepository repository, + ICacheService cache) + : IRequestHandler +{ + public async Task Handle(GetBrandRequest request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + var item = await cache.GetOrSetAsync( + $"brand:{request.Id}", + async () => + { + var brandItem = await repository.GetByIdAsync(request.Id, cancellationToken); + if (brandItem == null) throw new BrandNotFoundException(request.Id); + return new BrandResponse(brandItem.Id, brandItem.Name, brandItem.Description); + }, + cancellationToken: cancellationToken); + return item!; + } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Get/v1/GetBrandRequest.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Get/v1/GetBrandRequest.cs new file mode 100644 index 000000000..a9354be5a --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Get/v1/GetBrandRequest.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Get.v1; +public class GetBrandRequest : IRequest +{ + public Guid Id { get; set; } + public GetBrandRequest(Guid id) => Id = id; +} diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Search/v1/SearchBrandSpecs.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Search/v1/SearchBrandSpecs.cs new file mode 100644 index 000000000..b18cadc7f --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Search/v1/SearchBrandSpecs.cs @@ -0,0 +1,15 @@ +using Ardalis.Specification; +using FSH.Framework.Core.Paging; +using FSH.Framework.Core.Specifications; +using FSH.Starter.WebApi.Catalog.Application.Brands.Get.v1; +using FSH.Starter.WebApi.Catalog.Domain; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Search.v1; +public class SearchBrandSpecs : EntitiesByPaginationFilterSpec +{ + public SearchBrandSpecs(SearchBrandsCommand command) + : base(command) => + Query + .OrderBy(c => c.Name, !command.HasOrderBy()) + .Where(b => b.Name.Contains(command.Keyword), !string.IsNullOrEmpty(command.Keyword)); +} diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Search/v1/SearchBrandsCommand.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Search/v1/SearchBrandsCommand.cs new file mode 100644 index 000000000..70f4b3e0a --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Search/v1/SearchBrandsCommand.cs @@ -0,0 +1,11 @@ +using FSH.Framework.Core.Paging; +using FSH.Starter.WebApi.Catalog.Application.Brands.Get.v1; +using MediatR; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Search.v1; + +public class SearchBrandsCommand : PaginationFilter, IRequest> +{ + public string? Name { get; set; } + public string? Description { get; set; } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Search/v1/SearchBrandsHandler.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Search/v1/SearchBrandsHandler.cs new file mode 100644 index 000000000..29b7107f4 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Search/v1/SearchBrandsHandler.cs @@ -0,0 +1,24 @@ +using FSH.Framework.Core.Paging; +using FSH.Framework.Core.Persistence; +using FSH.Starter.WebApi.Catalog.Application.Brands.Get.v1; +using FSH.Starter.WebApi.Catalog.Domain; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Search.v1; +public sealed class SearchBrandsHandler( + [FromKeyedServices("catalog:brands")] IReadRepository repository) + : IRequestHandler> +{ + public async Task> Handle(SearchBrandsCommand request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + var spec = new SearchBrandSpecs(request); + + var items = await repository.ListAsync(spec, cancellationToken).ConfigureAwait(false); + var totalCount = await repository.CountAsync(spec, cancellationToken).ConfigureAwait(false); + + return new PagedList(items, request!.PageNumber, request!.PageSize, totalCount); + } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandCommand.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandCommand.cs new file mode 100644 index 000000000..ce7dd54cf --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandCommand.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Update.v1; +public sealed record UpdateBrandCommand( + Guid Id, + string? Name, + string? Description = null) : IRequest; diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandCommandValidator.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandCommandValidator.cs new file mode 100644 index 000000000..a3ce8da6c --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandCommandValidator.cs @@ -0,0 +1,11 @@ +using FluentValidation; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Update.v1; +public class UpdateBrandCommandValidator : AbstractValidator +{ + public UpdateBrandCommandValidator() + { + RuleFor(b => b.Name).NotEmpty().MinimumLength(2).MaximumLength(100); + RuleFor(b => b.Description).MaximumLength(1000); + } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandHandler.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandHandler.cs new file mode 100644 index 000000000..2477fdb4a --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandHandler.cs @@ -0,0 +1,24 @@ +using FSH.Framework.Core.Persistence; +using FSH.Starter.WebApi.Catalog.Domain; +using FSH.Starter.WebApi.Catalog.Domain.Exceptions; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Update.v1; +public sealed class UpdateBrandHandler( + ILogger logger, + [FromKeyedServices("catalog:brands")] IRepository repository) + : IRequestHandler +{ + public async Task Handle(UpdateBrandCommand request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + var brand = await repository.GetByIdAsync(request.Id, cancellationToken); + _ = brand ?? throw new BrandNotFoundException(request.Id); + var updatedBrand = brand.Update(request.Name, request.Description); + await repository.UpdateAsync(updatedBrand, cancellationToken); + logger.LogInformation("Brand with id : {BrandId} updated.", brand.Id); + return new UpdateBrandResponse(brand.Id); + } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandResponse.cs b/src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandResponse.cs new file mode 100644 index 000000000..6b4acdc87 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Brands/Update/v1/UpdateBrandResponse.cs @@ -0,0 +1,2 @@ +namespace FSH.Starter.WebApi.Catalog.Application.Brands.Update.v1; +public sealed record UpdateBrandResponse(Guid? Id); From 3e6645b9b619f694de30e99a384e3beb4045e501 Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Sat, 16 Nov 2024 10:26:09 +0100 Subject: [PATCH 03/14] Add brand-related endpoints in API version 1 Introduce new endpoints for brand operations in the FSH.Starter.WebApi.Catalog.Infrastructure.Endpoints.v1 namespace. Endpoints include CreateBrand, DeleteBrand, GetBrand, SearchBrands, and UpdateBrand, each secured with specific permissions and mapped to appropriate routes. Handlers use MediatR for request handling and response production. --- .../Endpoints/v1/CreateBrandEndpoint.cs | 26 ++++++++++++++++ .../Endpoints/v1/DeleteBrandEndpoint.cs | 26 ++++++++++++++++ .../Endpoints/v1/GetBrandEndpoint.cs | 26 ++++++++++++++++ .../Endpoints/v1/SearchBrandsEndpoint.cs | 30 +++++++++++++++++++ .../Endpoints/v1/UpdateBrandEndpoint.cs | 27 +++++++++++++++++ 5 files changed, 135 insertions(+) create mode 100644 src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/CreateBrandEndpoint.cs create mode 100644 src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/DeleteBrandEndpoint.cs create mode 100644 src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/GetBrandEndpoint.cs create mode 100644 src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/SearchBrandsEndpoint.cs create mode 100644 src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/UpdateBrandEndpoint.cs diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/CreateBrandEndpoint.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/CreateBrandEndpoint.cs new file mode 100644 index 000000000..b7adb6b9b --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/CreateBrandEndpoint.cs @@ -0,0 +1,26 @@ +using FSH.Framework.Infrastructure.Auth.Policy; +using FSH.Starter.WebApi.Catalog.Application.Brands.Create.v1; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace FSH.Starter.WebApi.Catalog.Infrastructure.Endpoints.v1; +public static class CreateBrandEndpoint +{ + internal static RouteHandlerBuilder MapBrandCreationEndpoint(this IEndpointRouteBuilder endpoints) + { + return endpoints + .MapPost("/", async (CreateBrandCommand request, ISender mediator) => + { + var response = await mediator.Send(request); + return Results.Ok(response); + }) + .WithName(nameof(CreateBrandEndpoint)) + .WithSummary("creates a brand") + .WithDescription("creates a brand") + .Produces() + .RequirePermission("Permissions.Brands.Create") + .MapToApiVersion(1); + } +} diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/DeleteBrandEndpoint.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/DeleteBrandEndpoint.cs new file mode 100644 index 000000000..3b39820dc --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/DeleteBrandEndpoint.cs @@ -0,0 +1,26 @@ +using FSH.Framework.Infrastructure.Auth.Policy; +using FSH.Starter.WebApi.Catalog.Application.Brands.Delete.v1; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace FSH.Starter.WebApi.Catalog.Infrastructure.Endpoints.v1; +public static class DeleteBrandEndpoint +{ + internal static RouteHandlerBuilder MapBrandDeleteEndpoint(this IEndpointRouteBuilder endpoints) + { + return endpoints + .MapDelete("/{id:guid}", async (Guid id, ISender mediator) => + { + await mediator.Send(new DeleteBrandCommand(id)); + return Results.NoContent(); + }) + .WithName(nameof(DeleteBrandEndpoint)) + .WithSummary("deletes brand by id") + .WithDescription("deletes brand by id") + .Produces(StatusCodes.Status204NoContent) + .RequirePermission("Permissions.Brands.Delete") + .MapToApiVersion(1); + } +} diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/GetBrandEndpoint.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/GetBrandEndpoint.cs new file mode 100644 index 000000000..13600025c --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/GetBrandEndpoint.cs @@ -0,0 +1,26 @@ +using FSH.Framework.Infrastructure.Auth.Policy; +using FSH.Starter.WebApi.Catalog.Application.Brands.Get.v1; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace FSH.Starter.WebApi.Catalog.Infrastructure.Endpoints.v1; +public static class GetBrandEndpoint +{ + internal static RouteHandlerBuilder MapGetBrandEndpoint(this IEndpointRouteBuilder endpoints) + { + return endpoints + .MapGet("/{id:guid}", async (Guid id, ISender mediator) => + { + var response = await mediator.Send(new GetBrandRequest(id)); + return Results.Ok(response); + }) + .WithName(nameof(GetBrandEndpoint)) + .WithSummary("gets brand by id") + .WithDescription("gets brand by id") + .Produces() + .RequirePermission("Permissions.Brands.View") + .MapToApiVersion(1); + } +} diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/SearchBrandsEndpoint.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/SearchBrandsEndpoint.cs new file mode 100644 index 000000000..bc6d9a83f --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/SearchBrandsEndpoint.cs @@ -0,0 +1,30 @@ +using FSH.Framework.Core.Paging; +using FSH.Framework.Infrastructure.Auth.Policy; +using FSH.Starter.WebApi.Catalog.Application.Brands.Get.v1; +using FSH.Starter.WebApi.Catalog.Application.Brands.Search.v1; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace FSH.Starter.WebApi.Catalog.Infrastructure.Endpoints.v1; + +public static class SearchBrandsEndpoint +{ + internal static RouteHandlerBuilder MapGetBrandListEndpoint(this IEndpointRouteBuilder endpoints) + { + return endpoints + .MapPost("/search", async (ISender mediator, [FromBody] SearchBrandsCommand command) => + { + var response = await mediator.Send(command); + return Results.Ok(response); + }) + .WithName(nameof(SearchBrandsEndpoint)) + .WithSummary("Gets a list of brands") + .WithDescription("Gets a list of brands with pagination and filtering support") + .Produces>() + .RequirePermission("Permissions.Brands.View") + .MapToApiVersion(1); + } +} diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/UpdateBrandEndpoint.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/UpdateBrandEndpoint.cs new file mode 100644 index 000000000..3e07b34be --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Endpoints/v1/UpdateBrandEndpoint.cs @@ -0,0 +1,27 @@ +using FSH.Framework.Infrastructure.Auth.Policy; +using FSH.Starter.WebApi.Catalog.Application.Brands.Update.v1; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace FSH.Starter.WebApi.Catalog.Infrastructure.Endpoints.v1; +public static class UpdateBrandEndpoint +{ + internal static RouteHandlerBuilder MapBrandUpdateEndpoint(this IEndpointRouteBuilder endpoints) + { + return endpoints + .MapPut("/{id:guid}", async (Guid id, UpdateBrandCommand request, ISender mediator) => + { + if (id != request.Id) return Results.BadRequest(); + var response = await mediator.Send(request); + return Results.Ok(response); + }) + .WithName(nameof(UpdateBrandEndpoint)) + .WithSummary("update a brand") + .WithDescription("update a brand") + .Produces() + .RequirePermission("Permissions.Brands.Update") + .MapToApiVersion(1); + } +} From 90c86abdff8b2c154a00fb62d742c2ba0db0cc2c Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Sat, 16 Nov 2024 10:26:42 +0100 Subject: [PATCH 04/14] Add BrandConfiguration for EF Core in Catalog project Introduce BrandConfiguration class to configure the Brand entity for Entity Framework Core. This includes enabling multi-tenancy, setting the primary key, and specifying maximum lengths for the Name (100 characters) and Description (1000 characters) properties. --- .../Configurations/BrandConfigurations.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/api/modules/Catalog/Catalog.Infrastructure/Persistence/Configurations/BrandConfigurations.cs diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Persistence/Configurations/BrandConfigurations.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Persistence/Configurations/BrandConfigurations.cs new file mode 100644 index 000000000..0abf96da3 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Persistence/Configurations/BrandConfigurations.cs @@ -0,0 +1,16 @@ +using Finbuckle.MultiTenant; +using FSH.Starter.WebApi.Catalog.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace FSH.Starter.WebApi.Catalog.Infrastructure.Persistence.Configurations; +internal sealed class BrandConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.IsMultiTenant(); + builder.HasKey(x => x.Id); + builder.Property(x => x.Name).HasMaxLength(100); + builder.Property(x => x.Description).HasMaxLength(1000); + } +} From 5ee1bb122085be4fec6c9a888610837b7dc10b2e Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Sat, 16 Nov 2024 10:30:12 +0100 Subject: [PATCH 05/14] Add permissions for managing Brands in FshPermissions The changes introduce new permissions related to the "Brands" resource in the `FshPermissions` class. Specifically, the following permissions are added: - View Brands (with `IsBasic` set to true) - Search Brands (with `IsBasic` set to true) - Create Brands - Update Brands - Delete Brands - Export Brands The "View Brands" and "Search Brands" permissions are marked as basic, indicating they might be available to users with basic access rights. --- src/Shared/Authorization/FshPermissions.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Shared/Authorization/FshPermissions.cs b/src/Shared/Authorization/FshPermissions.cs index 01872af96..e18c1b500 100644 --- a/src/Shared/Authorization/FshPermissions.cs +++ b/src/Shared/Authorization/FshPermissions.cs @@ -36,6 +36,14 @@ public static class FshPermissions new("Delete Products", FshActions.Delete, FshResources.Products), new("Export Products", FshActions.Export, FshResources.Products), + //brands + new("View Brands", FshActions.View, FshResources.Brands, IsBasic: true), + new("Search Brands", FshActions.Search, FshResources.Brands, IsBasic: true), + new("Create Brands", FshActions.Create, FshResources.Brands), + new("Update Brands", FshActions.Update, FshResources.Brands), + new("Delete Brands", FshActions.Delete, FshResources.Brands), + new("Export Brands", FshActions.Export, FshResources.Brands), + //todos new("View Todos", FshActions.View, FshResources.Todos, IsBasic: true), new("Search Todos", FshActions.Search, FshResources.Todos, IsBasic: true), From c2f8010788987c99f74367d3e829db53bd55bead Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Sat, 16 Nov 2024 10:46:16 +0100 Subject: [PATCH 06/14] Refactor product handling to include brand details - Modified GetProductHandler to use specification pattern - Added GetProductSpecs for flexible product querying - Updated ProductResponse to include BrandResponse - Enhanced SearchProductSpecs to include brand details - Updated Product class to establish brand relationship - Modified Create and Update methods to accept brandId --- .../Products/Get/v1/GetProductHandler.cs | 5 +++-- .../Products/Get/v1/GetProductSpecs.cs | 14 ++++++++++++++ .../Products/Get/v1/ProductResponse.cs | 4 +++- .../Products/Search/v1/SearchProductSpecs.cs | 1 + src/api/modules/Catalog/Catalog.Domain/Product.cs | 13 +++++++++---- 5 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductSpecs.cs diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductHandler.cs b/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductHandler.cs index 8aea28540..53f327e26 100644 --- a/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductHandler.cs +++ b/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductHandler.cs @@ -18,9 +18,10 @@ public async Task Handle(GetProductRequest request, Cancellatio $"product:{request.Id}", async () => { - var productItem = await repository.GetByIdAsync(request.Id, cancellationToken); + var spec = new GetProductSpecs(request.Id); + var productItem = await repository.FirstOrDefaultAsync(spec, cancellationToken); if (productItem == null) throw new ProductNotFoundException(request.Id); - return new ProductResponse(productItem.Id, productItem.Name, productItem.Description, productItem.Price); + return productItem; }, cancellationToken: cancellationToken); return item!; diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductSpecs.cs b/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductSpecs.cs new file mode 100644 index 000000000..9e30c3767 --- /dev/null +++ b/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductSpecs.cs @@ -0,0 +1,14 @@ +using Ardalis.Specification; +using FSH.Starter.WebApi.Catalog.Domain; + +namespace FSH.Starter.WebApi.Catalog.Application.Products.Get.v1; + +public class GetProductSpecs : Specification +{ + public GetProductSpecs(Guid id) + { + Query + .Where(p => p.Id == id) + .Include(p => p.Brand); + } +} diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/ProductResponse.cs b/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/ProductResponse.cs index 2c199ef2d..080d35868 100644 --- a/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/ProductResponse.cs +++ b/src/api/modules/Catalog/Catalog.Application/Products/Get/v1/ProductResponse.cs @@ -1,2 +1,4 @@ +using FSH.Starter.WebApi.Catalog.Application.Brands.Get.v1; + namespace FSH.Starter.WebApi.Catalog.Application.Products.Get.v1; -public sealed record ProductResponse(Guid? Id, string Name, string? Description, decimal Price); +public sealed record ProductResponse(Guid? Id, string Name, string? Description, decimal Price, BrandResponse? Brand); diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductSpecs.cs b/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductSpecs.cs index 6d9ee52a0..a2159a082 100644 --- a/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductSpecs.cs +++ b/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductSpecs.cs @@ -10,6 +10,7 @@ public class SearchProductSpecs : EntitiesByPaginationFilterSpec Query + .Include(p => p.Brand) .OrderBy(c => c.Name, !command.HasOrderBy()) .Where(p => p.Price >= command.MinimumRate!.Value, command.MinimumRate.HasValue) .Where(p => p.Price <= command.MaximumRate!.Value, command.MaximumRate.HasValue); diff --git a/src/api/modules/Catalog/Catalog.Domain/Product.cs b/src/api/modules/Catalog/Catalog.Domain/Product.cs index 6c5080a51..fbb610bfb 100644 --- a/src/api/modules/Catalog/Catalog.Domain/Product.cs +++ b/src/api/modules/Catalog/Catalog.Domain/Product.cs @@ -8,38 +8,43 @@ public class Product : AuditableEntity, IAggregateRoot public string Name { get; private set; } = default!; public string? Description { get; private set; } public decimal Price { get; private set; } + public Guid? BrandId { get; private set; } + public virtual Brand Brand { get; private set; } = default!; - public static Product Create(string name, string? description, decimal price) + public static Product Create(string name, string? description, decimal price, Guid? brandId) { var product = new Product(); product.Name = name; product.Description = description; product.Price = price; + product.BrandId = brandId; product.QueueDomainEvent(new ProductCreated() { Product = product }); return product; } - public Product Update(string? name, string? description, decimal? price) + public Product Update(string? name, string? description, decimal? price, Guid? brandId) { if (name is not null && Name?.Equals(name, StringComparison.OrdinalIgnoreCase) is not true) Name = name; if (description is not null && Description?.Equals(description, StringComparison.OrdinalIgnoreCase) is not true) Description = description; if (price.HasValue && Price != price) Price = price.Value; + if (brandId.HasValue && brandId.Value != Guid.Empty && !BrandId.Equals(brandId.Value)) BrandId = brandId.Value; this.QueueDomainEvent(new ProductUpdated() { Product = this }); return this; } - public static Product Update(Guid id, string name, string? description, decimal price) + public static Product Update(Guid id, string name, string? description, decimal price, Guid? brandId) { var product = new Product { Id = id, Name = name, Description = description, - Price = price + Price = price, + BrandId = brandId }; product.QueueDomainEvent(new ProductUpdated() { Product = product }); From a4262a30a30ba3595026e7e37a410cdc8c4c9252 Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Sat, 16 Nov 2024 11:08:21 +0100 Subject: [PATCH 07/14] Add brand management endpoints and services Introduce new endpoints in CatalogModule for brand creation, retrieval, listing, updating, and deletion. Register scoped services for Brand in RegisterCatalogServices method. Add DbSet property in CatalogDbContext for managing Brand entities. --- .../Catalog/Catalog.Infrastructure/CatalogModule.cs | 9 +++++++++ .../Persistence/CatalogDbContext.cs | 1 + 2 files changed, 10 insertions(+) diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/CatalogModule.cs b/src/api/modules/Catalog/Catalog.Infrastructure/CatalogModule.cs index e9f3a4eec..8327a6a9a 100644 --- a/src/api/modules/Catalog/Catalog.Infrastructure/CatalogModule.cs +++ b/src/api/modules/Catalog/Catalog.Infrastructure/CatalogModule.cs @@ -23,6 +23,13 @@ public override void AddRoutes(IEndpointRouteBuilder app) productGroup.MapGetProductListEndpoint(); productGroup.MapProductUpdateEndpoint(); productGroup.MapProductDeleteEndpoint(); + + var brandGroup = app.MapGroup("brands").WithTags("brands"); + brandGroup.MapBrandCreationEndpoint(); + brandGroup.MapGetBrandEndpoint(); + brandGroup.MapGetBrandListEndpoint(); + brandGroup.MapBrandUpdateEndpoint(); + brandGroup.MapBrandDeleteEndpoint(); } } public static WebApplicationBuilder RegisterCatalogServices(this WebApplicationBuilder builder) @@ -32,6 +39,8 @@ public static WebApplicationBuilder RegisterCatalogServices(this WebApplicationB builder.Services.AddScoped(); builder.Services.AddKeyedScoped, CatalogRepository>("catalog:products"); builder.Services.AddKeyedScoped, CatalogRepository>("catalog:products"); + builder.Services.AddKeyedScoped, CatalogRepository>("catalog:brands"); + builder.Services.AddKeyedScoped, CatalogRepository>("catalog:brands"); return builder; } public static WebApplication UseCatalogModule(this WebApplication app) diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Persistence/CatalogDbContext.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Persistence/CatalogDbContext.cs index cc3964f6e..1dc8297ed 100644 --- a/src/api/modules/Catalog/Catalog.Infrastructure/Persistence/CatalogDbContext.cs +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Persistence/CatalogDbContext.cs @@ -17,6 +17,7 @@ public CatalogDbContext(IMultiTenantContextAccessor multiTenantCo } public DbSet Products { get; set; } = null!; + public DbSet Brands { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) { From deda091ce00ffa1754a901c9e67a562d956d23ea Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Sat, 16 Nov 2024 11:18:12 +0100 Subject: [PATCH 08/14] Add BrandId property to product commands and handlers - Updated CreateProductCommand and UpdateProductCommand to include BrandId with a default value of null. - Modified CreateProductHandler and UpdateProductHandler to pass BrandId when creating or updating a product. - Added BrandId filter condition in SearchProductSpecs. - Updated CatalogDbInitializer to include BrandId when seeding the database. --- .../Products/Create/v1/CreateProductCommand.cs | 3 ++- .../Products/Create/v1/CreateProductHandler.cs | 2 +- .../Products/Search/v1/SearchProductSpecs.cs | 1 + .../Products/Update/v1/UpdateProductCommand.cs | 3 ++- .../Products/Update/v1/UpdateProductHandler.cs | 2 +- .../Catalog.Infrastructure/Persistence/CatalogDbInitializer.cs | 3 ++- 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Create/v1/CreateProductCommand.cs b/src/api/modules/Catalog/Catalog.Application/Products/Create/v1/CreateProductCommand.cs index 94a69924c..99291ae8e 100644 --- a/src/api/modules/Catalog/Catalog.Application/Products/Create/v1/CreateProductCommand.cs +++ b/src/api/modules/Catalog/Catalog.Application/Products/Create/v1/CreateProductCommand.cs @@ -5,4 +5,5 @@ namespace FSH.Starter.WebApi.Catalog.Application.Products.Create.v1; public sealed record CreateProductCommand( [property: DefaultValue("Sample Product")] string? Name, [property: DefaultValue(10)] decimal Price, - [property: DefaultValue("Descriptive Description")] string? Description = null) : IRequest; + [property: DefaultValue("Descriptive Description")] string? Description = null, + [property: DefaultValue(null)] Guid? BrandId = null) : IRequest; diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Create/v1/CreateProductHandler.cs b/src/api/modules/Catalog/Catalog.Application/Products/Create/v1/CreateProductHandler.cs index 7f02a1951..cb640ac64 100644 --- a/src/api/modules/Catalog/Catalog.Application/Products/Create/v1/CreateProductHandler.cs +++ b/src/api/modules/Catalog/Catalog.Application/Products/Create/v1/CreateProductHandler.cs @@ -13,7 +13,7 @@ public sealed class CreateProductHandler( public async Task Handle(CreateProductCommand request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); - var product = Product.Create(request.Name!, request.Description, request.Price); + var product = Product.Create(request.Name!, request.Description, request.Price, request.BrandId); await repository.AddAsync(product, cancellationToken); logger.LogInformation("product created {ProductId}", product.Id); return new CreateProductResponse(product.Id); diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductSpecs.cs b/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductSpecs.cs index a2159a082..98567c6a5 100644 --- a/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductSpecs.cs +++ b/src/api/modules/Catalog/Catalog.Application/Products/Search/v1/SearchProductSpecs.cs @@ -12,6 +12,7 @@ public SearchProductSpecs(SearchProductsCommand command) Query .Include(p => p.Brand) .OrderBy(c => c.Name, !command.HasOrderBy()) + .Where(p => p.BrandId == command.BrandId!.Value, command.BrandId.HasValue) .Where(p => p.Price >= command.MinimumRate!.Value, command.MinimumRate.HasValue) .Where(p => p.Price <= command.MaximumRate!.Value, command.MaximumRate.HasValue); } diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Update/v1/UpdateProductCommand.cs b/src/api/modules/Catalog/Catalog.Application/Products/Update/v1/UpdateProductCommand.cs index 76d2bf2c7..dd7db751c 100644 --- a/src/api/modules/Catalog/Catalog.Application/Products/Update/v1/UpdateProductCommand.cs +++ b/src/api/modules/Catalog/Catalog.Application/Products/Update/v1/UpdateProductCommand.cs @@ -5,4 +5,5 @@ public sealed record UpdateProductCommand( Guid Id, string? Name, decimal Price, - string? Description = null) : IRequest; + string? Description = null, + Guid? BrandId = null) : IRequest; diff --git a/src/api/modules/Catalog/Catalog.Application/Products/Update/v1/UpdateProductHandler.cs b/src/api/modules/Catalog/Catalog.Application/Products/Update/v1/UpdateProductHandler.cs index e1d6156ca..506219625 100644 --- a/src/api/modules/Catalog/Catalog.Application/Products/Update/v1/UpdateProductHandler.cs +++ b/src/api/modules/Catalog/Catalog.Application/Products/Update/v1/UpdateProductHandler.cs @@ -16,7 +16,7 @@ public async Task Handle(UpdateProductCommand request, Ca ArgumentNullException.ThrowIfNull(request); var product = await repository.GetByIdAsync(request.Id, cancellationToken); _ = product ?? throw new ProductNotFoundException(request.Id); - var updatedProduct = product.Update(request.Name, request.Description, request.Price); + var updatedProduct = product.Update(request.Name, request.Description, request.Price, request.BrandId); await repository.UpdateAsync(updatedProduct, cancellationToken); logger.LogInformation("product with id : {ProductId} updated.", product.Id); return new UpdateProductResponse(product.Id); diff --git a/src/api/modules/Catalog/Catalog.Infrastructure/Persistence/CatalogDbInitializer.cs b/src/api/modules/Catalog/Catalog.Infrastructure/Persistence/CatalogDbInitializer.cs index e627108e6..db213a062 100644 --- a/src/api/modules/Catalog/Catalog.Infrastructure/Persistence/CatalogDbInitializer.cs +++ b/src/api/modules/Catalog/Catalog.Infrastructure/Persistence/CatalogDbInitializer.cs @@ -22,9 +22,10 @@ public async Task SeedAsync(CancellationToken cancellationToken) const string Name = "Keychron V6 QMK Custom Wired Mechanical Keyboard"; const string Description = "A full-size layout QMK/VIA custom mechanical keyboard"; const decimal Price = 79; + Guid? BrandId = null; if (await context.Products.FirstOrDefaultAsync(t => t.Name == Name, cancellationToken).ConfigureAwait(false) is null) { - var product = Product.Create(Name, Description, Price); + var product = Product.Create(Name, Description, Price, BrandId); await context.Products.AddAsync(product, cancellationToken); await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); logger.LogInformation("[{Tenant}] seeding default catalog data", context.TenantInfo!.Identifier); From 6f1f6f664e7d580e26d06a9b6fef4a62107e7101 Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Sat, 16 Nov 2024 11:31:08 +0100 Subject: [PATCH 09/14] Add Brands table and update Catalog schema Removed old migration and added new migration to create Brands table alongside Products table. Updated Designer and DbContext snapshot to reflect new schema. Updated project file to include new Catalog folder. --- .../20240601095057_Add Catalog Schema.cs | 45 ---------- ...1116102306_Add Catalog Schema.Designer.cs} | 57 ++++++++++++- .../20241116102306_Add Catalog Schema.cs | 82 +++++++++++++++++++ .../Catalog/CatalogDbContextModelSnapshot.cs | 55 ++++++++++++- .../migrations/PostgreSQL/PostgreSQL.csproj | 3 + 5 files changed, 194 insertions(+), 48 deletions(-) delete mode 100644 src/api/migrations/PostgreSQL/Catalog/20240601095057_Add Catalog Schema.cs rename src/api/migrations/PostgreSQL/Catalog/{20240601095057_Add Catalog Schema.Designer.cs => 20241116102306_Add Catalog Schema.Designer.cs} (55%) create mode 100644 src/api/migrations/PostgreSQL/Catalog/20241116102306_Add Catalog Schema.cs diff --git a/src/api/migrations/PostgreSQL/Catalog/20240601095057_Add Catalog Schema.cs b/src/api/migrations/PostgreSQL/Catalog/20240601095057_Add Catalog Schema.cs deleted file mode 100644 index c95ee16aa..000000000 --- a/src/api/migrations/PostgreSQL/Catalog/20240601095057_Add Catalog Schema.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace FSH.Starter.WebApi.Migrations.PostgreSQL.Catalog -{ - /// - public partial class AddCatalogSchema : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.EnsureSchema( - name: "catalog"); - - migrationBuilder.CreateTable( - name: "Products", - schema: "catalog", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), - Price = table.Column(type: "numeric", nullable: false), - TenantId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Created = table.Column(type: "timestamp with time zone", nullable: false), - CreatedBy = table.Column(type: "uuid", nullable: false), - LastModified = table.Column(type: "timestamp with time zone", nullable: false), - LastModifiedBy = table.Column(type: "uuid", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Products", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Products", - schema: "catalog"); - } - } -} diff --git a/src/api/migrations/PostgreSQL/Catalog/20240601095057_Add Catalog Schema.Designer.cs b/src/api/migrations/PostgreSQL/Catalog/20241116102306_Add Catalog Schema.Designer.cs similarity index 55% rename from src/api/migrations/PostgreSQL/Catalog/20240601095057_Add Catalog Schema.Designer.cs rename to src/api/migrations/PostgreSQL/Catalog/20241116102306_Add Catalog Schema.Designer.cs index 994aa4477..e20c0bba2 100644 --- a/src/api/migrations/PostgreSQL/Catalog/20240601095057_Add Catalog Schema.Designer.cs +++ b/src/api/migrations/PostgreSQL/Catalog/20241116102306_Add Catalog Schema.Designer.cs @@ -12,7 +12,7 @@ namespace FSH.Starter.WebApi.Migrations.PostgreSQL.Catalog { [DbContext(typeof(CatalogDbContext))] - [Migration("20240601095057_Add Catalog Schema")] + [Migration("20241116102306_Add Catalog Schema")] partial class AddCatalogSchema { /// @@ -21,17 +21,59 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("catalog") - .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("ProductVersion", "8.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("FSH.Starter.WebApi.Catalog.Domain.Brand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.ToTable("Brands", "catalog"); + + b.HasAnnotation("Finbuckle:MultiTenant", true); + }); + modelBuilder.Entity("FSH.Starter.WebApi.Catalog.Domain.Product", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("BrandId") + .HasColumnType("uuid"); + b.Property("Created") .HasColumnType("timestamp with time zone"); @@ -63,10 +105,21 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("BrandId"); + b.ToTable("Products", "catalog"); b.HasAnnotation("Finbuckle:MultiTenant", true); }); + + modelBuilder.Entity("FSH.Starter.WebApi.Catalog.Domain.Product", b => + { + b.HasOne("FSH.Starter.WebApi.Catalog.Domain.Brand", "Brand") + .WithMany() + .HasForeignKey("BrandId"); + + b.Navigation("Brand"); + }); #pragma warning restore 612, 618 } } diff --git a/src/api/migrations/PostgreSQL/Catalog/20241116102306_Add Catalog Schema.cs b/src/api/migrations/PostgreSQL/Catalog/20241116102306_Add Catalog Schema.cs new file mode 100644 index 000000000..17b995411 --- /dev/null +++ b/src/api/migrations/PostgreSQL/Catalog/20241116102306_Add Catalog Schema.cs @@ -0,0 +1,82 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FSH.Starter.WebApi.Migrations.PostgreSQL.Catalog +{ + /// + public partial class AddCatalogSchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "catalog"); + + migrationBuilder.CreateTable( + name: "Brands", + schema: "catalog", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + TenantId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Created = table.Column(type: "timestamp with time zone", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + LastModified = table.Column(type: "timestamp with time zone", nullable: false), + LastModifiedBy = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Brands", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Products", + schema: "catalog", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + Price = table.Column(type: "numeric", nullable: false), + BrandId = table.Column(type: "uuid", nullable: true), + TenantId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Created = table.Column(type: "timestamp with time zone", nullable: false), + CreatedBy = table.Column(type: "uuid", nullable: false), + LastModified = table.Column(type: "timestamp with time zone", nullable: false), + LastModifiedBy = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.Id); + table.ForeignKey( + name: "FK_Products_Brands_BrandId", + column: x => x.BrandId, + principalSchema: "catalog", + principalTable: "Brands", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Products_BrandId", + schema: "catalog", + table: "Products", + column: "BrandId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Products", + schema: "catalog"); + + migrationBuilder.DropTable( + name: "Brands", + schema: "catalog"); + } + } +} diff --git a/src/api/migrations/PostgreSQL/Catalog/CatalogDbContextModelSnapshot.cs b/src/api/migrations/PostgreSQL/Catalog/CatalogDbContextModelSnapshot.cs index 9a90e2270..615efd23b 100644 --- a/src/api/migrations/PostgreSQL/Catalog/CatalogDbContextModelSnapshot.cs +++ b/src/api/migrations/PostgreSQL/Catalog/CatalogDbContextModelSnapshot.cs @@ -18,17 +18,59 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("catalog") - .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("ProductVersion", "8.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("FSH.Starter.WebApi.Catalog.Domain.Brand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("LastModified") + .HasColumnType("timestamp with time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("TenantId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.ToTable("Brands", "catalog"); + + b.HasAnnotation("Finbuckle:MultiTenant", true); + }); + modelBuilder.Entity("FSH.Starter.WebApi.Catalog.Domain.Product", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("BrandId") + .HasColumnType("uuid"); + b.Property("Created") .HasColumnType("timestamp with time zone"); @@ -60,10 +102,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("BrandId"); + b.ToTable("Products", "catalog"); b.HasAnnotation("Finbuckle:MultiTenant", true); }); + + modelBuilder.Entity("FSH.Starter.WebApi.Catalog.Domain.Product", b => + { + b.HasOne("FSH.Starter.WebApi.Catalog.Domain.Brand", "Brand") + .WithMany() + .HasForeignKey("BrandId"); + + b.Navigation("Brand"); + }); #pragma warning restore 612, 618 } } diff --git a/src/api/migrations/PostgreSQL/PostgreSQL.csproj b/src/api/migrations/PostgreSQL/PostgreSQL.csproj index 6952fa90e..64df10b42 100644 --- a/src/api/migrations/PostgreSQL/PostgreSQL.csproj +++ b/src/api/migrations/PostgreSQL/PostgreSQL.csproj @@ -8,4 +8,7 @@ + + + From 3d52528b110cb5f03081693989b59611fcce9f4a Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Sat, 16 Nov 2024 11:35:17 +0100 Subject: [PATCH 10/14] Add cancellation tokens, brand methods, and update runtime Enhanced ApiClient with cancellation tokens and new brand methods. Updated serialization to use JsonSerializerSettings. Upgraded NJsonSchema and NSwag to 14.1.0.0. Changed runtime in nswag.json from WinX64 to Net80. --- .../blazor/infrastructure/Api/ApiClient.cs | 988 ++++++++++++++++-- src/apps/blazor/infrastructure/Api/nswag.json | 2 +- 2 files changed, 874 insertions(+), 116 deletions(-) diff --git a/src/apps/blazor/infrastructure/Api/ApiClient.cs b/src/apps/blazor/infrastructure/Api/ApiClient.cs index b3e9b0bcc..0de5930cb 100644 --- a/src/apps/blazor/infrastructure/Api/ApiClient.cs +++ b/src/apps/blazor/infrastructure/Api/ApiClient.cs @@ -1,6 +1,6 @@ //---------------------- // -// Generated using the NSwag toolchain v14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// Generated using the NSwag toolchain v14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) // //---------------------- @@ -10,6 +10,7 @@ #pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." #pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' #pragma warning disable 612 // Disable "CS0612 '...' is obsolete" +#pragma warning disable 649 // Disable "CS0649 Field is never assigned to, and will always have its default value null" #pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... #pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." #pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" @@ -17,15 +18,130 @@ #pragma warning disable 8603 // Disable "CS8603 Possible null reference return" #pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" #pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" -#pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). +#pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." namespace FSH.Starter.Blazor.Infrastructure.Api { using System = global::System; - [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial interface IApiClient { + /// + /// creates a brand + /// + /// + /// creates a brand + /// + /// The requested API version + /// OK + /// A server side error occurred. + System.Threading.Tasks.Task CreateBrandEndpointAsync(string version, CreateBrandCommand body); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// creates a brand + /// + /// + /// creates a brand + /// + /// The requested API version + /// OK + /// A server side error occurred. + System.Threading.Tasks.Task CreateBrandEndpointAsync(string version, CreateBrandCommand body, System.Threading.CancellationToken cancellationToken); + + /// + /// gets brand by id + /// + /// + /// gets brand by id + /// + /// The requested API version + /// OK + /// A server side error occurred. + System.Threading.Tasks.Task GetBrandEndpointAsync(string version, System.Guid id); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// gets brand by id + /// + /// + /// gets brand by id + /// + /// The requested API version + /// OK + /// A server side error occurred. + System.Threading.Tasks.Task GetBrandEndpointAsync(string version, System.Guid id, System.Threading.CancellationToken cancellationToken); + + /// + /// update a brand + /// + /// + /// update a brand + /// + /// The requested API version + /// OK + /// A server side error occurred. + System.Threading.Tasks.Task UpdateBrandEndpointAsync(string version, System.Guid id, UpdateBrandCommand body); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// update a brand + /// + /// + /// update a brand + /// + /// The requested API version + /// OK + /// A server side error occurred. + System.Threading.Tasks.Task UpdateBrandEndpointAsync(string version, System.Guid id, UpdateBrandCommand body, System.Threading.CancellationToken cancellationToken); + + /// + /// deletes brand by id + /// + /// + /// deletes brand by id + /// + /// The requested API version + /// No Content + /// A server side error occurred. + System.Threading.Tasks.Task DeleteBrandEndpointAsync(string version, System.Guid id); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// deletes brand by id + /// + /// + /// deletes brand by id + /// + /// The requested API version + /// No Content + /// A server side error occurred. + System.Threading.Tasks.Task DeleteBrandEndpointAsync(string version, System.Guid id, System.Threading.CancellationToken cancellationToken); + + /// + /// Gets a list of brands + /// + /// + /// Gets a list of brands with pagination and filtering support + /// + /// The requested API version + /// OK + /// A server side error occurred. + System.Threading.Tasks.Task SearchBrandsEndpointAsync(string version, SearchBrandsCommand body); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Gets a list of brands + /// + /// + /// Gets a list of brands with pagination and filtering support + /// + /// The requested API version + /// OK + /// A server side error occurred. + System.Threading.Tasks.Task SearchBrandsEndpointAsync(string version, SearchBrandsCommand body, System.Threading.CancellationToken cancellationToken); + /// /// creates a product /// @@ -823,77 +939,593 @@ public partial interface IApiClient /// A server side error occurred. System.Threading.Tasks.Task AssignRolesToUserEndpointAsync(string id, AssignUserRoleCommand body, System.Threading.CancellationToken cancellationToken); - /// - /// get user roles - /// - /// - /// get user roles - /// - /// OK - /// A server side error occurred. - System.Threading.Tasks.Task> GetUserRolesEndpointAsync(string id); + /// + /// get user roles + /// + /// + /// get user roles + /// + /// OK + /// A server side error occurred. + System.Threading.Tasks.Task> GetUserRolesEndpointAsync(string id); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// get user roles + /// + /// + /// get user roles + /// + /// OK + /// A server side error occurred. + System.Threading.Tasks.Task> GetUserRolesEndpointAsync(string id, System.Threading.CancellationToken cancellationToken); + + /// + /// Get user's audit trail details + /// + /// + /// Get user's audit trail details. + /// + /// OK + /// A server side error occurred. + System.Threading.Tasks.Task> GetUserAuditTrailEndpointAsync(System.Guid id); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Get user's audit trail details + /// + /// + /// Get user's audit trail details. + /// + /// OK + /// A server side error occurred. + System.Threading.Tasks.Task> GetUserAuditTrailEndpointAsync(System.Guid id, System.Threading.CancellationToken cancellationToken); + + } + + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ApiClient : IApiClient + { + private System.Net.Http.HttpClient _httpClient; + private static System.Lazy _settings = new System.Lazy(CreateSerializerSettings, true); + private System.Text.Json.JsonSerializerOptions _instanceSettings; + + #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public ApiClient(System.Net.Http.HttpClient httpClient) + #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + { + _httpClient = httpClient; + Initialize(); + } + + private static System.Text.Json.JsonSerializerOptions CreateSerializerSettings() + { + var settings = new System.Text.Json.JsonSerializerOptions(); + UpdateJsonSerializerSettings(settings); + return settings; + } + + protected System.Text.Json.JsonSerializerOptions JsonSerializerSettings { get { return _instanceSettings ?? _settings.Value; } } + + static partial void UpdateJsonSerializerSettings(System.Text.Json.JsonSerializerOptions settings); + + partial void Initialize(); + + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url); + partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder); + partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response); + + /// + /// creates a brand + /// + /// + /// creates a brand + /// + /// The requested API version + /// OK + /// A server side error occurred. + public virtual System.Threading.Tasks.Task CreateBrandEndpointAsync(string version, CreateBrandCommand body) + { + return CreateBrandEndpointAsync(version, body, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// creates a brand + /// + /// + /// creates a brand + /// + /// The requested API version + /// OK + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task CreateBrandEndpointAsync(string version, CreateBrandCommand body, System.Threading.CancellationToken cancellationToken) + { + if (version == null) + throw new System.ArgumentNullException("version"); + + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/v{version}/catalog/brands" + urlBuilder_.Append("api/v"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(version, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/catalog/brands"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// gets brand by id + /// + /// + /// gets brand by id + /// + /// The requested API version + /// OK + /// A server side error occurred. + public virtual System.Threading.Tasks.Task GetBrandEndpointAsync(string version, System.Guid id) + { + return GetBrandEndpointAsync(version, id, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// gets brand by id + /// + /// + /// gets brand by id + /// + /// The requested API version + /// OK + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task GetBrandEndpointAsync(string version, System.Guid id, System.Threading.CancellationToken cancellationToken) + { + if (version == null) + throw new System.ArgumentNullException("version"); + + if (id == null) + throw new System.ArgumentNullException("id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/v{version}/catalog/brands/{id}" + urlBuilder_.Append("api/v"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(version, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/catalog/brands/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture))); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// update a brand + /// + /// + /// update a brand + /// + /// The requested API version + /// OK + /// A server side error occurred. + public virtual System.Threading.Tasks.Task UpdateBrandEndpointAsync(string version, System.Guid id, UpdateBrandCommand body) + { + return UpdateBrandEndpointAsync(version, id, body, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// update a brand + /// + /// + /// update a brand + /// + /// The requested API version + /// OK + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task UpdateBrandEndpointAsync(string version, System.Guid id, UpdateBrandCommand body, System.Threading.CancellationToken cancellationToken) + { + if (version == null) + throw new System.ArgumentNullException("version"); + + if (id == null) + throw new System.ArgumentNullException("id"); + + if (body == null) + throw new System.ArgumentNullException("body"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("PUT"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/v{version}/catalog/brands/{id}" + urlBuilder_.Append("api/v"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(version, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/catalog/brands/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture))); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + + /// + /// deletes brand by id + /// + /// + /// deletes brand by id + /// + /// The requested API version + /// No Content + /// A server side error occurred. + public virtual System.Threading.Tasks.Task DeleteBrandEndpointAsync(string version, System.Guid id) + { + return DeleteBrandEndpointAsync(version, id, System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// deletes brand by id + /// + /// + /// deletes brand by id + /// + /// The requested API version + /// No Content + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task DeleteBrandEndpointAsync(string version, System.Guid id, System.Threading.CancellationToken cancellationToken) + { + if (version == null) + throw new System.ArgumentNullException("version"); + + if (id == null) + throw new System.ArgumentNullException("id"); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("DELETE"); + + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/v{version}/catalog/brands/{id}" + urlBuilder_.Append("api/v"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(version, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/catalog/brands/"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture))); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); - /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. - /// - /// get user roles - /// - /// - /// get user roles - /// - /// OK - /// A server side error occurred. - System.Threading.Tasks.Task> GetUserRolesEndpointAsync(string id, System.Threading.CancellationToken cancellationToken); + var status_ = (int)response_.StatusCode; + if (status_ == 204) + { + return; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } /// - /// Get user's audit trail details + /// Gets a list of brands /// /// - /// Get user's audit trail details. + /// Gets a list of brands with pagination and filtering support /// + /// The requested API version /// OK /// A server side error occurred. - System.Threading.Tasks.Task> GetUserAuditTrailEndpointAsync(System.Guid id); + public virtual System.Threading.Tasks.Task SearchBrandsEndpointAsync(string version, SearchBrandsCommand body) + { + return SearchBrandsEndpointAsync(version, body, System.Threading.CancellationToken.None); + } /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// - /// Get user's audit trail details + /// Gets a list of brands /// /// - /// Get user's audit trail details. + /// Gets a list of brands with pagination and filtering support /// + /// The requested API version /// OK /// A server side error occurred. - System.Threading.Tasks.Task> GetUserAuditTrailEndpointAsync(System.Guid id, System.Threading.CancellationToken cancellationToken); + public virtual async System.Threading.Tasks.Task SearchBrandsEndpointAsync(string version, SearchBrandsCommand body, System.Threading.CancellationToken cancellationToken) + { + if (version == null) + throw new System.ArgumentNullException("version"); - } + if (body == null) + throw new System.ArgumentNullException("body"); - [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] - public partial class ApiClient : IApiClient - { - private System.Net.Http.HttpClient _httpClient; - private static System.Lazy _settings = new System.Lazy(CreateSerializerSettings, true); + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); + var content_ = new System.Net.Http.ByteArrayContent(json_); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); - #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public ApiClient(System.Net.Http.HttpClient httpClient) - #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - { - _httpClient = httpClient; - } + var urlBuilder_ = new System.Text.StringBuilder(); + + // Operation Path: "api/v{version}/catalog/brands/search" + urlBuilder_.Append("api/v"); + urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(version, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/catalog/brands/search"); - private static System.Text.Json.JsonSerializerOptions CreateSerializerSettings() - { - var settings = new System.Text.Json.JsonSerializerOptions(); - UpdateJsonSerializerSettings(settings); - return settings; - } + PrepareRequest(client_, request_, urlBuilder_); - protected System.Text.Json.JsonSerializerOptions JsonSerializerSettings { get { return _settings.Value; } } + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); - static partial void UpdateJsonSerializerSettings(System.Text.Json.JsonSerializerOptions settings); + PrepareRequest(client_, request_, url_); - partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, string url); - partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder); - partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response); + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new ApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } /// /// creates a product @@ -933,7 +1565,7 @@ public virtual async System.Threading.Tasks.Task CreatePr { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -1141,7 +1773,7 @@ public virtual async System.Threading.Tasks.Task UpdatePr { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -1341,7 +1973,7 @@ public virtual async System.Threading.Tasks.Task Searc { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -1712,7 +2344,7 @@ public virtual async System.Threading.Tasks.Task CreateOrUpdateRoleEndp { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -1907,7 +2539,7 @@ public virtual async System.Threading.Tasks.Task UpdateRolePermissionsEndpointAs { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -2000,7 +2632,7 @@ public virtual async System.Threading.Tasks.Task CreateTen { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -2281,7 +2913,7 @@ public virtual async System.Threading.Tasks.Task Up { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -2575,7 +3207,7 @@ public virtual async System.Threading.Tasks.Task CreateTodoE { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -2783,7 +3415,7 @@ public virtual async System.Threading.Tasks.Task UpdateTodoE { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -2983,7 +3615,7 @@ public virtual async System.Threading.Tasks.Task GetTodoListEn { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -3086,7 +3718,7 @@ public virtual async System.Threading.Tasks.Task RefreshTokenEndp if (tenant == null) throw new System.ArgumentNullException("tenant"); request_.Headers.TryAddWithoutValidation("tenant", ConvertToString(tenant, System.Globalization.CultureInfo.InvariantCulture)); - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -3187,7 +3819,7 @@ public virtual async System.Threading.Tasks.Task TokenGenerationE if (tenant == null) throw new System.ArgumentNullException("tenant"); request_.Headers.TryAddWithoutValidation("tenant", ConvertToString(tenant, System.Globalization.CultureInfo.InvariantCulture)); - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -3284,7 +3916,7 @@ public virtual async System.Threading.Tasks.Task RegisterU { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -3385,7 +4017,7 @@ public virtual async System.Threading.Tasks.Task SelfRegis if (tenant == null) throw new System.ArgumentNullException("tenant"); request_.Headers.TryAddWithoutValidation("tenant", ConvertToString(tenant, System.Globalization.CultureInfo.InvariantCulture)); - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -3482,7 +4114,7 @@ public virtual async System.Threading.Tasks.Task UpdateUserEndpointAsync(UpdateU { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -3939,7 +4571,7 @@ public virtual async System.Threading.Tasks.Task ForgotPasswordEndpointAsync(str if (tenant == null) throw new System.ArgumentNullException("tenant"); request_.Headers.TryAddWithoutValidation("tenant", ConvertToString(tenant, System.Globalization.CultureInfo.InvariantCulture)); - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -4030,7 +4662,7 @@ public virtual async System.Threading.Tasks.Task ChangePasswordEndpointAsync(Cha { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -4125,7 +4757,7 @@ public virtual async System.Threading.Tasks.Task ResetPasswordEndpointAsync(stri if (tenant == null) throw new System.ArgumentNullException("tenant"); request_.Headers.TryAddWithoutValidation("tenant", ConvertToString(tenant, System.Globalization.CultureInfo.InvariantCulture)); - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -4309,7 +4941,7 @@ public virtual async System.Threading.Tasks.Task ToggleUserStatusEndpointAsync(s { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -4405,7 +5037,7 @@ public virtual async System.Threading.Tasks.Task AssignRolesToUserEndpointAsync( { using (var request_ = new System.Net.Http.HttpRequestMessage()) { - var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, _settings.Value); + var json_ = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(body, JsonSerializerSettings); var content_ = new System.Net.Http.ByteArrayContent(json_); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); request_.Content = content_; @@ -4764,7 +5396,7 @@ private string ConvertToString(object? value, System.Globalization.CultureInfo c } } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class ActivateTenantResponse { @@ -4773,7 +5405,7 @@ public partial class ActivateTenantResponse } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class AssignUserRoleCommand { @@ -4782,7 +5414,7 @@ public partial class AssignUserRoleCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class AuditTrail { @@ -4815,7 +5447,49 @@ public partial class AuditTrail } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class BrandResponse + { + + [System.Text.Json.Serialization.JsonPropertyName("id")] + public System.Guid? Id { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string? Name { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("description")] + public string? Description { get; set; } = default!; + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class BrandResponsePagedList + { + + [System.Text.Json.Serialization.JsonPropertyName("items")] + public System.Collections.Generic.ICollection? Items { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("pageNumber")] + public int PageNumber { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("pageSize")] + public int PageSize { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("totalCount")] + public int TotalCount { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("totalPages")] + public int TotalPages { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("hasPrevious")] + public bool HasPrevious { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("hasNext")] + public bool HasNext { get; set; } = default!; + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class ChangePasswordCommand { @@ -4830,7 +5504,28 @@ public partial class ChangePasswordCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class CreateBrandCommand + { + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string? Name { get; set; } = "Sample Brand"; + + [System.Text.Json.Serialization.JsonPropertyName("description")] + public string? Description { get; set; } = "Descriptive Description"; + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class CreateBrandResponse + { + + [System.Text.Json.Serialization.JsonPropertyName("id")] + public System.Guid? Id { get; set; } = default!; + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class CreateOrUpdateRoleCommand { @@ -4845,7 +5540,7 @@ public partial class CreateOrUpdateRoleCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class CreateProductCommand { @@ -4858,9 +5553,12 @@ public partial class CreateProductCommand [System.Text.Json.Serialization.JsonPropertyName("description")] public string? Description { get; set; } = "Descriptive Description"; + [System.Text.Json.Serialization.JsonPropertyName("brandId")] + public System.Guid? BrandId { get; set; } = default!; + } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class CreateProductResponse { @@ -4869,7 +5567,7 @@ public partial class CreateProductResponse } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class CreateTenantCommand { @@ -4890,7 +5588,7 @@ public partial class CreateTenantCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class CreateTenantResponse { @@ -4899,7 +5597,7 @@ public partial class CreateTenantResponse } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class CreateTodoCommand { @@ -4911,7 +5609,7 @@ public partial class CreateTodoCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class CreateTodoResponse { @@ -4920,7 +5618,7 @@ public partial class CreateTodoResponse } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class DisableTenantResponse { @@ -4929,7 +5627,7 @@ public partial class DisableTenantResponse } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class FileUploadCommand { @@ -4944,7 +5642,7 @@ public partial class FileUploadCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class Filter { @@ -4965,7 +5663,7 @@ public partial class Filter } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class ForgotPasswordCommand { @@ -4974,7 +5672,7 @@ public partial class ForgotPasswordCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class GetTodoResponse { @@ -4989,7 +5687,7 @@ public partial class GetTodoResponse } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class PaginationFilter { @@ -5013,7 +5711,7 @@ public partial class PaginationFilter } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class ProductResponse { @@ -5029,9 +5727,12 @@ public partial class ProductResponse [System.Text.Json.Serialization.JsonPropertyName("price")] public double Price { get; set; } = default!; + [System.Text.Json.Serialization.JsonPropertyName("brand")] + public BrandResponse Brand { get; set; } = default!; + } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class ProductResponsePagedList { @@ -5058,7 +5759,7 @@ public partial class ProductResponsePagedList } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class RefreshTokenCommand { @@ -5070,7 +5771,7 @@ public partial class RefreshTokenCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class RegisterUserCommand { @@ -5097,7 +5798,7 @@ public partial class RegisterUserCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class RegisterUserResponse { @@ -5106,7 +5807,7 @@ public partial class RegisterUserResponse } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class ResetPasswordCommand { @@ -5121,7 +5822,7 @@ public partial class ResetPasswordCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class RoleDto { @@ -5139,7 +5840,7 @@ public partial class RoleDto } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class Search { @@ -5151,7 +5852,37 @@ public partial class Search } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class SearchBrandsCommand + { + + [System.Text.Json.Serialization.JsonPropertyName("advancedSearch")] + public Search AdvancedSearch { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("keyword")] + public string? Keyword { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("advancedFilter")] + public Filter AdvancedFilter { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("pageNumber")] + public int PageNumber { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("pageSize")] + public int PageSize { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("orderBy")] + public System.Collections.Generic.ICollection? OrderBy { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string? Name { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("description")] + public string? Description { get; set; } = default!; + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class SearchProductsCommand { @@ -5184,7 +5915,7 @@ public partial class SearchProductsCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class TenantDetail { @@ -5211,7 +5942,7 @@ public partial class TenantDetail } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class TodoDto { @@ -5226,7 +5957,7 @@ public partial class TodoDto } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class TodoDtoPagedList { @@ -5253,7 +5984,7 @@ public partial class TodoDtoPagedList } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class ToggleUserStatusCommand { @@ -5265,7 +5996,7 @@ public partial class ToggleUserStatusCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class TokenGenerationCommand { @@ -5277,7 +6008,7 @@ public partial class TokenGenerationCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class TokenResponse { @@ -5292,7 +6023,31 @@ public partial class TokenResponse } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class UpdateBrandCommand + { + + [System.Text.Json.Serialization.JsonPropertyName("id")] + public System.Guid Id { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("name")] + public string? Name { get; set; } = default!; + + [System.Text.Json.Serialization.JsonPropertyName("description")] + public string? Description { get; set; } = default!; + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class UpdateBrandResponse + { + + [System.Text.Json.Serialization.JsonPropertyName("id")] + public System.Guid? Id { get; set; } = default!; + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class UpdatePermissionsCommand { @@ -5304,7 +6059,7 @@ public partial class UpdatePermissionsCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class UpdateProductCommand { @@ -5320,9 +6075,12 @@ public partial class UpdateProductCommand [System.Text.Json.Serialization.JsonPropertyName("description")] public string? Description { get; set; } = default!; + [System.Text.Json.Serialization.JsonPropertyName("brandId")] + public System.Guid? BrandId { get; set; } = default!; + } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class UpdateProductResponse { @@ -5331,7 +6089,7 @@ public partial class UpdateProductResponse } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class UpdateTodoCommand { @@ -5346,7 +6104,7 @@ public partial class UpdateTodoCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class UpdateTodoResponse { @@ -5355,7 +6113,7 @@ public partial class UpdateTodoResponse } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class UpdateUserCommand { @@ -5382,7 +6140,7 @@ public partial class UpdateUserCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class UpgradeSubscriptionCommand { @@ -5394,7 +6152,7 @@ public partial class UpgradeSubscriptionCommand } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class UpgradeSubscriptionResponse { @@ -5406,7 +6164,7 @@ public partial class UpgradeSubscriptionResponse } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class UserDetail { @@ -5439,7 +6197,7 @@ public partial class UserDetail } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class UserRoleDetail { @@ -5459,7 +6217,7 @@ public partial class UserRoleDetail - [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class ApiException : System.Exception { public int StatusCode { get; private set; } @@ -5482,7 +6240,7 @@ public override string ToString() } } - [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.0.7.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))")] + [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class ApiException : ApiException { public TResult Result { get; private set; } diff --git a/src/apps/blazor/infrastructure/Api/nswag.json b/src/apps/blazor/infrastructure/Api/nswag.json index ac898f089..4d3fb1c43 100644 --- a/src/apps/blazor/infrastructure/Api/nswag.json +++ b/src/apps/blazor/infrastructure/Api/nswag.json @@ -1,5 +1,5 @@ { - "runtime": "WinX64", + "runtime": "Net80", "defaultVariables": null, "documentGenerator": { "fromDocument": { From 50036c19d26cb63122000428804ae8933227c69e Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Sat, 16 Nov 2024 13:40:37 +0100 Subject: [PATCH 11/14] Add Brands management feature with navigation and CRUD Introduced a new "Brands" section in the application: - Added a navigation link for "Brands" in `NavMenu.razor`. - Implemented permission checks for viewing Brands in `NavMenu.razor.cs`. - Created `Brands.razor` page with route `/catalog/brands`. - Set up `EntityTable` component for managing brands. - Added `Brands` class and dependency injection in `Brands.razor.cs`. - Defined `BrandViewModel` for CRUD operations in `Brands.razor.cs`. --- src/apps/blazor/client/Layout/NavMenu.razor | 1 + .../blazor/client/Layout/NavMenu.razor.cs | 2 + .../blazor/client/Pages/Catalog/Brands.razor | 44 ++++++++++++++++ .../client/Pages/Catalog/Brands.razor.cs | 50 +++++++++++++++++++ 4 files changed, 97 insertions(+) create mode 100644 src/apps/blazor/client/Pages/Catalog/Brands.razor create mode 100644 src/apps/blazor/client/Pages/Catalog/Brands.razor.cs diff --git a/src/apps/blazor/client/Layout/NavMenu.razor b/src/apps/blazor/client/Layout/NavMenu.razor index bd13ae88d..86ab42d0d 100644 --- a/src/apps/blazor/client/Layout/NavMenu.razor +++ b/src/apps/blazor/client/Layout/NavMenu.razor @@ -10,6 +10,7 @@ Modules Products + Brands Todos @if (CanViewAdministrationGroup) diff --git a/src/apps/blazor/client/Layout/NavMenu.razor.cs b/src/apps/blazor/client/Layout/NavMenu.razor.cs index f2b4f2550..41b598a48 100644 --- a/src/apps/blazor/client/Layout/NavMenu.razor.cs +++ b/src/apps/blazor/client/Layout/NavMenu.razor.cs @@ -18,6 +18,7 @@ public partial class NavMenu private bool _canViewRoles; private bool _canViewUsers; private bool _canViewProducts; + private bool _canViewBrands; private bool _canViewTodos; private bool _canViewTenants; private bool _canViewAuditTrails; @@ -31,6 +32,7 @@ protected override async Task OnParametersSetAsync() _canViewRoles = await AuthService.HasPermissionAsync(user, FshActions.View, FshResources.Roles); _canViewUsers = await AuthService.HasPermissionAsync(user, FshActions.View, FshResources.Users); _canViewProducts = await AuthService.HasPermissionAsync(user, FshActions.View, FshResources.Products); + _canViewBrands = await AuthService.HasPermissionAsync(user, FshActions.View, FshResources.Brands); _canViewTodos = await AuthService.HasPermissionAsync(user, FshActions.View, FshResources.Todos); _canViewTenants = await AuthService.HasPermissionAsync(user, FshActions.View, FshResources.Tenants); _canViewAuditTrails = await AuthService.HasPermissionAsync(user, FshActions.View, FshResources.AuditTrails); diff --git a/src/apps/blazor/client/Pages/Catalog/Brands.razor b/src/apps/blazor/client/Pages/Catalog/Brands.razor new file mode 100644 index 000000000..e805ff379 --- /dev/null +++ b/src/apps/blazor/client/Pages/Catalog/Brands.razor @@ -0,0 +1,44 @@ +@page "/catalog/brands" + + + + + + + + + + + @if (!Context.AddEditModal.IsCreate) + { + + + + } + + + + + + + + +
+ @if(!Context.AddEditModal.IsCreate) + { + + View + + + + Delete + + } +
+
+
+
+ +
diff --git a/src/apps/blazor/client/Pages/Catalog/Brands.razor.cs b/src/apps/blazor/client/Pages/Catalog/Brands.razor.cs new file mode 100644 index 000000000..846f2985f --- /dev/null +++ b/src/apps/blazor/client/Pages/Catalog/Brands.razor.cs @@ -0,0 +1,50 @@ +using FSH.Starter.Blazor.Client.Components.EntityTable; +using FSH.Starter.Blazor.Infrastructure.Api; +using FSH.Starter.Shared.Authorization; +using Mapster; +using Microsoft.AspNetCore.Components; + +namespace FSH.Starter.Blazor.Client.Pages.Catalog; + +public partial class Brands +{ + [Inject] + protected IApiClient _client { get; set; } = default!; + + protected EntityServerTableContext Context { get; set; } = default!; + + private EntityTable _table = default!; + + protected override void OnInitialized() => + Context = new( + entityName: "Brand", + entityNamePlural: "Brands", + entityResource: FshResources.Brands, + fields: new() + { + new(brand => brand.Id, "Id", "Id"), + new(brand => brand.Name, "Name", "Name"), + new(brand => brand.Description, "Description", "Description") + }, + enableAdvancedSearch: true, + idFunc: brand => brand.Id!.Value, + searchFunc: async filter => + { + var brandFilter = filter.Adapt(); + var result = await _client.SearchBrandsEndpointAsync("1", brandFilter); + return result.Adapt>(); + }, + createFunc: async brand => + { + await _client.CreateBrandEndpointAsync("1", brand.Adapt()); + }, + updateFunc: async (id, brand) => + { + await _client.UpdateBrandEndpointAsync("1", id, brand.Adapt()); + }, + deleteFunc: async id => await _client.DeleteBrandEndpointAsync("1", id)); +} + +public class BrandViewModel : UpdateBrandCommand +{ +} From f04c426f1f1052119d01d3b3ac29b781e1776ba2 Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Sat, 16 Nov 2024 14:07:36 +0100 Subject: [PATCH 12/14] Add brand selection to Products component Added a `MudSelect` component in `Products.razor` for brand selection, bound to `context.BrandId` and populated with a list of brands. Introduced a private `_brands` field in `Products.razor.cs` to store the list of brands. Modified `OnInitialized` to `OnInitializedAsync` and added `LoadBrandsAsync` to fetch brands from the server. Updated `EntityServerTableContext` initialization to include the brand name field. --- .../client/Pages/Catalog/Products.razor | 8 +++++++ .../client/Pages/Catalog/Products.razor.cs | 23 +++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/apps/blazor/client/Pages/Catalog/Products.razor b/src/apps/blazor/client/Pages/Catalog/Products.razor index ceb965042..d38eb4ab1 100644 --- a/src/apps/blazor/client/Pages/Catalog/Products.razor +++ b/src/apps/blazor/client/Pages/Catalog/Products.razor @@ -26,6 +26,14 @@ + + + @foreach (var brand in _brands) + { + @brand.Name + } + +
diff --git a/src/apps/blazor/client/Pages/Catalog/Products.razor.cs b/src/apps/blazor/client/Pages/Catalog/Products.razor.cs index 3cae28ca5..8283308f8 100644 --- a/src/apps/blazor/client/Pages/Catalog/Products.razor.cs +++ b/src/apps/blazor/client/Pages/Catalog/Products.razor.cs @@ -15,7 +15,10 @@ public partial class Products private EntityTable _table = default!; - protected override void OnInitialized() => + private List _brands = new(); + + protected override async Task OnInitializedAsync() + { Context = new( entityName: "Product", entityNamePlural: "Products", @@ -25,7 +28,8 @@ protected override void OnInitialized() => new(prod => prod.Id,"Id", "Id"), new(prod => prod.Name,"Name", "Name"), new(prod => prod.Description, "Description", "Description"), - new(prod => prod.Price, "Price", "Price") + new(prod => prod.Price, "Price", "Price"), + new(prod => prod.Brand?.Name, "Brand", "Brand") }, enableAdvancedSearch: true, idFunc: prod => prod.Id!.Value, @@ -47,6 +51,21 @@ protected override void OnInitialized() => }, deleteFunc: async id => await _client.DeleteProductEndpointAsync("1", id)); + await LoadBrandsAsync(); + } + + private async Task LoadBrandsAsync() + { + if (_brands.Count == 0) + { + var response = await _client.SearchBrandsEndpointAsync("1", new SearchBrandsCommand()); + if (response?.Items != null) + { + _brands = response.Items.ToList(); + } + } + } + // Advanced Search private Guid _searchBrandId; From 35ef710ea354f653acd0f71e60e961a3834e9425 Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Sat, 16 Nov 2024 14:11:50 +0100 Subject: [PATCH 13/14] Add brand filter dropdown to advanced search Added a dropdown (`MudSelect`) for selecting a brand in the advanced search section of the `Products.razor` file, allowing users to filter products by brand with an "All Brands" option. Updated the search function in `Products.razor.cs` to include the selected brand ID (`SearchBrandId`). Changed the type of `SearchBrandId` from `Guid` to `Guid?` to support the nullable brand ID for the "All Brands" option. --- src/apps/blazor/client/Pages/Catalog/Products.razor | 7 +++++++ src/apps/blazor/client/Pages/Catalog/Products.razor.cs | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/apps/blazor/client/Pages/Catalog/Products.razor b/src/apps/blazor/client/Pages/Catalog/Products.razor index d38eb4ab1..f3cb893b1 100644 --- a/src/apps/blazor/client/Pages/Catalog/Products.razor +++ b/src/apps/blazor/client/Pages/Catalog/Products.razor @@ -5,6 +5,13 @@ + + All Brands + @foreach (var brand in _brands) + { + @brand.Name + } + Minimum Rate: @_searchMinimumRate.ToString() Maximum Rate: @_searchMaximumRate.ToString() diff --git a/src/apps/blazor/client/Pages/Catalog/Products.razor.cs b/src/apps/blazor/client/Pages/Catalog/Products.razor.cs index 8283308f8..46266197c 100644 --- a/src/apps/blazor/client/Pages/Catalog/Products.razor.cs +++ b/src/apps/blazor/client/Pages/Catalog/Products.razor.cs @@ -38,6 +38,7 @@ protected override async Task OnInitializedAsync() var productFilter = filter.Adapt(); productFilter.MinimumRate = Convert.ToDouble(SearchMinimumRate); productFilter.MaximumRate = Convert.ToDouble(SearchMaximumRate); + productFilter.BrandId = SearchBrandId; var result = await _client.SearchProductsEndpointAsync("1", productFilter); return result.Adapt>(); }, @@ -68,8 +69,8 @@ private async Task LoadBrandsAsync() // Advanced Search - private Guid _searchBrandId; - private Guid SearchBrandId + private Guid? _searchBrandId; + private Guid? SearchBrandId { get => _searchBrandId; set From c0a9312e936f963b15b11188a31c82fcd9e64d2f Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Sat, 16 Nov 2024 14:18:54 +0100 Subject: [PATCH 14/14] Remove Catalog folder reference from PostgreSQL.csproj The `ItemGroup` containing the `` line was removed from the `PostgreSQL.csproj` file. This change eliminates the folder reference to `Catalog` from the project file. --- src/api/migrations/PostgreSQL/PostgreSQL.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/api/migrations/PostgreSQL/PostgreSQL.csproj b/src/api/migrations/PostgreSQL/PostgreSQL.csproj index 64df10b42..6952fa90e 100644 --- a/src/api/migrations/PostgreSQL/PostgreSQL.csproj +++ b/src/api/migrations/PostgreSQL/PostgreSQL.csproj @@ -8,7 +8,4 @@ - - -