Skip to content

Commit

Permalink
added product getbyid function (#933)
Browse files Browse the repository at this point in the history
* added product getbyid

* renamed exception class

* removed handler for query

* Revert "removed handler for query"

This reverts commit 0fa4df0.

* removed product read handler

* reverting proj and settings file

* Added product get list method

* added update endpoint for product

* fixes to product update

* fix update mechanism

* Added delete endpoint for product

* changed response of delete to 204 and made endpoints async

* updated proj file

* Update launchSettings.json

* Update Server.csproj

---------

Co-authored-by: Vipul Malhotra <vipulmalhotra@192.168.1.3>
Co-authored-by: Mukesh Murugan <31455818+iammukeshm@users.noreply.github.com>
Co-authored-by: Mukesh Murugan <iammukeshm@gmail.com>
  • Loading branch information
4 people authored Jun 13, 2024
1 parent 93e3787 commit 9051e95
Show file tree
Hide file tree
Showing 23 changed files with 305 additions and 22 deletions.
26 changes: 6 additions & 20 deletions api/framework/Infrastructure/Behaviours/ValidationBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,22 @@
using MediatR;

namespace FSH.Framework.Infrastructure.Behaviours;
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
public class ValidationBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators) : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;

public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
private readonly IEnumerable<IValidator<TRequest>> _validators = validators;

public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(next);

if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);

var validationResults = await Task.WhenAll(
_validators.Select(v =>
v.ValidateAsync(context, cancellationToken))).ConfigureAwait(false);

var failures = validationResults
.Where(r => r.Errors.Count > 0)
.SelectMany(r => r.Errors)
.ToList();
var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList();

if (failures.Count > 0)
throw new ValidationException(failures);
}
return await next().ConfigureAwait(false);
return await next();
}
}
1 change: 1 addition & 0 deletions api/framework/Infrastructure/Persistence/FshDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
this.TenantNotSetMode = TenantNotSetMode.Overwrite;
int result = await base.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await PublishDomainEventsAsync().ConfigureAwait(false);
return result;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using MediatR;

