Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Catalogue API: Implement the remaining CRUD operations #3

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
104 changes: 101 additions & 3 deletions postman/ProdTrack.postman_collection.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"info": {
"_postman_id": "41edf411-0886-48e8-a33f-4cc115dcca70",
"_postman_id": "3267fd2c-10ea-48b7-9858-815cb7c848c6",
"name": "ProdTrack",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "15705090"
"_exporter_id": "29345409"
},
"item": [
{
Expand All @@ -16,7 +16,7 @@
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"Name\": \"Generic Product\",\r\n \"Category\": [\"Clothes\", \"Accessories\"],\r\n \"Description\": \"Description of a generic product.\",\r\n \"ImageFile\": \"Generic Product Image File\",\r\n \"Price\": 199.99\r\n}",
"raw": "{\r\n \"Name\": \"Generic Product\",\r\n \"Category\": [\"Clothes\", \"Accessories\"],\r\n \"Price\": 199.99,\r\n \"PurchaseDate\": \"2024-11-05\",\r\n \"Description\": \"Description of a generic product.\"\r\n}",
"options": {
"raw": {
"language": "json"
Expand All @@ -34,6 +34,104 @@
}
},
"response": []
},
{
"name": "Get Products",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{catalogue_url}}/products",
"host": [
"{{catalogue_url}}"
],
"path": [
"products"
]
}
},
"response": []
},
{
"name": "Get Product By Id",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{catalogue_url}}/products/0192fe70-7998-483d-93d5-662363288acf",
"host": [
"{{catalogue_url}}"
],
"path": [
"products",
"0192fe70-7998-483d-93d5-662363288acf"
]
}
},
"response": []
},
{
"name": "Get Products By Category",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{catalogue_url}}/products/category/Clothes",
"host": [
"{{catalogue_url}}"
],
"path": [
"products",
"category",
"Clothes"
]
}
},
"response": []
},
{
"name": "Update Product",
"request": {
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\r\n \"Id\": \"0192fe73-a957-4a61-82c1-1192e9d81c86\",\r\n \"Name\": \"Updated Generic Product\",\r\n \"Category\": [\"Clothes\", \"Accessories\"],\r\n \"Price\": 169.69,\r\n \"PurchaseDate\": \"2024-11-05\",\r\n \"Description\": \"Description of a generic product.\"\r\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{catalogue_url}}/products",
"host": [
"{{catalogue_url}}"
],
"path": [
"products"
]
}
},
"response": []
},
{
"name": "Delete Product",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "{{catalogue_url}}/products/0192fe70-7998-483d-93d5-662363288acf",
"host": [
"{{catalogue_url}}"
],
"path": [
"products",
"0192fe70-7998-483d-93d5-662363288acf"
]
}
},
"response": []
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion src/BuildingBlocks/BuildingBlocks/CQRS/ICommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

public interface ICommand : ICommand<Unit> { }

public interface ICommand<out TResponse> : IRequest<TResponse> { }
public interface ICommand<out TResponse> : IRequest<TResponse> { }
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ public interface ICommandHandler<in TCommand> : ICommandHandler<TCommand, Unit>

public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
where TResponse : notnull { }
where TResponse : notnull { }
2 changes: 1 addition & 1 deletion src/BuildingBlocks/BuildingBlocks/CQRS/IQuery.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
namespace BuildingBlocks.Cqrs;

public interface IQuery<out TResponse> : IRequest<TResponse> where TResponse : notnull { }
public interface IQuery<out TResponse> : IRequest<TResponse> where TResponse : notnull { }
2 changes: 1 addition & 1 deletion src/BuildingBlocks/BuildingBlocks/CQRS/IQueryHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

public interface IQueryHandler<in TQuery, TResponse> : IRequestHandler<TQuery, TResponse>
where TQuery : IQuery<TResponse>
where TResponse : notnull { }
where TResponse : notnull { }
2 changes: 1 addition & 1 deletion src/BuildingBlocks/BuildingBlocks/GlobalUsing.cs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
global using MediatR;
global using MediatR;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Catalogue.Api.Exceptions;

public class ProductNotFoundException() : Exception("Product not found!");
3 changes: 2 additions & 1 deletion src/Services/Catalogue/Catalogue.Api/GlobalUsing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
global using Carter;
global using Marten;

global using Catalogue.Api.Exceptions;
global using Catalogue.Api.Models;
global using BuildingBlocks.Cqrs;
global using BuildingBlocks.Cqrs;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Catalogue.Api.Products.DeleteProduct;

internal class DeleteProductCommandHandler(IDocumentSession session, ILogger<DeleteProductCommandHandler> logger) : ICommandHandler<DeleteProductCommand, DeleteProductResult>
{
public async Task<DeleteProductResult> Handle(DeleteProductCommand command, CancellationToken cancellationToken)
{
logger.LogInformation("DeleteProductsCommandHandler.Handle called with {@Command}", command);

session.Delete<Product>(command.Id);
await session.SaveChangesAsync(cancellationToken);

return new DeleteProductResult(true);
}
}

public record DeleteProductCommand(Guid Id) : ICommand<DeleteProductResult>;
public record DeleteProductResult(bool IsSuccess);
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Catalogue.Api.Products.DeleteProduct;

public class DeleteProductEndpoint : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapDelete("/products/{id}",
async (Guid id, ISender sender) =>
{
var result = await sender.Send(new DeleteProductCommand(id));
var response = result.Adapt<DeleteProductResponse>();

return Results.Ok(response);
})
.WithName("DeleteProduct")
.Produces<DeleteProductResponse>()
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status404NotFound)
.WithSummary("Delete Product")
.WithDescription("Delete Product");
}
}

