diff --git a/src/Category/Category.csproj b/src/Category/Category.csproj
new file mode 100644
index 000000000..64ede4c9f
--- /dev/null
+++ b/src/Category/Category.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/Category/CategoryModule.cs b/src/Category/CategoryModule.cs
new file mode 100644
index 000000000..4c53c067c
--- /dev/null
+++ b/src/Category/CategoryModule.cs
@@ -0,0 +1,45 @@
+using Carter;
+using Category.Domain;
+using Category.Features.Create.v1;
+using Category.Features.Delete.v1;
+using Category.Features.Get.v1;
+using Category.Features.GetList.v1;
+using Category.Features.Update.v1;
+using Category.Persistence;
+using FSH.Framework.Core.Persistence;
+using FSH.Framework.Infrastructure.Persistence;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+namespace Category;
+
+public static class CategoryModule
+{
+
+ public class Endpoints : CarterModule
+ {
+ public override void AddRoutes(IEndpointRouteBuilder app)
+ {
+ var categoryItemGroup = app.MapGroup("categoryItems").WithTags("categoryItems");
+ categoryItemGroup.MapCategoryItemCreationEndpoint();
+ categoryItemGroup.MapGetCategoryItemEndpoint();
+ categoryItemGroup.MapGetCategoryItemListEndpoint();
+ categoryItemGroup.MapCategoryItemUpdationEndpoint();
+ categoryItemGroup.MapCategoryItemDeletionEndpoint();
+ }
+ }
+ public static WebApplicationBuilder RegisterCategoryItemServices(this WebApplicationBuilder builder)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ builder.Services.BindDbContext();
+ builder.Services.AddScoped();
+ builder.Services.AddKeyedScoped, CategoryItemRepository>("categoryItem");
+ builder.Services.AddKeyedScoped, CategoryItemRepository>("categoryItem");
+ return builder;
+ }
+ public static WebApplication UseCategoryItemModule(this WebApplication app)
+ {
+ return app;
+ }
+}
diff --git a/src/Category/Domain/CategoryItem.cs b/src/Category/Domain/CategoryItem.cs
new file mode 100644
index 000000000..125766d18
--- /dev/null
+++ b/src/Category/Domain/CategoryItem.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Category.Domain.Events;
+using FSH.Framework.Core.Domain;
+using FSH.Framework.Core.Domain.Contracts;
+
+namespace Category.Domain;
+
+public sealed class CategoryItem : AuditableEntity, IAggregateRoot
+{
+ public string Name { get; private set; } = string.Empty;
+ public string Description { get; private set; } = string.Empty;
+
+ private CategoryItem() { }
+
+ private CategoryItem(string name, string description)
+ {
+ Name = name;
+ Description = description;
+ QueueDomainEvent(new CategoryItemCreated(Id, Name, Description));
+ }
+
+ public static CategoryItem Create(string name, string description) => new(name, description);
+
+ public CategoryItem Update(string? name, string? description)
+ {
+ bool isUpdated = false;
+
+ if (!string.IsNullOrWhiteSpace(name) && !string.Equals(Name, name, StringComparison.OrdinalIgnoreCase))
+ {
+ Name = name;
+ isUpdated = true;
+ }
+
+ if (!string.IsNullOrWhiteSpace(description) && !string.Equals(Description, description, StringComparison.OrdinalIgnoreCase))
+ {
+ Description = description;
+ isUpdated = true;
+ }
+
+ if (isUpdated)
+ {
+ QueueDomainEvent(new CategoryItemUpdated(this));
+ }
+
+ return this;
+ }
+}
diff --git a/src/Category/Domain/Events/CategoryItemCreated.cs b/src/Category/Domain/Events/CategoryItemCreated.cs
new file mode 100644
index 000000000..daba999b7
--- /dev/null
+++ b/src/Category/Domain/Events/CategoryItemCreated.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Category.Features.Get.v1;
+using FSH.Framework.Core.Caching;
+using FSH.Framework.Core.Domain.Events;
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace Category.Domain.Events;
+
+public record CategoryItemCreated(Guid Id, string Name, string Description) : DomainEvent;
+
+public class CategoryItemCreatedEventHandler(
+ ILogger logger,
+ ICacheService cache)
+ : INotificationHandler
+{
+ public async Task Handle(CategoryItemCreated notification, CancellationToken cancellationToken)
+ {
+ logger.LogInformation("handling categoryItem item created domain event..");
+ var cacheResponse = new GetCategoryItemResponse(notification.Id, notification.Name, notification.Description);
+ await cache.SetAsync($"categoryItem:{notification.Id}", cacheResponse, cancellationToken: cancellationToken);
+ }
+}
diff --git a/src/Category/Domain/Events/CategoryItemUpdated.cs b/src/Category/Domain/Events/CategoryItemUpdated.cs
new file mode 100644
index 000000000..b32d25a34
--- /dev/null
+++ b/src/Category/Domain/Events/CategoryItemUpdated.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Category.Features.Get.v1;
+using FSH.Framework.Core.Caching;
+using FSH.Framework.Core.Domain.Events;
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace Category.Domain.Events;
+
+public record CategoryItemUpdated(CategoryItem item) : DomainEvent;
+
+public class CategoryItemUpdatedEventHandler(
+ ILogger logger,
+ ICacheService cache)
+ : INotificationHandler
+{
+ public async Task Handle(CategoryItemUpdated notification, CancellationToken cancellationToken)
+ {
+ logger.LogInformation("handling CategoryItem update domain event..");
+ var cacheResponse = new GetCategoryItemResponse(notification.item.Id, notification.item.Name, notification.item.Description );
+ await cache.SetAsync($"categoryItem:{notification.item.Id}", cacheResponse, cancellationToken: cancellationToken);
+ }
+}
diff --git a/src/Category/Exceptions/CategoryItemNotFoundException.cs b/src/Category/Exceptions/CategoryItemNotFoundException.cs
new file mode 100644
index 000000000..ddf85209b
--- /dev/null
+++ b/src/Category/Exceptions/CategoryItemNotFoundException.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FSH.Framework.Core.Exceptions;
+
+namespace Category.Exceptions;
+
+internal sealed class CategoryItemNotFoundException : NotFoundException
+{
+ public CategoryItemNotFoundException(Guid id)
+ : base($"category item with id {id} not found")
+ {
+ }
+}
diff --git a/src/Category/Features/Create/v1/CreateCategoryItemCommand.cs b/src/Category/Features/Create/v1/CreateCategoryItemCommand.cs
new file mode 100644
index 000000000..9c12d772a
--- /dev/null
+++ b/src/Category/Features/Create/v1/CreateCategoryItemCommand.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MediatR;
+
+namespace Category.Features.Create.v1;
+
+public record CreateCategoryItemCommand(
+ [property: DefaultValue("Hello World!")] string Name,
+ [property: DefaultValue("Important Description.")] string Description) : IRequest;
+
+
diff --git a/src/Category/Features/Create/v1/CreateCategoryItemEndPoint.cs b/src/Category/Features/Create/v1/CreateCategoryItemEndPoint.cs
new file mode 100644
index 000000000..a1696e067
--- /dev/null
+++ b/src/Category/Features/Create/v1/CreateCategoryItemEndPoint.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Asp.Versioning;
+using FSH.Framework.Infrastructure.Auth.Policy;
+using MediatR;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace Category.Features.Create.v1;
+
+public static class CreateCategoryItemEndPoint
+{
+ internal static RouteHandlerBuilder MapCategoryItemCreationEndpoint(this IEndpointRouteBuilder endpoints)
+ {
+ return endpoints.MapPost("/", async (CreateCategoryItemCommand request, ISender mediator) =>
+ {
+ var response = await mediator.Send(request);
+ return Results.CreatedAtRoute(nameof(CreateCategoryItemEndPoint), new { id = response.Id }, response);
+ })
+ .WithName(nameof(CreateCategoryItemEndPoint))
+ .WithSummary("Creates a CategoryItem item")
+ .WithDescription("Creates a CategoryItem item")
+ .Produces(StatusCodes.Status201Created)
+ .RequirePermission("Permissions.CategoryItems.Create")
+ .MapToApiVersion(new ApiVersion(1, 0));
+
+ }
+}
diff --git a/src/Category/Features/Create/v1/CreateCategoryItemHandler.cs b/src/Category/Features/Create/v1/CreateCategoryItemHandler.cs
new file mode 100644
index 000000000..81257345a
--- /dev/null
+++ b/src/Category/Features/Create/v1/CreateCategoryItemHandler.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Category.Domain;
+using FSH.Framework.Core.Persistence;
+using MediatR;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Category.Features.Create.v1;
+
+public sealed class CreateCategoryItemHandler(
+ ILogger logger,
+ [FromKeyedServices("categoryItem")] IRepository repository)
+ : IRequestHandler
+{
+ public async Task Handle(CreateCategoryItemCommand request, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+ var item = CategoryItem.Create(request.Name, request.Description);
+ await repository.AddAsync(item, cancellationToken).ConfigureAwait(false);
+ await repository.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ logger.LogInformation("CategoryItem item created {CategoryItemId}", item.Id);
+ return new CreateCategoryItemResponse(item.Id);
+ }
+}
diff --git a/src/Category/Features/Create/v1/CreateCategoryItemResponse.cs b/src/Category/Features/Create/v1/CreateCategoryItemResponse.cs
new file mode 100644
index 000000000..4c3896004
--- /dev/null
+++ b/src/Category/Features/Create/v1/CreateCategoryItemResponse.cs
@@ -0,0 +1,9 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Category.Features.Create.v1;
+
+public record CreateCategoryItemResponse(Guid? Id);
diff --git a/src/Category/Features/Create/v1/CreateCategoryItemValidator.cs b/src/Category/Features/Create/v1/CreateCategoryItemValidator.cs
new file mode 100644
index 000000000..a751d6276
--- /dev/null
+++ b/src/Category/Features/Create/v1/CreateCategoryItemValidator.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Category.Persistence;
+using FluentValidation;
+
+namespace Category.Features.Create.v1;
+
+public class CreateCategoryItemValidator : AbstractValidator
+{
+ public CreateCategoryItemValidator(CategoryItemDbContext context)
+ {
+ RuleFor(p => p.Name).NotEmpty();
+ RuleFor(p => p.Description).NotEmpty();
+ }
+}
diff --git a/src/Category/Features/Delete/v1/DeleteCategoryItemCommand.cs b/src/Category/Features/Delete/v1/DeleteCategoryItemCommand.cs
new file mode 100644
index 000000000..12c19bb9c
--- /dev/null
+++ b/src/Category/Features/Delete/v1/DeleteCategoryItemCommand.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MediatR;
+
+namespace Category.Features.Delete.v1;
+
+public sealed record DeleteCategoryItemCommand(
+ Guid Id) : IRequest;
+
+
+
diff --git a/src/Category/Features/Delete/v1/DeleteCategoryItemEndpoint.cs b/src/Category/Features/Delete/v1/DeleteCategoryItemEndpoint.cs
new file mode 100644
index 000000000..32e90f632
--- /dev/null
+++ b/src/Category/Features/Delete/v1/DeleteCategoryItemEndpoint.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Asp.Versioning;
+using FSH.Framework.Infrastructure.Auth.Policy;
+using MediatR;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace Category.Features.Delete.v1;
+
+public static class DeleteCategoryItemEndpoint
+{
+ internal static RouteHandlerBuilder MapCategoryItemDeletionEndpoint(this IEndpointRouteBuilder endpoints)
+ {
+ return endpoints
+ .MapDelete("/{id:guid}", async (Guid id, ISender mediator) =>
+ {
+ await mediator.Send(new DeleteCategoryItemCommand(id));
+ return Results.NoContent();
+ })
+ .WithName(nameof(DeleteCategoryItemEndpoint))
+ .WithSummary("Deletes a category item")
+ .WithDescription("Deleted a category item")
+ .Produces(StatusCodes.Status204NoContent)
+ .RequirePermission("Permissions.CategoryItems.Delete")
+ .MapToApiVersion(new ApiVersion(1, 0));
+
+ }
+}
diff --git a/src/Category/Features/Delete/v1/DeleteCategoryItemHandler.cs b/src/Category/Features/Delete/v1/DeleteCategoryItemHandler.cs
new file mode 100644
index 000000000..9abeedf4c
--- /dev/null
+++ b/src/Category/Features/Delete/v1/DeleteCategoryItemHandler.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Category.Domain;
+using Category.Exceptions;
+using FSH.Framework.Core.Persistence;
+using MediatR;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Category.Features.Delete.v1;
+
+public sealed class DeleteCategoryItemHandler(
+ ILogger logger,
+ [FromKeyedServices("categoryItem")] IRepository repository)
+ : IRequestHandler
+{
+ public async Task Handle(DeleteCategoryItemCommand request, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+ var categoryItem = await repository.GetByIdAsync(request.Id, cancellationToken);
+ _ = categoryItem ?? throw new CategoryItemNotFoundException(request.Id);
+ await repository.DeleteAsync(categoryItem, cancellationToken);
+ logger.LogInformation("categoryItem with id : {CategoryItemId} deleted", categoryItem.Id);
+ }
+}
diff --git a/src/Category/Features/Get/v1/GetCategoryItemEndpoint.cs b/src/Category/Features/Get/v1/GetCategoryItemEndpoint.cs
new file mode 100644
index 000000000..4ba80e9df
--- /dev/null
+++ b/src/Category/Features/Get/v1/GetCategoryItemEndpoint.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FSH.Framework.Infrastructure.Auth.Policy;
+using MediatR;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace Category.Features.Get.v1;
+
+public static class GetCategoryItemEndpoint
+{
+ internal static RouteHandlerBuilder MapGetCategoryItemEndpoint(this IEndpointRouteBuilder endpoints)
+ {
+ return endpoints.MapGet("/{id:guid}", async (Guid id, ISender mediator) =>
+ {
+ var response = await mediator.Send(new GetCategoryItemRequest(id));
+ return Results.Ok(response);
+ })
+ .WithName(nameof(GetCategoryItemEndpoint))
+ .WithSummary("gets category item by id")
+ .WithDescription("gets category item by id")
+ .Produces()
+ .RequirePermission("Permissions.CategoryItems.View")
+ .MapToApiVersion(1);
+ }
+}
diff --git a/src/Category/Features/Get/v1/GetCategoryItemHandler.cs b/src/Category/Features/Get/v1/GetCategoryItemHandler.cs
new file mode 100644
index 000000000..1c187521f
--- /dev/null
+++ b/src/Category/Features/Get/v1/GetCategoryItemHandler.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Category.Domain;
+using Category.Exceptions;
+using FSH.Framework.Core.Caching;
+using FSH.Framework.Core.Persistence;
+using MediatR;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Category.Features.Get.v1;
+
+public sealed class GetCategoryItemHandler(
+ [FromKeyedServices("categoryItem")] IReadRepository repository,
+ ICacheService cache)
+ : IRequestHandler
+{
+ public async Task Handle(GetCategoryItemRequest request, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+ var item = await cache.GetOrSetAsync(
+ $"categoryItem:{request.Id}",
+ async () =>
+ {
+ var categoryItem = await repository.GetByIdAsync(request.Id, cancellationToken);
+ if (categoryItem == null) throw new CategoryItemNotFoundException(request.Id);
+ return new GetCategoryItemResponse(categoryItem.Id, categoryItem.Name!, categoryItem.Description!);
+ },
+ cancellationToken: cancellationToken);
+ return item!;
+ }
+}
diff --git a/src/Category/Features/Get/v1/GetCategoryItemRequest.cs b/src/Category/Features/Get/v1/GetCategoryItemRequest.cs
new file mode 100644
index 000000000..5209a967a
--- /dev/null
+++ b/src/Category/Features/Get/v1/GetCategoryItemRequest.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MediatR;
+
+namespace Category.Features.Get.v1;
+
+public class GetCategoryItemRequest : IRequest
+{
+ public Guid Id { get; set; }
+ public GetCategoryItemRequest(Guid id) => Id = id;
+}
diff --git a/src/Category/Features/Get/v1/GetCategoryItemResponse.cs b/src/Category/Features/Get/v1/GetCategoryItemResponse.cs
new file mode 100644
index 000000000..95c60ac80
--- /dev/null
+++ b/src/Category/Features/Get/v1/GetCategoryItemResponse.cs
@@ -0,0 +1,9 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Category.Features.Get.v1;
+
+public record GetCategoryItemResponse(Guid? Id, string? Name, string? Description);
diff --git a/src/Category/Features/GetList/v1/CategoryItemDto.cs b/src/Category/Features/GetList/v1/CategoryItemDto.cs
new file mode 100644
index 000000000..688c41ef9
--- /dev/null
+++ b/src/Category/Features/GetList/v1/CategoryItemDto.cs
@@ -0,0 +1,9 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Category.Features.GetList.v1;
+
+public record CategoryItemDto(Guid? Id, string Name, string Description);
diff --git a/src/Category/Features/GetList/v1/GetCategoryItemListEndpoint.cs b/src/Category/Features/GetList/v1/GetCategoryItemListEndpoint.cs
new file mode 100644
index 000000000..61734987b
--- /dev/null
+++ b/src/Category/Features/GetList/v1/GetCategoryItemListEndpoint.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FSH.Framework.Core.Paging;
+using FSH.Framework.Infrastructure.Auth.Policy;
+using MediatR;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Routing;
+
+namespace Category.Features.GetList.v1;
+
+public static class GetCategoryItemListEndpoint
+{
+ internal static RouteHandlerBuilder MapGetCategoryItemListEndpoint(this IEndpointRouteBuilder endpoints)
+ {
+ return endpoints.MapPost("/search", async (ISender mediator, [FromBody] PaginationFilter filter) =>
+ {
+ var response = await mediator.Send(new GetCategoryItemListRequest(filter));
+ return Results.Ok(response);
+ })
+ .WithName(nameof(GetCategoryItemListEndpoint))
+ .WithSummary("Gets a list of Category items with paging support")
+ .WithDescription("Gets a list of Category items with paging support")
+ .Produces>()
+ .RequirePermission("Permissions.CategoryItems.View")
+ .MapToApiVersion(1);
+ }
+}
diff --git a/src/Category/Features/GetList/v1/GetCategoryItemListHandler.cs b/src/Category/Features/GetList/v1/GetCategoryItemListHandler.cs
new file mode 100644
index 000000000..de31d91a9
--- /dev/null
+++ b/src/Category/Features/GetList/v1/GetCategoryItemListHandler.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Category.Domain;
+using FSH.Framework.Core.Paging;
+using FSH.Framework.Core.Persistence;
+using FSH.Framework.Core.Specifications;
+using MediatR;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Category.Features.GetList.v1;
+
+public sealed class GetCategoryItemListHandler(
+ [FromKeyedServices("categoryItem")] IReadRepository repository)
+ : IRequestHandler>
+{
+ public async Task> Handle(GetCategoryItemListRequest request, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+
+ var spec = new EntitiesByPaginationFilterSpec(request.Filter);
+
+ var items = await repository.ListAsync(spec, cancellationToken).ConfigureAwait(false);
+ var totalCount = await repository.CountAsync(spec, cancellationToken).ConfigureAwait(false);
+
+ return new PagedList(items, request.Filter.PageNumber, request.Filter.PageSize, totalCount);
+ }
+}
diff --git a/src/Category/Features/GetList/v1/GetCategoryItemListRequest.cs b/src/Category/Features/GetList/v1/GetCategoryItemListRequest.cs
new file mode 100644
index 000000000..61e9affa1
--- /dev/null
+++ b/src/Category/Features/GetList/v1/GetCategoryItemListRequest.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using FSH.Framework.Core.Paging;
+using MediatR;
+
+namespace Category.Features.GetList.v1;
+
+public record GetCategoryItemListRequest(PaginationFilter Filter) : IRequest>;
diff --git a/src/Category/Features/Update/v1/UpdateCategoryItemCommand.cs b/src/Category/Features/Update/v1/UpdateCategoryItemCommand.cs
new file mode 100644
index 000000000..a2f812c31
--- /dev/null
+++ b/src/Category/Features/Update/v1/UpdateCategoryItemCommand.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MediatR;
+
+namespace Category.Features.Update.v1;
+
+public sealed record UpdateCategoryItemCommand(
+ Guid Id,
+ string? Name,
+ string? Description = null) : IRequest;
+
+
+
diff --git a/src/Category/Features/Update/v1/UpdateCategoryItemEndpoint.cs b/src/Category/Features/Update/v1/UpdateCategoryItemEndpoint.cs
new file mode 100644
index 000000000..1da55adf3
--- /dev/null
+++ b/src/Category/Features/Update/v1/UpdateCategoryItemEndpoint.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Asp.Versioning;
+using FSH.Framework.Infrastructure.Auth.Policy;
+using MediatR;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace Category.Features.Update.v1;
+
+public static class UpdateCategoryItemEndpoint
+{
+ internal static RouteHandlerBuilder MapCategoryItemUpdationEndpoint(this IEndpointRouteBuilder endpoints)
+ {
+ return endpoints.
+ MapPut("/{id:guid}", async (Guid id, UpdateCategoryItemCommand request, ISender mediator) =>
+ {
+ if (id != request.Id) return Results.BadRequest();
+ var response = await mediator.Send(request);
+ return Results.Ok(response);
+ })
+ .WithName(nameof(UpdateCategoryItemEndpoint))
+ .WithSummary("Updates a Category item")
+ .WithDescription("Updated a Category item")
+ .Produces(StatusCodes.Status200OK)
+ .RequirePermission("Permissions.CategoryItems.Update")
+ .MapToApiVersion(new ApiVersion(1, 0));
+
+ }
+}
diff --git a/src/Category/Features/Update/v1/UpdateCategoryItemHandler.cs b/src/Category/Features/Update/v1/UpdateCategoryItemHandler.cs
new file mode 100644
index 000000000..cd4a2a454
--- /dev/null
+++ b/src/Category/Features/Update/v1/UpdateCategoryItemHandler.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Category.Domain;
+using Category.Exceptions;
+using FSH.Framework.Core.Persistence;
+using MediatR;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Category.Features.Update.v1;
+
+public sealed class UpdateCategoryItemHandler(
+ ILogger logger,
+ [FromKeyedServices("categoryItem")] IRepository repository)
+ : IRequestHandler
+{
+ public async Task Handle(UpdateCategoryItemCommand request, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+ var categoryItem = await repository.GetByIdAsync(request.Id, cancellationToken);
+ _ = categoryItem ?? throw new CategoryItemNotFoundException(request.Id);
+ var updatedCategoryItem = categoryItem.Update(request.Name, request.Description);
+ await repository.UpdateAsync(updatedCategoryItem, cancellationToken);
+ logger.LogInformation("categoryItem item updated {CategoryItemItemId}", updatedCategoryItem.Id);
+ return new UpdateCategoryItemResponse(updatedCategoryItem.Id);
+ }
+}
diff --git a/src/Category/Features/Update/v1/UpdateCategoryItemResponse.cs b/src/Category/Features/Update/v1/UpdateCategoryItemResponse.cs
new file mode 100644
index 000000000..11b7bdb07
--- /dev/null
+++ b/src/Category/Features/Update/v1/UpdateCategoryItemResponse.cs
@@ -0,0 +1,9 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Category.Features.Update.v1;
+
+public record UpdateCategoryItemResponse(Guid? Id);
diff --git a/src/Category/Features/Update/v1/UpdateCategoryItemValidator.cs b/src/Category/Features/Update/v1/UpdateCategoryItemValidator.cs
new file mode 100644
index 000000000..0d3eccaf6
--- /dev/null
+++ b/src/Category/Features/Update/v1/UpdateCategoryItemValidator.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Category.Persistence;
+using FluentValidation;
+
+namespace Category.Features.Update.v1;
+
+public class UpdateCategoryItemValidator : AbstractValidator
+{
+ public UpdateCategoryItemValidator(CategoryItemDbContext context)
+ {
+ RuleFor(p => p.Name).NotEmpty();
+ RuleFor(p => p.Description).NotEmpty();
+ }
+}
diff --git a/src/Category/Persistence/CategoryItemDbContext.cs b/src/Category/Persistence/CategoryItemDbContext.cs
new file mode 100644
index 000000000..4e76a995a
--- /dev/null
+++ b/src/Category/Persistence/CategoryItemDbContext.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection.Emit;
+using System.Text;
+using System.Threading.Tasks;
+using Category.Domain;
+using Finbuckle.MultiTenant.Abstractions;
+using FSH.Framework.Core.Persistence;
+using FSH.Framework.Infrastructure.Persistence;
+using FSH.Framework.Infrastructure.Tenant;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Options;
+using Shared.Constants;
+
+namespace Category.Persistence;
+
+public sealed class CategoryItemDbContext : FshDbContext
+{
+ public CategoryItemDbContext(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options, IPublisher publisher, IOptions settings)
+ : base(multiTenantContextAccessor, options, publisher, settings)
+ {
+ }
+
+ public DbSet CategoryItems { get; set; } = null!;
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ ArgumentNullException.ThrowIfNull(modelBuilder);
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(CategoryItemDbContext).Assembly);
+ modelBuilder.HasDefaultSchema(SchemaNames.CategoryItem);
+ }
+}
diff --git a/src/Category/Persistence/CategoryItemDbInitializer.cs b/src/Category/Persistence/CategoryItemDbInitializer.cs
new file mode 100644
index 000000000..a1d4c269b
--- /dev/null
+++ b/src/Category/Persistence/CategoryItemDbInitializer.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Category.Domain;
+using FSH.Framework.Core.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Category.Persistence;
+
+internal sealed class CategoryItemDbInitializer(
+ ILogger logger,
+ CategoryItemDbContext context) : IDbInitializer
+{
+ public async Task MigrateAsync(CancellationToken cancellationToken)
+ {
+ if ((await context.Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any())
+ {
+ await context.Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
+ logger.LogInformation("[{Tenant}] applied database migrations for CategoryItem module", context.TenantInfo!.Identifier);
+ }
+ }
+
+ public async Task SeedAsync(CancellationToken cancellationToken)
+ {
+ const string name = "Hello World!";
+ const string description = "This is your first task";
+ if (await context.CategoryItems.FirstOrDefaultAsync(t => t.Name == name, cancellationToken).ConfigureAwait(false) is null)
+ {
+ var categoryItem = CategoryItem.Create(name , description);
+ await context.CategoryItems.AddAsync(categoryItem, cancellationToken);
+ await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
+ logger.LogInformation("[{Tenant}] seeding default categoryItem data", context.TenantInfo!.Identifier);
+ }
+ }
+}
diff --git a/src/Category/Persistence/CategoryItemRepository.cs b/src/Category/Persistence/CategoryItemRepository.cs
new file mode 100644
index 000000000..870460681
--- /dev/null
+++ b/src/Category/Persistence/CategoryItemRepository.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Ardalis.Specification.EntityFrameworkCore;
+using Ardalis.Specification;
+using FSH.Framework.Core.Domain.Contracts;
+using FSH.Framework.Core.Persistence;
+using Mapster;
+
+namespace Category.Persistence;
+
+internal sealed class CategoryItemRepository : RepositoryBase, IReadRepository, IRepository
+ where T : class, IAggregateRoot
+{
+ public CategoryItemRepository(CategoryItemDbContext context)
+ : base(context)
+ {
+ }
+
+ // We override the default behavior when mapping to a dto.
+ // We're using Mapster's ProjectToType here to immediately map the result from the database.
+ // This is only done when no Selector is defined, so regular specifications with a selector also still work.
+ protected override IQueryable ApplySpecification(ISpecification specification) =>
+ specification.Selector is not null
+ ? base.ApplySpecification(specification)
+ : ApplySpecification(specification, false)
+ .ProjectToType();
+}
diff --git a/src/Category/Persistence/Configurations/CategoryItemConfiguration.cs b/src/Category/Persistence/Configurations/CategoryItemConfiguration.cs
new file mode 100644
index 000000000..9f31b838f
--- /dev/null
+++ b/src/Category/Persistence/Configurations/CategoryItemConfiguration.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Category.Domain;
+using Finbuckle.MultiTenant;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Category.Persistence.Configurations;
+
+internal sealed class CategoryItemConfiguration : 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/FSH.Starter.sln b/src/FSH.Starter.sln
index 904c59f77..21181d177 100644
--- a/src/FSH.Starter.sln
+++ b/src/FSH.Starter.sln
@@ -64,6 +64,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Auth", "Auth", "{F17769D7-0
GetToken.http = GetToken.http
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Category", "Category", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Category", "Category\Category.csproj", "{902D2705-E1A7-40C0-934A-436E66B2EED0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -254,6 +258,18 @@ Global
{49AA63BF-3DBA-4490-9470-5AE0EB7F49F0}.Release|x64.Build.0 = Release|Any CPU
{49AA63BF-3DBA-4490-9470-5AE0EB7F49F0}.Release|x86.ActiveCfg = Release|Any CPU
{49AA63BF-3DBA-4490-9470-5AE0EB7F49F0}.Release|x86.Build.0 = Release|Any CPU
+ {902D2705-E1A7-40C0-934A-436E66B2EED0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {902D2705-E1A7-40C0-934A-436E66B2EED0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {902D2705-E1A7-40C0-934A-436E66B2EED0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {902D2705-E1A7-40C0-934A-436E66B2EED0}.Debug|x64.Build.0 = Debug|Any CPU
+ {902D2705-E1A7-40C0-934A-436E66B2EED0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {902D2705-E1A7-40C0-934A-436E66B2EED0}.Debug|x86.Build.0 = Debug|Any CPU
+ {902D2705-E1A7-40C0-934A-436E66B2EED0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {902D2705-E1A7-40C0-934A-436E66B2EED0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {902D2705-E1A7-40C0-934A-436E66B2EED0}.Release|x64.ActiveCfg = Release|Any CPU
+ {902D2705-E1A7-40C0-934A-436E66B2EED0}.Release|x64.Build.0 = Release|Any CPU
+ {902D2705-E1A7-40C0-934A-436E66B2EED0}.Release|x86.ActiveCfg = Release|Any CPU
+ {902D2705-E1A7-40C0-934A-436E66B2EED0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -280,6 +296,8 @@ Global
{2119CE89-308D-4932-BFCE-8CDC0A05EB9E} = {D36E77BC-4568-4BC8-9506-1EFB7B1CD335}
{FE1B1E84-F993-4840-9CAB-9082EB523FDD} = {CE64E92B-E088-46FB-9028-7FB6B67DEC55}
{F17769D7-0E41-4E80-BDD4-282EBE7B5199} = {FE1B1E84-F993-4840-9CAB-9082EB523FDD}
+ {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {F3DF5AC5-8CDC-46D4-969D-1245A6880215}
+ {902D2705-E1A7-40C0-934A-436E66B2EED0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {EA8248C2-3877-4AF7-8777-A17E7881E030}
diff --git a/src/Shared/Authorization/FshPermissions.cs b/src/Shared/Authorization/FshPermissions.cs
index fad6676ee..dad9800a2 100644
--- a/src/Shared/Authorization/FshPermissions.cs
+++ b/src/Shared/Authorization/FshPermissions.cs
@@ -57,6 +57,14 @@ public static class FshPermissions
//audit
new("View Audit Trails", FshActions.View, FshResources.AuditTrails),
+
+ //categoryItems
+ new("View CategoryItems", FshActions.View, FshResources.CategoryItems, IsBasic: true),
+ new("Search CategoryItems", FshActions.Search, FshResources.CategoryItems, IsBasic: true),
+ new("Create CategoryItems", FshActions.Create, FshResources.CategoryItems),
+ new("Update CategoryItems", FshActions.Update, FshResources.CategoryItems),
+ new("Delete CategoryItems", FshActions.Delete, FshResources.CategoryItems),
+ new("Export CategoryItems", FshActions.Export, FshResources.CategoryItems),
];
public static IReadOnlyList All { get; } = new ReadOnlyCollection(AllPermissions);
diff --git a/src/Shared/Authorization/FshResources.cs b/src/Shared/Authorization/FshResources.cs
index e8d276c47..735e92a39 100644
--- a/src/Shared/Authorization/FshResources.cs
+++ b/src/Shared/Authorization/FshResources.cs
@@ -12,4 +12,5 @@ public static class FshResources
public const string Brands = nameof(Brands);
public const string Todos = nameof(Todos);
public const string AuditTrails = nameof(AuditTrails);
+ public const string CategoryItems = nameof(CategoryItems);
}
diff --git a/src/Shared/Constants/SchemaNames.cs b/src/Shared/Constants/SchemaNames.cs
index 6f6763a8b..dcbb651b8 100644
--- a/src/Shared/Constants/SchemaNames.cs
+++ b/src/Shared/Constants/SchemaNames.cs
@@ -4,4 +4,5 @@ public static class SchemaNames
public const string Todo = "todo";
public const string Catalog = "catalog";
public const string Tenant = "tenant";
+ public const string CategoryItem = "categoryItem";
}
diff --git a/src/api/server/Server.csproj b/src/api/server/Server.csproj
index 11c255c90..dd5563554 100644
--- a/src/api/server/Server.csproj
+++ b/src/api/server/Server.csproj
@@ -16,6 +16,7 @@
+