namespace FSH.WebApi.Catalog.Application.Products.Delete.v1;
public sealed record DeleteProductCommand(
Guid Id) : IRequest;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using FSH.Framework.Core.Persistence;
using FSH.WebApi.Catalog.Domain;
using FSH.WebApi.Catalog.Domain.Exceptions;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace FSH.WebApi.Catalog.Application.Products.Delete.v1;
public sealed class DeleteProductHandler(
ILogger<DeleteProductHandler> logger,
[FromKeyedServices("catalog:products")] IRepository<Product> repository)
: IRequestHandler<DeleteProductCommand>
{
public async Task Handle(DeleteProductCommand request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var product = await repository.GetByIdAsync(request.Id, cancellationToken);
_ = product ?? throw new ProductNotFoundException(request.Id);
await repository.DeleteAsync(product, cancellationToken);
logger.LogInformation("product with id : {ProductId} deleted", product.Id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Microsoft.Extensions.DependencyInjection;
using FSH.WebApi.Catalog.Domain.Exceptions;
using FSH.Framework.Core.Persistence;
using FSH.Framework.Core.Caching;
using FSH.WebApi.Catalog.Domain;
using MediatR;

namespace FSH.WebApi.Catalog.Application.Products.Get.v1;
public sealed class GetProductHandler(
[FromKeyedServices("catalog:products")] IReadRepository<Product> repository,
ICacheService cache)
: IRequestHandler<GetProductRequest, GetProductResponse>
{
public async Task<GetProductResponse> Handle(GetProductRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var item = await cache.GetOrSetAsync(
$"product:{request.Id}",
async () =>
{
var productItem = await repository.GetByIdAsync(request.Id, cancellationToken);
if (productItem == null) throw new ProductNotFoundException(request.Id);
return new GetProductResponse(productItem.Id, productItem.Name, productItem.Description, productItem.Price);
},
cancellationToken: cancellationToken);
return item!;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using MediatR;

namespace FSH.WebApi.Catalog.Application.Products.Get.v1;
public class GetProductRequest : IRequest<GetProductResponse>
{
public Guid Id { get; set; }
public GetProductRequest(Guid id) => Id = id;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
namespace FSH.WebApi.Catalog.Application.Products.Get.v1;
public sealed record GetProductResponse(Guid? Id, string Name, string? Description, decimal Price);
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using FSH.WebApi.Catalog.Application.Products.Get.v1;
using Microsoft.Extensions.DependencyInjection;
using FSH.Framework.Core.Specifications;
using FSH.Framework.Core.Persistence;
using FSH.WebApi.Catalog.Domain;
using FSH.Framework.Core.Paging;
using MediatR;


namespace FSH.WebApi.Catalog.Application.Products.GetList.v1;
public sealed class GetProductListHandler(
[FromKeyedServices("catalog:products")] IReadRepository<Product> repository)
: IRequestHandler<GetProductListRequest, PagedList<GetProductResponse>>
{
public async Task<PagedList<GetProductResponse>> Handle(GetProductListRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
var spec = new ListSpecification<Product, GetProductResponse>(request.PageNumber, request.PageSize);
var items = await repository.PaginatedListAsync(spec, request.PageNumber, request.PageSize, cancellationToken).ConfigureAwait(false);
return items;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using FSH.WebApi.Catalog.Application.Products.Get.v1;
using FSH.Framework.Core.Paging;
using MediatR;

namespace FSH.WebApi.Catalog.Application.Products.GetList.v1;

public record GetProductListRequest(int PageNumber, int PageSize) : IRequest<PagedList<GetProductResponse>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using MediatR;

namespace FSH.WebApi.Catalog.Application.Products.Update.v1;
public sealed record UpdateProductCommand(
Guid Id,
string? Name,
decimal Price,
string? Description = null) : IRequest<UpdateProductResponse>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using FluentValidation;

namespace FSH.WebApi.Catalog.Application.Products.Update.v1;
public class UpdateProductCommandValidator : AbstractValidator<UpdateProductCommand>
{
public UpdateProductCommandValidator()
{
RuleFor(p => p.Name).NotEmpty().MinimumLength(2).MaximumLength(75);
RuleFor(p => p.Price).GreaterThan(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using FSH.Framework.Core.Persistence;
using FSH.WebApi.Catalog.Domain;
using FSH.WebApi.Catalog.Domain.Exceptions;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace FSH.WebApi.Catalog.Application.Products.Update.v1;
public sealed class UpdateProductHandler(
ILogger<UpdateProductHandler> logger,
[FromKeyedServices("catalog:products")] IRepository<Product> repository)
: IRequestHandler<UpdateProductCommand, UpdateProductResponse>
{
public async Task<UpdateProductResponse> Handle(UpdateProductCommand request, CancellationToken cancellationToken)
{
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);
await repository.UpdateAsync(updatedProduct, cancellationToken);
logger.LogInformation("product with id : {ProductId} updated.", product.Id);
return new UpdateProductResponse(product.Id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
namespace FSH.WebApi.Catalog.Application.Products.Update.v1;
public sealed record UpdateProductResponse(Guid? Id);
7 changes: 7 additions & 0 deletions api/modules/Catalog/Catalog.Domain/Events/ProductUpdated.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using FSH.Framework.Core.Domain.Events;

namespace FSH.WebApi.Catalog.Domain.Events;
public sealed record ProductUpdated : DomainEvent
{
public Product? Product { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using FSH.Framework.Core.Exceptions;

namespace FSH.WebApi.Catalog.Domain.Exceptions;
public sealed class ProductNotFoundException : NotFoundException
{
public ProductNotFoundException(Guid id)
: base($"product with id {id} not found")
{
}
}
25 changes: 25 additions & 0 deletions api/modules/Catalog/Catalog.Domain/Product.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,29 @@ public static Product Create(string name, string? description, decimal price)

return product;
}

public Product Update(string? name, string? description, decimal? price)
{
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;

this.QueueDomainEvent(new ProductUpdated() { Product = this });
return this;
}

public static Product Update(Guid id, string name, string? description, decimal price)
{
var product = new Product
{
Id = id,
Name = name,
Description = description,
Price = price
};

product.QueueDomainEvent(new ProductUpdated() { Product = product });

return product;
}
}
4 changes: 4 additions & 0 deletions api/modules/Catalog/Catalog.Infrastructure/CatalogModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ public override void AddRoutes(IEndpointRouteBuilder app)
{
var productGroup = app.MapGroup("products").WithTags("products");
productGroup.MapProductCreationEndpoint();
productGroup.MapGetProductEndpoint();
productGroup.MapGetProductListEndpoint();
productGroup.MapProductUpdateEndpoint();
productGroup.MapProductDeleteEndpoint();
}
}
public static WebApplicationBuilder RegisterCatalogServices(this WebApplicationBuilder builder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ public static class CreateProductEndpoint
internal static RouteHandlerBuilder MapProductCreationEndpoint(this IEndpointRouteBuilder endpoints)
{
return endpoints
.MapPost("/", (CreateProductCommand request, ISender mediator) => mediator.Send(request))
.MapPost("/", async (CreateProductCommand request, ISender mediator) =>
{
var response = await mediator.Send(request);
return Results.Ok(response);
})
.WithName(nameof(CreateProductEndpoint))
.WithSummary("creates a product")
.WithDescription("creates a product")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using FSH.Framework.Infrastructure.Auth.Policy;
using FSH.WebApi.Catalog.Application.Products.Delete.v1;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace FSH.WebApi.Catalog.Infrastructure.Endpoints.v1;
public static class DeleteProductEndpoint
{
internal static RouteHandlerBuilder MapProductDeleteEndpoint(this IEndpointRouteBuilder endpoints)
{
return endpoints
.MapDelete("/{id:guid}", async (Guid id, ISender mediator) =>
{
await mediator.Send(new DeleteProductCommand(id));
return Results.NoContent();
})
.WithName(nameof(DeleteProductEndpoint))
.WithSummary("deletes product by id")
.WithDescription("deletes product by id")
.Produces(StatusCodes.Status204NoContent)
.RequirePermission("Permissions.Products.Delete")
.MapToApiVersion(1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using FSH.Framework.Infrastructure.Auth.Policy;
using FSH.WebApi.Catalog.Application.Products.Get.v1;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace FSH.WebApi.Catalog.Infrastructure.Endpoints.v1;
public static class GetProductEndpoint
{
internal static RouteHandlerBuilder MapGetProductEndpoint(this IEndpointRouteBuilder endpoints)
{
return endpoints
.MapGet("/{id:guid}", async (Guid id, ISender mediator) =>
{
var response = await mediator.Send(new GetProductRequest(id));
return Results.Ok(response);
})
.WithName(nameof(GetProductEndpoint))
.WithSummary("gets product by id")
.WithDescription("gets prodct by id")
.Produces<GetProductResponse>()
.RequirePermission("Permissions.Products.View")
.MapToApiVersion(1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using FSH.Framework.Core.Paging;
using FSH.Framework.Infrastructure.Auth.Policy;
using FSH.WebApi.Catalog.Application.Products.Get.v1;
using FSH.WebApi.Catalog.Application.Products.GetList.v1;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace FSH.WebApi.Catalog.Infrastructure.Endpoints.v1;
public static class GetProductListEndpoint
{
internal static RouteHandlerBuilder MapGetProductListEndpoint(this IEndpointRouteBuilder endpoints)
{
return endpoints
.MapGet("/", async (ISender mediator, int pageNumber = 1, int pageSize = 10) =>
{
var response = await mediator.Send(new GetProductListRequest(pageNumber, pageSize));
return Results.Ok(response);
})
.WithName(nameof(GetProductListEndpoint))
.WithSummary("gets a list of products")
.WithDescription("gets a list of products")
.Produces<PagedList<GetProductResponse>>()
.RequirePermission("Permissions.Products.View")
.MapToApiVersion(1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using FSH.Framework.Infrastructure.Auth.Policy;
using FSH.WebApi.Catalog.Application.Products.Update.v1;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;

namespace FSH.WebApi.Catalog.Infrastructure.Endpoints.v1;
public static class UpdateProductEndpoint
{
internal static RouteHandlerBuilder MapProductUpdateEndpoint(this IEndpointRouteBuilder endpoints)
{
return endpoints
.MapPut("/{id:guid}", async (Guid id, UpdateProductCommand request, ISender mediator) =>
{
if (id != request.Id) return Results.BadRequest();
var response = await mediator.Send(request);
return Results.Ok(response);
})
.WithName(nameof(UpdateProductEndpoint))
.WithSummary("update a product")
.WithDescription("update a product")
.Produces<UpdateProductResponse>()
.RequirePermission("Permissions.Products.Update")
.MapToApiVersion(1);
}
}
2 changes: 1 addition & 1 deletion api/server/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
}
},
"$schema": "http://json.schemastore.org/launchsettings.json"
}
}

0 comments on commit 9051e95

Please sign in to comment.