From cd770dc6bdd1dba0ac87f21a405af9784850ebc4 Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Fri, 22 Nov 2024 21:46:35 +0100 Subject: [PATCH 1/3] feature : catalog brand (#1057) * 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. * 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. * 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. * 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. * 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. * 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 * 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. * 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. * 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. * 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. * 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`. * 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. * 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. * 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/Shared/Authorization/FshPermissions.cs | 8 + .../20240601095057_Add Catalog Schema.cs | 45 - ...1116102306_Add Catalog Schema.Designer.cs} | 57 +- .../20241116102306_Add Catalog Schema.cs | 82 ++ .../Catalog/CatalogDbContextModelSnapshot.cs | 55 +- .../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 + .../Create/v1/CreateProductCommand.cs | 3 +- .../Create/v1/CreateProductHandler.cs | 2 +- .../Products/Get/v1/GetProductHandler.cs | 5 +- .../Products/Get/v1/GetProductSpecs.cs | 14 + .../Products/Get/v1/ProductResponse.cs | 4 +- .../Products/Search/v1/SearchProductSpecs.cs | 2 + .../Update/v1/UpdateProductCommand.cs | 3 +- .../Update/v1/UpdateProductHandler.cs | 2 +- .../modules/Catalog/Catalog.Domain/Brand.cs | 48 + .../Catalog.Domain/Events/BrandCreated.cs | 7 + .../Catalog.Domain/Events/BrandUpdated.cs | 7 + .../Exceptions/BrandNotFoundException.cs | 10 + .../modules/Catalog/Catalog.Domain/Product.cs | 13 +- .../Catalog.Infrastructure/CatalogModule.cs | 9 + .../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 + .../Persistence/CatalogDbContext.cs | 1 + .../Persistence/CatalogDbInitializer.cs | 3 +- .../Configurations/BrandConfigurations.cs | 16 + 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 + .../client/Pages/Catalog/Products.razor | 15 + .../client/Pages/Catalog/Products.razor.cs | 28 +- .../blazor/infrastructure/Api/ApiClient.cs | 988 ++++++++++++++++-- src/apps/blazor/infrastructure/Api/nswag.json | 2 +- 52 files changed, 1700 insertions(+), 180 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 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 create mode 100644 src/api/modules/Catalog/Catalog.Application/Products/Get/v1/GetProductSpecs.cs 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 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 create mode 100644 src/api/modules/Catalog/Catalog.Infrastructure/Persistence/Configurations/BrandConfigurations.cs 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/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), 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/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); 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/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..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 @@ -10,7 +10,9 @@ public class SearchProductSpecs : EntitiesByPaginationFilterSpec 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.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") + { + } +} 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 }); 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/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); + } +} 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) { 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); 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); + } +} 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 +{ +} diff --git a/src/apps/blazor/client/Pages/Catalog/Products.razor b/src/apps/blazor/client/Pages/Catalog/Products.razor index ceb965042..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() @@ -26,6 +33,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..46266197c 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, @@ -34,6 +38,7 @@ protected override void OnInitialized() => 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>(); }, @@ -47,10 +52,25 @@ 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; - private Guid SearchBrandId + private Guid? _searchBrandId; + private Guid? SearchBrandId { get => _searchBrandId; set 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 e7b55147268598c87c390ac40eb9e49b87f32d57 Mon Sep 17 00:00:00 2001 From: Jacek Michalski Date: Fri, 22 Nov 2024 21:47:33 +0100 Subject: [PATCH 2/3] Update claims in TokenService and add mobile phone claim (#1058) Replaced JwtRegisteredClaimNames with ClaimTypes for NameIdentifier, Email, and Name in the GetClaims method. Added a new claim for ClaimTypes.MobilePhone to include the user's phone number. --- .../Infrastructure/Identity/Tokens/TokenService.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/api/framework/Infrastructure/Identity/Tokens/TokenService.cs b/src/api/framework/Infrastructure/Identity/Tokens/TokenService.cs index 1eb27e300..ca633e70c 100644 --- a/src/api/framework/Infrastructure/Identity/Tokens/TokenService.cs +++ b/src/api/framework/Infrastructure/Identity/Tokens/TokenService.cs @@ -134,9 +134,10 @@ private List GetClaims(FshUser user, string ipAddress) => new List { new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), - new(JwtRegisteredClaimNames.Sub, user.Id), - new(JwtRegisteredClaimNames.Email, user.Email!), - new(JwtRegisteredClaimNames.Name, user.FirstName ?? string.Empty), + new(ClaimTypes.NameIdentifier, user.Id), + new(ClaimTypes.Email, user.Email!), + new(ClaimTypes.Name, user.FirstName ?? string.Empty), + new(ClaimTypes.MobilePhone, user.PhoneNumber ?? string.Empty), new(FshClaims.Fullname, $"{user.FirstName} {user.LastName}"), new(ClaimTypes.Surname, user.LastName ?? string.Empty), new(FshClaims.IpAddress, ipAddress), From df50461f31026802a1d5fda4dbe334348641eb26 Mon Sep 17 00:00:00 2001 From: Ali Rafay <64028495+AliRafay@users.noreply.github.com> Date: Sat, 23 Nov 2024 01:49:07 +0500 Subject: [PATCH 3/3] feat : Add soft deletion support and global query filters (#1051) Updated `AuditableEntity` and `ISoftDeletable` to include `Deleted` and `DeletedBy` properties for soft deletion tracking. Modified `FshDbContext` to apply a global query filter for `ISoftDeletable` entities, ensuring deleted entities are excluded from queries. Enhanced `AuditInterceptor` to handle soft deletions, including setting `Deleted` and `DeletedBy` properties and updating entity states. Added `AppendGlobalQueryFilter` extension method to facilitate the application of global query filters to entities implementing specific interfaces. Co-authored-by: Mukesh Murugan <31455818+iammukeshm@users.noreply.github.com> --- .../framework/Core/Domain/AuditableEntity.cs | 2 + .../Core/Domain/Contracts/ISoftDeletable.cs | 3 +- .../AppendGlobalQueryFilterExtension.cs | 36 ++++++++++++++++ .../Persistence/FshDbContext.cs | 6 +++ .../Interceptors/AuditInterceptor.cs | 42 ++++++++++++------- 5 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 src/api/framework/Infrastructure/Persistence/AppendGlobalQueryFilterExtension.cs diff --git a/src/api/framework/Core/Domain/AuditableEntity.cs b/src/api/framework/Core/Domain/AuditableEntity.cs index f62ad672b..6639a0215 100644 --- a/src/api/framework/Core/Domain/AuditableEntity.cs +++ b/src/api/framework/Core/Domain/AuditableEntity.cs @@ -8,6 +8,8 @@ public class AuditableEntity : BaseEntity, IAuditable, ISoftDeletable public Guid CreatedBy { get; set; } public DateTimeOffset LastModified { get; set; } public Guid? LastModifiedBy { get; set; } + public DateTimeOffset? Deleted { get; set; } + public Guid? DeletedBy { get; set; } } public abstract class AuditableEntity : AuditableEntity diff --git a/src/api/framework/Core/Domain/Contracts/ISoftDeletable.cs b/src/api/framework/Core/Domain/Contracts/ISoftDeletable.cs index b64d5dd57..d129d02e4 100644 --- a/src/api/framework/Core/Domain/Contracts/ISoftDeletable.cs +++ b/src/api/framework/Core/Domain/Contracts/ISoftDeletable.cs @@ -2,5 +2,6 @@ public interface ISoftDeletable { - + DateTimeOffset? Deleted { get; set; } + Guid? DeletedBy { get; set; } } diff --git a/src/api/framework/Infrastructure/Persistence/AppendGlobalQueryFilterExtension.cs b/src/api/framework/Infrastructure/Persistence/AppendGlobalQueryFilterExtension.cs new file mode 100644 index 000000000..dbd09831d --- /dev/null +++ b/src/api/framework/Infrastructure/Persistence/AppendGlobalQueryFilterExtension.cs @@ -0,0 +1,36 @@ +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; + +namespace FSH.Framework.Infrastructure.Persistence; + +internal static class ModelBuilderExtensions +{ + public static ModelBuilder AppendGlobalQueryFilter(this ModelBuilder modelBuilder, Expression> filter) + { + // get a list of entities without a baseType that implement the interface TInterface + var entities = modelBuilder.Model.GetEntityTypes() + .Where(e => e.BaseType is null && e.ClrType.GetInterface(typeof(TInterface).Name) is not null) + .Select(e => e.ClrType); + + foreach (var entity in entities) + { + var parameterType = Expression.Parameter(modelBuilder.Entity(entity).Metadata.ClrType); + var filterBody = ReplacingExpressionVisitor.Replace(filter.Parameters.Single(), parameterType, filter.Body); + + // get the existing query filter + if (modelBuilder.Entity(entity).Metadata.GetQueryFilter() is { } existingFilter) + { + var existingFilterBody = ReplacingExpressionVisitor.Replace(existingFilter.Parameters.Single(), parameterType, existingFilter.Body); + + // combine the existing query filter with the new query filter + filterBody = Expression.AndAlso(existingFilterBody, filterBody); + } + + // apply the new query filter + modelBuilder.Entity(entity).HasQueryFilter(Expression.Lambda(filterBody, parameterType)); + } + + return modelBuilder; + } +} diff --git a/src/api/framework/Infrastructure/Persistence/FshDbContext.cs b/src/api/framework/Infrastructure/Persistence/FshDbContext.cs index f30fc1966..1f3186e3e 100644 --- a/src/api/framework/Infrastructure/Persistence/FshDbContext.cs +++ b/src/api/framework/Infrastructure/Persistence/FshDbContext.cs @@ -17,6 +17,12 @@ public class FshDbContext(IMultiTenantContextAccessor multiTenant private readonly IPublisher _publisher = publisher; private readonly DatabaseOptions _settings = settings.Value; + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // QueryFilters need to be applied before base.OnModelCreating + modelBuilder.AppendGlobalQueryFilter(s => s.Deleted == null); + base.OnModelCreating(modelBuilder); + } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.EnableSensitiveDataLogging(); diff --git a/src/api/framework/Infrastructure/Persistence/Interceptors/AuditInterceptor.cs b/src/api/framework/Infrastructure/Persistence/Interceptors/AuditInterceptor.cs index e933663d7..6c2d819ca 100644 --- a/src/api/framework/Infrastructure/Persistence/Interceptors/AuditInterceptor.cs +++ b/src/api/framework/Infrastructure/Persistence/Interceptors/AuditInterceptor.cs @@ -38,11 +38,12 @@ private async Task PublishAuditTrailsAsync(DbContextEventData eventData) var utcNow = timeProvider.GetUtcNow(); foreach (var entry in eventData.Context.ChangeTracker.Entries().Where(x => x.State is EntityState.Added or EntityState.Deleted or EntityState.Modified).ToList()) { + var userId = currentUser.GetUserId(); var trail = new TrailDto() { Id = Guid.NewGuid(), TableName = entry.Entity.GetType().Name, - UserId = currentUser.GetUserId(), + UserId = userId, DateTime = utcNow }; @@ -72,19 +73,26 @@ private async Task PublishAuditTrailsAsync(DbContextEventData eventData) break; case EntityState.Modified: - if (property.IsModified && property.OriginalValue == null && property.CurrentValue != null) + if (property.IsModified) { - trail.ModifiedProperties.Add(propertyName); - trail.Type = TrailType.Delete; - trail.OldValues[propertyName] = property.OriginalValue; - trail.NewValues[propertyName] = property.CurrentValue; - } - else if (property.IsModified && property.OriginalValue?.Equals(property.CurrentValue) == false) - { - trail.ModifiedProperties.Add(propertyName); - trail.Type = TrailType.Update; - trail.OldValues[propertyName] = property.OriginalValue; - trail.NewValues[propertyName] = property.CurrentValue; + if (entry.Entity is ISoftDeletable && property.OriginalValue == null && property.CurrentValue != null) + { + trail.ModifiedProperties.Add(propertyName); + trail.Type = TrailType.Delete; + trail.OldValues[propertyName] = property.OriginalValue; + trail.NewValues[propertyName] = property.CurrentValue; + } + else if (property.OriginalValue?.Equals(property.CurrentValue) == false) + { + trail.ModifiedProperties.Add(propertyName); + trail.Type = TrailType.Update; + trail.OldValues[propertyName] = property.OriginalValue; + trail.NewValues[propertyName] = property.CurrentValue; + } + else + { + property.IsModified = false; + } } break; } @@ -106,9 +114,9 @@ public void UpdateEntities(DbContext? context) if (context == null) return; foreach (var entry in context.ChangeTracker.Entries()) { + var utcNow = timeProvider.GetUtcNow(); if (entry.State is EntityState.Added or EntityState.Modified || entry.HasChangedOwnedEntities()) { - var utcNow = timeProvider.GetUtcNow(); if (entry.State == EntityState.Added) { entry.Entity.CreatedBy = currentUser.GetUserId(); @@ -117,6 +125,12 @@ public void UpdateEntities(DbContext? context) entry.Entity.LastModifiedBy = currentUser.GetUserId(); entry.Entity.LastModified = utcNow; } + if(entry.State is EntityState.Deleted && entry.Entity is ISoftDeletable softDelete) + { + softDelete.DeletedBy = currentUser.GetUserId(); + softDelete.Deleted = utcNow; + entry.State = EntityState.Modified; + } } } }