//public record DeleteProductRequest(Guid Id);
public record DeleteProductResponse(bool IsSuccess);
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Catalogue.Api.Products.GetProductById;

public class GetProductByIdEndpoint : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("/products/{id}",
async (Guid id, ISender sender) =>
{
var result = await sender.Send(new GetProductByIdQuery(id));
var response = result.Adapt<GetProductByIdResponse>();

return Results.Ok(response);
})
.WithName("GetProductsById")
.Produces<GetProductByIdResponse>()
.ProducesProblem(StatusCodes.Status400BadRequest)
.WithSummary("Get Products By Id")
.WithDescription("Get Products By Id");
}
}

//public record GetProductByIdRequest();
public record GetProductByIdResponse(Product Product);
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Catalogue.Api.Products.GetProductById;

internal class GetProductByIdQueryHandler(IDocumentSession session, ILogger<GetProductByIdQueryHandler> logger) : IQueryHandler<GetProductByIdQuery, GetProductByIdResult>
{
public async Task<GetProductByIdResult> Handle(GetProductByIdQuery query, CancellationToken cancellationToken)
{
logger.LogInformation("GetProductByIdQueryHandler.Handle called with {@Query}", query);
var product = await session.LoadAsync<Product>(query.Id, cancellationToken);

if (product is null)
throw new ProductNotFoundException();

return new GetProductByIdResult(product);
}
}

public record GetProductByIdQuery(Guid Id) : IQuery<GetProductByIdResult>;
public record GetProductByIdResult(Product Product);
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Catalogue.Api.Products.GetProducts;

public class GetProductsEndpoint : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("/products",
async (ISender sender) =>
{
var result = await sender.Send(new GetProductsQuery());
var response = result.Adapt<GetProductsResponse>();

return Results.Ok(response);
})
.WithName("GetProducts")
.Produces<GetProductsResponse>()
.ProducesProblem(StatusCodes.Status400BadRequest)
.WithSummary("Get Products")
.WithDescription("Get Products");
}
}

//public record GetProductsRequest();
public record GetProductsResponse(IEnumerable<Product> Products);
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Catalogue.Api.Products.GetProducts;

internal class GetProductsQueryHandler(IDocumentSession session, ILogger<GetProductsQueryHandler> logger) : IQueryHandler<GetProductsQuery, GetProductsResult>
{
public async Task<GetProductsResult> Handle(GetProductsQuery query, CancellationToken cancellationToken)
{
logger.LogInformation("GetProductsQueryHandler.Handle called with {@Query}", query);
var products = await session.Query<Product>().ToListAsync(cancellationToken);

return new GetProductsResult(products);
}
}

public record GetProductsQuery : IQuery<GetProductsResult>;
public record GetProductsResult(IEnumerable<Product> Products);
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Catalogue.Api.Products.GetProductsByCategory;

public class GetProductsByCategoryEndpoint : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
app.MapGet("/products/category/{category}",
async (string category, ISender sender) =>
{
var result = await sender.Send(new GetProductsByCategoryQuery(category));
var response = result.Adapt<GetProductsByCategoryResponse>();

return Results.Ok(response);
})
.WithName("GetProductsByCategory")
.Produces<GetProductsByCategoryResponse>()
.ProducesProblem(StatusCodes.Status400BadRequest)
.WithSummary("Get Products By Category")
.WithDescription("Get Products By Category");
}
}

//public record GetProductsByCategoryRequest();
public record GetProductsByCategoryResponse(IEnumerable<Product> Products);
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Catalogue.Api.Products.GetProductsByCategory;

internal class GetProductsByCategoryQueryHandler(IDocumentSession session, ILogger<GetProductsByCategoryQueryHandler> logger) : IQueryHandler<GetProductsByCategoryQuery, GetProductsByCategoryResult>
{
public async Task<GetProductsByCategoryResult> Handle(GetProductsByCategoryQuery query, CancellationToken cancellationToken)
{
logger.LogInformation("GetProductByCategoryQueryHandler.Handle called with {@Query}", query);

var products = await session.Query<Product>()
.Where(p => p.Category.Contains(query.Category))
.ToListAsync(cancellationToken);

return new GetProductsByCategoryResult(products);
}
}

public record GetProductsByCategoryQuery(string Category) : IQuery<GetProductsByCategoryResult>;
public record GetProductsByCategoryResult(IEnumerable<Product> Products);
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Catalogue.Api.Products.UpdateProduct;

internal class UpdateProductCommandHandler(IDocumentSession session, ILogger<UpdateProductCommandHandler> logger) : ICommandHandler<UpdateProductCommand, UpdateProductResult>
{
public async Task<UpdateProductResult> Handle(UpdateProductCommand command, CancellationToken cancellationToken)
{
logger.LogInformation("UpdateProductsCommandHandler.Handle called with {@Command}", command);
var product = await session.LoadAsync<Product>(command.Id, cancellationToken);

if (product is null)
throw new ProductNotFoundException();

product.Name = command.Name;
product.Category = command.Category;
product.Price = command.Price;
product.PurchaseDate = command.PurchaseDate;
product.Description = command.Description;

session.Update(product);
await session.SaveChangesAsync(cancellationToken);

return new UpdateProductResult(true);
}
}

public record UpdateProductCommand(
Guid Id,
string Name,
List<string> Category,
decimal Price,
DateTime PurchaseDate,
string Description) : ICommand<UpdateProductResult>;

public record UpdateProductResult(bool IsSuccess);
Loading