├── .dockerignore
├── .gitattributes
├── Empty
├── Warehouse.Api
│ ├── appsettings.Development.json
│ ├── Properties
│ │ └── launchSettings.json
│ ├── appsettings.json
│ ├── Warehouse.Api.csproj
│ ├── Middlewares
│ │ └── ExceptionHandling
│ │ │ ├── ExceptionToHttpStatusMapper.cs
│ │ │ └── ExceptionHandlingMiddleware.cs
│ ├── Migrations
│ │ ├── 20230205120130_Initial.cs
│ │ ├── WarehouseDBContextModelSnapshot.cs
│ │ └── 20230205120130_Initial.Designer.cs
│ ├── Core
│ │ └── Validation.cs
│ └── Program.cs
└── Warehouse.Api.Tests
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ ├── WarehouseTestWebApplicationFactory.cs
│ ├── Products
│ ├── GettingProductDetails
│ │ └── GetProductDetailsTests.cs
│ ├── RegisteringProduct
│ │ └── RegisterProductTests.cs
│ └── GettingProducts
│ │ └── GetProductsTests.cs
│ └── Warehouse.Api.Tests.csproj
├── Final
├── Warehouse.Api
│ ├── appsettings.Development.json
│ ├── Properties
│ │ └── launchSettings.json
│ ├── appsettings.json
│ ├── Program.cs
│ ├── Warehouse.Api.csproj
│ └── Middlewares
│ │ └── ExceptionHandling
│ │ ├── ExceptionToHttpStatusMapper.cs
│ │ └── ExceptionHandlingMiddleware.cs
├── Warehouse.Api.Tests
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ ├── WarehouseTestWebApplicationFactory.cs
│ ├── Products
│ │ ├── GettingProductDetails
│ │ │ └── GetProductDetailsTests.cs
│ │ ├── RegisteringProduct
│ │ │ └── RegisterProductTests.cs
│ │ └── GettingProducts
│ │ │ └── GetProductsTests.cs
│ └── Warehouse.Api.Tests.csproj
└── Warehouse
│ ├── Products
│ ├── Primitives
│ │ └── Primitives.cs
│ ├── ProductsRepository.cs
│ ├── GettingProductDetails
│ │ ├── Endpoint.cs
│ │ └── GetProductDetails.cs
│ ├── Product.cs
│ ├── GettingProducts
│ │ ├── Endpoint.cs
│ │ └── GetProductsEndpoint.cs
│ ├── RegisteringProduct
│ │ ├── RegisterProduct.cs
│ │ └── Endpoint.cs
│ └── Configuration.cs
│ ├── Core
│ ├── Entities
│ │ └── EntitiesExtensions.cs
│ └── Validation.cs
│ ├── Warehouse.csproj
│ ├── Configuration.cs
│ ├── Migrations
│ ├── 20230205120130_Initial.cs
│ ├── WarehouseDBContextModelSnapshot.cs
│ └── 20230205120130_Initial.Designer.cs
│ └── Storage
│ └── WarehouseDBContext.cs
├── Mediator
├── Warehouse.Api
│ ├── appsettings.Development.json
│ ├── Requests
│ │ └── ProductsRequests.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── appsettings.json
│ ├── Program.cs
│ ├── Controllers
│ │ ├── ProductsCommandsController.cs
│ │ └── ProductsQueriesController.cs
│ ├── Warehouse.Api.csproj
│ └── Middlewares
│ │ └── ExceptionHandling
│ │ ├── ExceptionToHttpStatusMapper.cs
│ │ └── ExceptionHandlingMiddleware.cs
├── Warehouse.Api.Tests
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ ├── WarehouseTestWebApplicationFactory.cs
│ ├── Products
│ │ ├── GettingProductDetails
│ │ │ └── GetProductDetailsTests.cs
│ │ ├── RegisteringProduct
│ │ │ └── RegisterProductTests.cs
│ │ └── GettingProducts
│ │ │ └── GetProductsTests.cs
│ └── Warehouse.Api.Tests.csproj
└── Warehouse
│ ├── Products
│ ├── ValueObjects
│ │ └── Primitives.cs
│ ├── QueryHandlers
│ │ ├── GetProductDetailsHandler.cs
│ │ ├── GetProductsHandler.cs
│ │ └── Queries.cs
│ ├── CommandHandlers
│ │ ├── Commands.cs
│ │ └── RegisterProductHandler.cs
│ ├── Configuration.cs
│ ├── Product.cs
│ └── Repositories
│ │ └── ProductsRepository.cs
│ ├── Migrations
│ ├── 20230205120130_Initial.cs
│ ├── WarehouseDBContextModelSnapshot.cs
│ └── 20230205120130_Initial.Designer.cs
│ ├── Warehouse.csproj
│ ├── Configuration.cs
│ ├── Core
│ ├── Commands.cs
│ ├── Queries.cs
│ └── Validation.cs
│ └── Storage
│ └── WarehouseDBContext.cs
├── ApplicationService
├── Warehouse.Api
│ ├── appsettings.Development.json
│ ├── Requests
│ │ └── ProductsRequests.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── appsettings.json
│ ├── Program.cs
│ ├── Controllers
│ │ ├── ProductsCommandsController.cs
│ │ └── ProductsQueriesController.cs
│ ├── Warehouse.Api.csproj
│ └── Middlewares
│ │ └── ExceptionHandling
│ │ ├── ExceptionToHttpStatusMapper.cs
│ │ └── ExceptionHandlingMiddleware.cs
├── Warehouse.Api.Tests
│ ├── appsettings.Development.json
│ ├── appsettings.json
│ ├── WarehouseTestWebApplicationFactory.cs
│ ├── Products
│ │ ├── GettingProductDetails
│ │ │ └── GetProductDetailsTests.cs
│ │ ├── RegisteringProduct
│ │ │ └── RegisterProductTests.cs
│ │ └── GettingProducts
│ │ │ └── GetProductsTests.cs
│ └── Warehouse.Api.Tests.csproj
└── Warehouse
│ ├── Products
│ ├── Primitives.cs
│ ├── ProductsRepository.cs
│ ├── Commands.cs
│ ├── Queries.cs
│ ├── ProductsCommandService.cs
│ ├── Product.cs
│ ├── Configuration.cs
│ └── ProductsQueryService.cs
│ ├── Core
│ ├── EntitiesExtensions.cs
│ └── Validation.cs
│ ├── Migrations
│ ├── 20230205120130_Initial.cs
│ ├── WarehouseDBContextModelSnapshot.cs
│ └── 20230205120130_Initial.Designer.cs
│ ├── Warehouse.csproj
│ ├── Configuration.cs
│ └── Storage
│ └── WarehouseDBContext.cs
├── Directory.Build.props
├── Core.Build.props
├── .github
├── FUNDING.yml
└── workflows
│ ├── build.docker.yml
│ └── build.dotnet.yml
├── docker-compose.yml
├── Dockerfile
├── README.md
├── .gitignore
├── .editorconfig
└── Warehouse.sln
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/bin/
2 | **/obj/
3 | **/out/
4 | **/TestResults/
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 | *.png binary
3 | *.ttf binary
4 | *.jpg binary
5 | *.jpeg binary
6 | *.pdf binary
--------------------------------------------------------------------------------
/Empty/Warehouse.Api/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api.Tests/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api.Tests/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api.Tests/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api.Tests/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | enable
5 | true
6 | enable
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api/Requests/ProductsRequests.cs:
--------------------------------------------------------------------------------
1 | namespace Warehouse.Api.Requests;
2 |
3 | public record RegisterProductRequest(
4 | string SKU,
5 | string Name,
6 | string? Description
7 | );
8 |
9 | public record GetProductsRequest(
10 | string? Filter,
11 | int? Page,
12 | int? PageSize
13 | );
14 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api/Requests/ProductsRequests.cs:
--------------------------------------------------------------------------------
1 | namespace Warehouse.Api.Requests;
2 |
3 | public record RegisterProductRequest(
4 | string SKU,
5 | string Name,
6 | string? Description
7 | );
8 |
9 | public record GetProductsRequest(
10 | string? Filter,
11 | int? Page,
12 | int? PageSize
13 | );
14 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Products/Primitives.cs:
--------------------------------------------------------------------------------
1 | using Warehouse.Core;
2 |
3 | namespace Warehouse.Products;
4 |
5 | public readonly record struct ProductId(Guid Value)
6 | {
7 | public static ProductId From(Guid? productId) =>
8 | new(productId.AssertNotEmpty());
9 | }
10 |
11 | public record SKU(string Value)
12 | {
13 | public static SKU From(string? sku) =>
14 | new(sku.AssertMatchesRegex("[A-Z]{2,4}[0-9]{4,18}"));
15 | }
16 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Products/ValueObjects/Primitives.cs:
--------------------------------------------------------------------------------
1 | using Warehouse.Core;
2 |
3 | namespace Warehouse.Products;
4 |
5 | public readonly record struct ProductId(Guid Value)
6 | {
7 | public static ProductId From(Guid? productId) =>
8 | new(productId.AssertNotEmpty());
9 | }
10 |
11 | public record SKU(string Value)
12 | {
13 | public static SKU From(string? sku) =>
14 | new(sku.AssertMatchesRegex("[A-Z]{2,4}[0-9]{4,18}"));
15 | }
16 |
--------------------------------------------------------------------------------
/Final/Warehouse/Products/Primitives/Primitives.cs:
--------------------------------------------------------------------------------
1 | using Warehouse.Core;
2 |
3 | namespace Warehouse.Products.Primitives;
4 |
5 | public readonly record struct ProductId(Guid Value)
6 | {
7 | public static ProductId From(Guid? productId) =>
8 | new(productId.AssertNotEmpty());
9 | }
10 |
11 | public record SKU(string Value)
12 | {
13 | public static SKU From(string? sku) =>
14 | new(sku.AssertMatchesRegex("[A-Z]{2,4}[0-9]{4,18}"));
15 | }
16 |
--------------------------------------------------------------------------------
/Final/Warehouse/Products/ProductsRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Warehouse.Products.Primitives;
3 | using Warehouse.Storage;
4 |
5 | namespace Warehouse.Products;
6 |
7 | internal static class ProductsRepository
8 | {
9 | public static ValueTask ProductWithSKUExists(this WarehouseDBContext dbContext, SKU productSKU, CancellationToken ct)
10 | => new (dbContext.Set().AnyAsync(product => product.Sku.Value == productSKU.Value, ct));
11 | }
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Products/ProductsRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Warehouse.Storage;
3 |
4 | namespace Warehouse.Products;
5 |
6 | internal static class ProductsRepository
7 | {
8 | public static ValueTask ProductWithSKUExists(
9 | this WarehouseDBContext dbContext,
10 | SKU productSKU,
11 | CancellationToken ct
12 | ) =>
13 | new(dbContext.Set().AnyAsync(product => product.Sku.Value == productSKU.Value, ct));
14 | }
15 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "Products.Api": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": "true",
7 | "launchBrowser": true,
8 | "launchUrl": "api/products",
9 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
10 | "environmentVariables": {
11 | "ASPNETCORE_ENVIRONMENT": "Development"
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "Products.Api": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": "true",
7 | "launchBrowser": true,
8 | "launchUrl": "api/products",
9 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
10 | "environmentVariables": {
11 | "ASPNETCORE_ENVIRONMENT": "Development"
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "Products.Api": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": "true",
7 | "launchBrowser": true,
8 | "launchUrl": "api/products",
9 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
10 | "environmentVariables": {
11 | "ASPNETCORE_ENVIRONMENT": "Development"
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "profiles": {
4 | "Products.Api": {
5 | "commandName": "Project",
6 | "dotnetRunMessages": "true",
7 | "launchBrowser": true,
8 | "launchUrl": "api/products",
9 | "applicationUrl": "https://localhost:5001;http://localhost:5000",
10 | "environmentVariables": {
11 | "ASPNETCORE_ENVIRONMENT": "Development"
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "ConnectionStrings": {
10 | "WarehouseDB": "PORT = 5432; HOST = 127.0.0.1; TIMEOUT = 15; POOLING = True; MINPOOLSIZE = 1; MAXPOOLSIZE = 100; COMMANDTIMEOUT = 20; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres'"
11 | },
12 | "AllowedHosts": "*"
13 | }
14 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "ConnectionStrings": {
10 | "WarehouseDB": "PORT = 5432; HOST = 127.0.0.1; TIMEOUT = 15; POOLING = True; MINPOOLSIZE = 1; MAXPOOLSIZE = 100; COMMANDTIMEOUT = 20; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres'"
11 | },
12 | "AllowedHosts": "*"
13 | }
14 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api.Tests/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "ConnectionStrings": {
10 | "WarehouseDB": "PORT = 5432; HOST = 127.0.0.1; TIMEOUT = 15; POOLING = True; MINPOOLSIZE = 1; MAXPOOLSIZE = 100; COMMANDTIMEOUT = 20; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres'"
11 | },
12 | "AllowedHosts": "*"
13 | }
14 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api.Tests/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "ConnectionStrings": {
10 | "WarehouseDB": "PORT = 5432; HOST = 127.0.0.1; TIMEOUT = 15; POOLING = True; MINPOOLSIZE = 1; MAXPOOLSIZE = 100; COMMANDTIMEOUT = 20; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres'"
11 | },
12 | "AllowedHosts": "*"
13 | }
14 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api.Tests/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "ConnectionStrings": {
10 | "WarehouseDB": "PORT = 5432; HOST = 127.0.0.1; TIMEOUT = 15; POOLING = True; MINPOOLSIZE = 1; MAXPOOLSIZE = 100; COMMANDTIMEOUT = 20; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres';"
11 | },
12 | "AllowedHosts": "*"
13 | }
14 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api.Tests/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "ConnectionStrings": {
10 | "WarehouseDB": "PORT = 5432; HOST = 127.0.0.1; TIMEOUT = 15; POOLING = True; MINPOOLSIZE = 1; MAXPOOLSIZE = 100; COMMANDTIMEOUT = 20; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres';"
11 | },
12 | "AllowedHosts": "*"
13 | }
14 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "ConnectionStrings": {
10 | "WarehouseDB": "PORT = 5432; HOST = 127.0.0.1; TIMEOUT = 15; POOLING = True; MINPOOLSIZE = 1; MAXPOOLSIZE = 100; COMMANDTIMEOUT = 20; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres'; searchpath = 'public'"
11 | },
12 | "AllowedHosts": "*"
13 | }
14 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "ConnectionStrings": {
10 | "WarehouseDB": "PORT = 5432; HOST = 127.0.0.1; TIMEOUT = 15; POOLING = True; MINPOOLSIZE = 1; MAXPOOLSIZE = 100; COMMANDTIMEOUT = 20; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres'; searchpath = 'public'"
11 | },
12 | "AllowedHosts": "*"
13 | }
14 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Products/QueryHandlers/GetProductDetailsHandler.cs:
--------------------------------------------------------------------------------
1 | using Warehouse.Core;
2 |
3 | namespace Warehouse.Products;
4 |
5 | internal class GetProductDetailsHandler: IQueryHandler
6 | {
7 | private readonly IProductRepository repository;
8 |
9 | public GetProductDetailsHandler(IProductRepository repository) =>
10 | this.repository = repository;
11 |
12 | public ValueTask Handle(GetProductDetails query, CancellationToken ct) =>
13 | repository.GetProductDetails(query.ProductId, ct);
14 | }
15 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Products/QueryHandlers/GetProductsHandler.cs:
--------------------------------------------------------------------------------
1 | using Warehouse.Core;
2 |
3 | namespace Warehouse.Products;
4 |
5 | internal class GetProductsHandler: IQueryHandler>
6 | {
7 | private readonly IProductRepository repository;
8 |
9 | public GetProductsHandler(IProductRepository repository) =>
10 | this.repository = repository;
11 |
12 | public ValueTask> Handle(GetProducts query, CancellationToken ct) =>
13 | repository.GetProducts(query.Filter, query.Page, query.PageSize, ct);
14 | }
15 |
--------------------------------------------------------------------------------
/Core.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | true
5 | true
6 | latest
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Final/Warehouse/Core/Entities/EntitiesExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | namespace Warehouse.Core.Entities;
4 |
5 | public static class EntitiesExtensions
6 | {
7 | public static async ValueTask AddAndSave(this DbContext dbContext, T entity, CancellationToken ct)
8 | where T : notnull
9 | {
10 | await dbContext.AddAsync(entity, ct);
11 | await dbContext.SaveChangesAsync(ct);
12 | }
13 |
14 | public static ValueTask Find(this DbContext dbContext, TId id, CancellationToken ct)
15 | where T : class where TId : notnull
16 | => dbContext.FindAsync(new object[] {id}, ct);
17 | }
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Core/EntitiesExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | namespace Warehouse.Core;
4 |
5 | public static class EntitiesExtensions
6 | {
7 | public static async ValueTask AddAndSave(this DbContext dbContext, T entity, CancellationToken ct)
8 | where T : notnull
9 | {
10 | await dbContext.AddAsync(entity, ct);
11 | await dbContext.SaveChangesAsync(ct);
12 | }
13 |
14 | public static ValueTask Find(this DbContext dbContext, TId id, CancellationToken ct)
15 | where T : class where TId : notnull
16 | => dbContext.FindAsync(new object[] { id }, ct);
17 | }
18 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [oskardudycz]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # eventsourcingnetcore
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api/Program.cs:
--------------------------------------------------------------------------------
1 | using Warehouse;
2 | using Warehouse.Api.Middlewares.ExceptionHandling;
3 |
4 | var builder = WebApplication.CreateBuilder(args);
5 |
6 | builder.Services
7 | .AddWarehouseServices()
8 | .AddEndpointsApiExplorer()
9 | .AddSwaggerGen()
10 | .AddControllers();
11 |
12 | var app = builder.Build();
13 |
14 | app.UseExceptionHandlingMiddleware()
15 | .UseRouting()
16 | .UseEndpoints(endpoints =>
17 | endpoints.MapControllers()
18 | )
19 | .ConfigureWarehouse()
20 | .UseSwagger()
21 | .UseSwaggerUI();
22 |
23 | app.Run();
24 |
25 | namespace Warehouse.Api
26 | {
27 | public partial class Program
28 | {
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api/Program.cs:
--------------------------------------------------------------------------------
1 | using Warehouse;
2 | using Warehouse.Api.Middlewares.ExceptionHandling;
3 |
4 | var builder = WebApplication.CreateBuilder(args);
5 |
6 | builder.Services
7 | .AddWarehouseServices()
8 | .AddEndpointsApiExplorer()
9 | .AddSwaggerGen()
10 | .AddControllers();
11 |
12 | var app = builder.Build();
13 |
14 | app.UseExceptionHandlingMiddleware()
15 | .UseRouting()
16 | .UseEndpoints(endpoints =>
17 | endpoints.MapControllers()
18 | )
19 | .ConfigureWarehouse()
20 | .UseSwagger()
21 | .UseSwaggerUI();
22 |
23 | app.Run();
24 |
25 | namespace Warehouse.Api
26 | {
27 | public partial class Program
28 | {
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api/Program.cs:
--------------------------------------------------------------------------------
1 | using Warehouse;
2 | using Warehouse.Api.Middlewares.ExceptionHandling;
3 |
4 | var builder = WebApplication.CreateBuilder(args);
5 |
6 | builder.Services
7 | .AddRouting()
8 | .AddWarehouseServices()
9 | .AddEndpointsApiExplorer()
10 | .AddSwaggerGen();
11 |
12 | var app = builder.Build();
13 |
14 | app.UseExceptionHandlingMiddleware()
15 | .UseRouting()
16 | .UseEndpoints(endpoints =>
17 | {
18 | endpoints.UseWarehouseEndpoints();
19 | })
20 | .ConfigureWarehouse()
21 | .UseSwagger()
22 | .UseSwaggerUI();
23 |
24 | app.Run();
25 |
26 | namespace Warehouse.Api
27 | {
28 | public partial class Program
29 | {
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Products/Commands.cs:
--------------------------------------------------------------------------------
1 | using Warehouse.Core;
2 |
3 | namespace Warehouse.Products;
4 |
5 | public record RegisterProduct(
6 | ProductId ProductId,
7 | SKU SKU,
8 | string Name,
9 | string? Description
10 | )
11 | {
12 | public static RegisterProduct From(Guid id, string sku, string name, string? description) =>
13 | new(
14 | ProductId.From(id),
15 | SKU.From(sku),
16 | name.AssertNotEmpty(),
17 | description.AssertNullOrNotEmpty()
18 | );
19 | }
20 |
21 | public record DeactivateProduct(
22 | ProductId ProductId
23 | )
24 | {
25 | public static DeactivateProduct From(Guid? id) =>
26 | new(ProductId.From(id));
27 | }
28 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Products/CommandHandlers/Commands.cs:
--------------------------------------------------------------------------------
1 | using Warehouse.Core;
2 |
3 | namespace Warehouse.Products;
4 |
5 | public record RegisterProduct(
6 | ProductId ProductId,
7 | SKU SKU,
8 | string Name,
9 | string? Description
10 | )
11 | {
12 | public static RegisterProduct From(Guid id, string sku, string name, string? description) =>
13 | new(
14 | ProductId.From(id),
15 | SKU.From(sku),
16 | name.AssertNotEmpty(),
17 | description.AssertNullOrNotEmpty()
18 | );
19 | }
20 |
21 | public record DeactivateProduct(
22 | ProductId ProductId
23 | )
24 | {
25 | public static DeactivateProduct From(Guid? id) =>
26 | new(ProductId.From(id));
27 | }
28 |
--------------------------------------------------------------------------------
/.github/workflows/build.docker.yml:
--------------------------------------------------------------------------------
1 | name: Build Docker
2 |
3 | on: [ push ]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - name: Check Out Repo
11 | uses: actions/checkout@v3
12 |
13 | - name: Set up Docker Buildx
14 | id: buildx
15 | uses: docker/setup-buildx-action@v2
16 |
17 | - name: Build and push
18 | id: docker_build
19 | uses: docker/build-push-action@v4
20 | with:
21 | push: false
22 | tags: oskardudycz/cqrs_is_simpler_with_net:latest
23 |
24 | - name: Image digest
25 | run: echo ${{ steps.docker_build.outputs.digest }}
26 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Products/CommandHandlers/RegisterProductHandler.cs:
--------------------------------------------------------------------------------
1 | using Warehouse.Core;
2 |
3 | namespace Warehouse.Products;
4 |
5 | internal class RegisterProductHandler: ICommandHandler
6 | {
7 | private readonly IProductRepository repository;
8 |
9 | public RegisterProductHandler(IProductRepository repository) =>
10 | this.repository = repository;
11 |
12 | public async ValueTask Handle(RegisterProduct command, CancellationToken ct)
13 | {
14 | if (await repository.ProductWithSKUExists(command.SKU, ct))
15 | throw new InvalidOperationException(
16 | $"Product with SKU `{command.SKU} already exists.");
17 |
18 | var product = new Product(
19 | command.ProductId,
20 | command.SKU,
21 | command.Name,
22 | command.Description
23 | );
24 |
25 | await repository.Add(product, ct);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api/Controllers/ProductsCommandsController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Warehouse.Api.Requests;
3 | using Warehouse.Core;
4 | using Warehouse.Products;
5 |
6 | namespace Warehouse.Api.Controllers;
7 |
8 | [Route("api/products")]
9 | public class ProductsCommandsController: Controller
10 | {
11 | private readonly ICommandBus commandBus;
12 |
13 | public ProductsCommandsController(ICommandBus commandBus) =>
14 | this.commandBus = commandBus;
15 |
16 | [HttpPost]
17 | public async Task Register([FromBody] RegisterProductRequest request, CancellationToken ct)
18 | {
19 | var (sku, name, description) = request;
20 | var productId = Guid.NewGuid();
21 |
22 | var command = RegisterProduct.From(productId, sku, name, description);
23 |
24 | await commandBus.Send(command, ct);
25 |
26 | return Created($"api/products/{productId}", productId);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Products/Queries.cs:
--------------------------------------------------------------------------------
1 | using Warehouse.Core;
2 |
3 | namespace Warehouse.Products;
4 |
5 | public record GetProducts(string? Filter, int Page, int PageSize)
6 | {
7 | private const int DefaultPage = 1;
8 | private const int DefaultPageSize = 10;
9 |
10 | public static GetProducts From(string? filter, int? page, int? pageSize) =>
11 | new(
12 | filter,
13 | (page ?? DefaultPage).AssertPositive(),
14 | (pageSize ?? DefaultPageSize).AssertPositive()
15 | );
16 | }
17 |
18 | public record ProductListItem(
19 | Guid Id,
20 | string Sku,
21 | string Name
22 | );
23 |
24 | public record GetProductDetails(ProductId ProductId)
25 | {
26 | public static GetProductDetails From(Guid productId) =>
27 | new(ProductId.From(productId));
28 | }
29 |
30 | public record ProductDetails(
31 | Guid Id,
32 | string Sku,
33 | string Name,
34 | string? Description
35 | );
36 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Products/QueryHandlers/Queries.cs:
--------------------------------------------------------------------------------
1 | using Warehouse.Core;
2 |
3 | namespace Warehouse.Products;
4 |
5 | public record GetProducts(string? Filter, int Page, int PageSize)
6 | {
7 | private const int DefaultPage = 1;
8 | private const int DefaultPageSize = 10;
9 |
10 | public static GetProducts From(string? filter, int? page, int? pageSize) =>
11 | new(
12 | filter,
13 | (page ?? DefaultPage).AssertPositive(),
14 | (pageSize ?? DefaultPageSize).AssertPositive()
15 | );
16 | }
17 |
18 | public record ProductListItem(
19 | Guid Id,
20 | string Sku,
21 | string Name
22 | );
23 |
24 | public record GetProductDetails(ProductId ProductId)
25 | {
26 | public static GetProductDetails From(Guid productId) =>
27 | new(ProductId.From(productId));
28 | }
29 |
30 | public record ProductDetails(
31 | Guid Id,
32 | string Sku,
33 | string Name,
34 | string? Description
35 | );
36 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | #######################################################
4 | # Postgres
5 | #######################################################
6 | postgres:
7 | image: postgres:15.1-alpine
8 | container_name: postgres
9 | ports:
10 | - "5432:5432"
11 | environment:
12 | - POSTGRES_DB=postgres
13 | - POSTGRES_PASSWORD=Password12!
14 | networks:
15 | - pg_network
16 |
17 | pgadmin:
18 | image: dpage/pgadmin4
19 | container_name: pgadmin
20 | environment:
21 | PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@pgadmin.org}
22 | PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin}
23 | ports:
24 | - "${PGADMIN_PORT:-5050}:80"
25 | networks:
26 | - pg_network
27 |
28 | networks:
29 | pg_network:
30 | driver: bridge
31 |
32 | volumes:
33 | postgres:
34 | pgadmin:
35 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api/Controllers/ProductsCommandsController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Warehouse.Api.Requests;
3 | using Warehouse.Products;
4 |
5 | namespace Warehouse.Api.Controllers;
6 |
7 | [Route("api/products")]
8 | public class ProductsCommandsController: Controller
9 | {
10 | private readonly ProductsCommandService productsCommandService;
11 |
12 | public ProductsCommandsController(ProductsCommandService productsCommandService) =>
13 | this.productsCommandService = productsCommandService;
14 |
15 | [HttpPost]
16 | public async Task Register([FromBody] RegisterProductRequest request, CancellationToken ct)
17 | {
18 | var (sku, name, description) = request;
19 | var productId = Guid.NewGuid();
20 |
21 | var command = RegisterProduct.From(productId, sku, name, description);
22 |
23 | await productsCommandService.Handle(command, ct);
24 |
25 | return Created($"api/products/{productId}", productId);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api/Controllers/ProductsQueriesController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Warehouse.Api.Requests;
3 | using Warehouse.Products;
4 |
5 | namespace Warehouse.Api.Controllers;
6 |
7 | [Route("api/products")]
8 | public class ProductsQueriesController: Controller
9 | {
10 | private readonly ProductsQueryService queryService;
11 |
12 | public ProductsQueriesController(ProductsQueryService queryService) =>
13 | this.queryService = queryService;
14 |
15 | [HttpGet]
16 | public ValueTask> Get(
17 | [FromQuery] GetProductsRequest request,
18 | CancellationToken ct
19 | ) =>
20 | queryService.Handle(GetProducts.From(request.Filter, request.Page, request.PageSize), ct);
21 |
22 | [HttpGet("{id:guid}")]
23 | public async Task GetById([FromRoute] Guid id, CancellationToken ct)
24 | {
25 | var product = await queryService.Handle(GetProductDetails.From(id), ct);
26 |
27 | return product != null ? Ok(product) : NotFound();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.github/workflows/build.dotnet.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Check Out Repo
15 | uses: actions/checkout@v3
16 |
17 | - name: Start containers
18 | run: docker-compose -f "docker-compose.yml" up -d
19 |
20 | - name: Setup .NET Core
21 | uses: actions/setup-dotnet@v3
22 | with:
23 | dotnet-version: "7.0.x"
24 |
25 | - name: Restore NuGet packages
26 | run: dotnet restore
27 |
28 | - name: Build
29 | run: dotnet build --configuration Release --no-restore
30 |
31 | - name: Run tests
32 | run: dotnet test --configuration Release --no-build --filter Category!=SkipCI
33 |
34 | - name: Stop containers
35 | if: always()
36 | run: docker-compose -f "docker-compose.yml" down
37 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Products/Configuration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using Warehouse.Core;
4 |
5 | namespace Warehouse.Products;
6 |
7 | internal static class Configuration
8 | {
9 | public static IServiceCollection AddProductServices(this IServiceCollection services) =>
10 | services
11 | .AddTransient()
12 | .AddCommandHandler()
13 | .AddQueryHandler()
14 | .AddQueryHandler, GetProductsHandler>();
15 |
16 | public static void SetupProductsModel(this ModelBuilder modelBuilder)
17 | {
18 | var product = modelBuilder.Entity();
19 |
20 | product
21 | .Property(e => e.Id)
22 | .HasConversion(
23 | typed => typed.Value,
24 | plain => new ProductId(plain)
25 | );
26 |
27 | product
28 | .OwnsOne(e => e.Sku);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Final/Warehouse/Products/GettingProductDetails/Endpoint.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Http;
3 | using Microsoft.AspNetCore.Routing;
4 | using static Microsoft.AspNetCore.Http.Results;
5 |
6 | namespace Warehouse.Products.GettingProductDetails;
7 |
8 | internal static class GetProductDetailsEndpoint
9 | {
10 | internal static IEndpointRouteBuilder UseGetProductDetailsEndpoint(this IEndpointRouteBuilder endpoints)
11 | {
12 | endpoints
13 | .MapGet(
14 | "/api/products/{id:guid}",
15 | async (
16 | IQueryable queryable,
17 | Guid id,
18 | CancellationToken ct
19 | ) =>
20 | {
21 | var query = GetProductDetails.From(id);
22 |
23 | var result = await queryable.Query(query, ct);
24 |
25 | return result != null ? Ok(result) : NotFound();
26 | })
27 | .Produces()
28 | .Produces(StatusCodes.Status400BadRequest);
29 |
30 | return endpoints;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api/Controllers/ProductsQueriesController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Warehouse.Api.Requests;
3 | using Warehouse.Core;
4 | using Warehouse.Products;
5 |
6 | namespace Warehouse.Api.Controllers;
7 |
8 | [Route("api/products")]
9 | public class ProductsQueriesController: Controller
10 | {
11 | private readonly IQueryBus queryBus;
12 |
13 | public ProductsQueriesController(IQueryBus queryBus) =>
14 | this.queryBus = queryBus;
15 |
16 | [HttpGet]
17 | public ValueTask> Get(
18 | [FromQuery] GetProductsRequest request,
19 | CancellationToken ct
20 | ) =>
21 | queryBus.Query>(
22 | GetProducts.From(request.Filter, request.Page, request.PageSize), ct
23 | );
24 |
25 | [HttpGet("{id:guid}")]
26 | public async Task GetById([FromRoute] Guid id, CancellationToken ct)
27 | {
28 | var product = await queryBus.Query(GetProductDetails.From(id), ct);
29 |
30 | return product != null ? Ok(product) : NotFound();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Products/ProductsCommandService.cs:
--------------------------------------------------------------------------------
1 | namespace Warehouse.Products;
2 |
3 | public class ProductsCommandService
4 | {
5 | private readonly Func addProduct;
6 | private readonly Func> productWithSKUExists;
7 |
8 | internal ProductsCommandService(
9 | Func addProduct,
10 | Func> productWithSKUExists
11 | )
12 | {
13 | this.addProduct = addProduct;
14 | this.productWithSKUExists = productWithSKUExists;
15 | }
16 |
17 | public async Task Handle(RegisterProduct command, CancellationToken ct)
18 | {
19 | var product = new Product(
20 | command.ProductId,
21 | command.SKU,
22 | command.Name,
23 | command.Description
24 | );
25 |
26 | if (await productWithSKUExists(command.SKU, ct))
27 | throw new InvalidOperationException(
28 | $"Product with SKU `{command.SKU} already exists.");
29 |
30 | await addProduct(product, ct);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Final/Warehouse/Products/GettingProductDetails/GetProductDetails.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Warehouse.Products.Primitives;
3 |
4 | namespace Warehouse.Products.GettingProductDetails;
5 |
6 | public record GetProductDetails(ProductId ProductId)
7 | {
8 | public static GetProductDetails From(Guid productId) =>
9 | new(ProductId.From(productId));
10 | }
11 |
12 | public record ProductDetails(
13 | Guid Id,
14 | string Sku,
15 | string Name,
16 | string? Description
17 | );
18 |
19 | internal static class GetProductDetailsQuery
20 | {
21 | internal static async ValueTask Query(
22 | this IQueryable products,
23 | GetProductDetails query,
24 | CancellationToken ct
25 | )
26 | {
27 | var product = await products
28 | .SingleOrDefaultAsync(p => p.Id == query.ProductId, ct);
29 |
30 | if (product == null)
31 | return null;
32 |
33 | return new ProductDetails(
34 | product.Id.Value,
35 | product.Sku.Value,
36 | product.Name,
37 | product.Description
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Products/Product.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 |
3 | namespace Warehouse.Products;
4 |
5 | internal record Product
6 | {
7 | public required ProductId Id { get; init; }
8 |
9 | ///
10 | /// The Stock Keeping Unit (SKU), i.e. a merchant-specific identifier for a product or service, or the product to which the offer refers.
11 | ///
12 | ///
13 | public required SKU Sku { get; init; }
14 |
15 | ///
16 | /// Product Name
17 | ///
18 | public required string Name { get; init; }
19 |
20 | ///
21 | /// Optional Product description
22 | ///
23 | public string? Description { get; init; }
24 |
25 | // Note: this is needed because we're using SKU DTO.
26 | // It would work if we had just primitives
27 | // Should be fixed in .NET 6
28 | private Product() { }
29 |
30 | [SetsRequiredMembers]
31 | public Product(ProductId id, SKU sku, string name, string? description)
32 | {
33 | Id = id;
34 | Sku = sku;
35 | Name = name;
36 | Description = description;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Products/Product.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 |
3 | namespace Warehouse.Products;
4 |
5 | internal record Product
6 | {
7 | public required ProductId Id { get; init; }
8 |
9 | ///
10 | /// The Stock Keeping Unit (SKU), i.e. a merchant-specific identifier for a product or service, or the product to which the offer refers.
11 | ///
12 | ///
13 | public required SKU Sku { get; init; }
14 |
15 | ///
16 | /// Product Name
17 | ///
18 | public required string Name { get; init; }
19 |
20 | ///
21 | /// Optional Product description
22 | ///
23 | public string? Description { get; init; }
24 |
25 | // Note: this is needed because we're using SKU DTO.
26 | // It would work if we had just primitives
27 | // Should be fixed in .NET 6
28 | private Product() { }
29 |
30 | [SetsRequiredMembers]
31 | public Product(ProductId id, SKU sku, string name, string? description)
32 | {
33 | Id = id;
34 | Sku = sku;
35 | Name = name;
36 | Description = description;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Final/Warehouse/Products/Product.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using Warehouse.Products.Primitives;
3 |
4 | namespace Warehouse.Products;
5 |
6 | public record Product
7 | {
8 | public required ProductId Id { get; init; }
9 |
10 | ///
11 | /// The Stock Keeping Unit (SKU), i.e. a merchant-specific identifier for a product or service, or the product to which the offer refers.
12 | ///
13 | ///
14 | public required SKU Sku { get; init; }
15 |
16 | ///
17 | /// Product Name
18 | ///
19 | public required string Name { get; init; }
20 |
21 | ///
22 | /// Optional Product description
23 | ///
24 | public string? Description { get; init; }
25 |
26 | // Note: this is needed because we're using SKU DTO.
27 | // It would work if we had just primitives
28 | // Should be fixed in .NET 6
29 | private Product() { }
30 |
31 | [SetsRequiredMembers]
32 | public Product(ProductId id, SKU sku, string name, string? description)
33 | {
34 | Id = id;
35 | Sku = sku;
36 | Name = name;
37 | Description = description;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api/Warehouse.Api.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | all
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | <_Parameter1>$(MSBuildProjectName).Tests
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api/Warehouse.Api.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | all
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | <_Parameter1>$(MSBuildProjectName).Tests
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api/Warehouse.Api.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | all
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | <_Parameter1>$(MSBuildProjectName).Tests
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Final/Warehouse/Warehouse.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | <_Parameter1>$(MSBuildProjectName).Tests
14 |
15 |
16 | <_Parameter1>$(MSBuildProjectName).Api.Tests
17 |
18 |
19 |
20 |
21 |
22 |
23 | all
24 | runtime; build; native; contentfiles; analyzers; buildtransitive
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/Final/Warehouse/Configuration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Routing;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Warehouse.Products;
6 | using Warehouse.Storage;
7 |
8 | namespace Warehouse;
9 |
10 | public static class WarehouseConfiguration
11 | {
12 | public static IServiceCollection AddWarehouseServices(this IServiceCollection services)
13 | => services
14 | .AddDbContext(
15 | options => options.UseNpgsql("name=ConnectionStrings:WarehouseDB"))
16 | .AddProductServices();
17 |
18 | public static IEndpointRouteBuilder UseWarehouseEndpoints(this IEndpointRouteBuilder endpoints)
19 | => endpoints.UseProductsEndpoints();
20 |
21 | public static IApplicationBuilder ConfigureWarehouse(this IApplicationBuilder app)
22 | {
23 | var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";
24 |
25 | if (environment == "Development")
26 | {
27 | app.ApplicationServices.CreateScope().ServiceProvider
28 | .GetRequiredService().Database.Migrate();
29 | }
30 |
31 | return app;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api/Warehouse.Api.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | all
14 | runtime; build; native; contentfiles; analyzers; buildtransitive
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | <_Parameter1>$(MSBuildProjectName).Tests
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api/Middlewares/ExceptionHandling/ExceptionToHttpStatusMapper.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using System.Net;
3 |
4 | namespace Warehouse.Api.Middlewares.ExceptionHandling;
5 |
6 | public class HttpStatusCodeInfo
7 | {
8 | public HttpStatusCode Code { get; }
9 | public string Message { get; }
10 |
11 | public HttpStatusCodeInfo(HttpStatusCode code, string message)
12 | {
13 | Code = code;
14 | Message = message;
15 | }
16 | }
17 |
18 | public static class ExceptionToHttpStatusMapper
19 | {
20 | public static Func? CustomMap { get; set; }
21 |
22 | public static HttpStatusCodeInfo Map(Exception exception)
23 | {
24 | var code = exception switch
25 | {
26 | UnauthorizedAccessException _ => HttpStatusCode.Unauthorized,
27 | NotImplementedException _ => HttpStatusCode.NotImplemented,
28 | InvalidOperationException _ => HttpStatusCode.Conflict,
29 | ArgumentException _ => HttpStatusCode.BadRequest,
30 | ValidationException _ => HttpStatusCode.BadRequest,
31 | _ => CustomMap?.Invoke(exception) ?? HttpStatusCode.InternalServerError
32 | };
33 |
34 | return new HttpStatusCodeInfo(code, exception.Message);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api/Middlewares/ExceptionHandling/ExceptionToHttpStatusMapper.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using System.Net;
3 |
4 | namespace Warehouse.Api.Middlewares.ExceptionHandling;
5 |
6 | public class HttpStatusCodeInfo
7 | {
8 | public HttpStatusCode Code { get; }
9 | public string Message { get; }
10 |
11 | public HttpStatusCodeInfo(HttpStatusCode code, string message)
12 | {
13 | Code = code;
14 | Message = message;
15 | }
16 | }
17 |
18 | public static class ExceptionToHttpStatusMapper
19 | {
20 | public static Func? CustomMap { get; set; }
21 |
22 | public static HttpStatusCodeInfo Map(Exception exception)
23 | {
24 | var code = exception switch
25 | {
26 | UnauthorizedAccessException _ => HttpStatusCode.Unauthorized,
27 | NotImplementedException _ => HttpStatusCode.NotImplemented,
28 | InvalidOperationException _ => HttpStatusCode.Conflict,
29 | ArgumentException _ => HttpStatusCode.BadRequest,
30 | ValidationException _ => HttpStatusCode.BadRequest,
31 | _ => CustomMap?.Invoke(exception) ?? HttpStatusCode.InternalServerError
32 | };
33 |
34 | return new HttpStatusCodeInfo(code, exception.Message);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api/Middlewares/ExceptionHandling/ExceptionToHttpStatusMapper.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using System.Net;
3 |
4 | namespace Warehouse.Api.Middlewares.ExceptionHandling;
5 |
6 | public class HttpStatusCodeInfo
7 | {
8 | public HttpStatusCode Code { get; }
9 | public string Message { get; }
10 |
11 | public HttpStatusCodeInfo(HttpStatusCode code, string message)
12 | {
13 | Code = code;
14 | Message = message;
15 | }
16 | }
17 |
18 | public static class ExceptionToHttpStatusMapper
19 | {
20 | public static Func? CustomMap { get; set; }
21 |
22 | public static HttpStatusCodeInfo Map(Exception exception)
23 | {
24 | var code = exception switch
25 | {
26 | UnauthorizedAccessException _ => HttpStatusCode.Unauthorized,
27 | NotImplementedException _ => HttpStatusCode.NotImplemented,
28 | InvalidOperationException _ => HttpStatusCode.Conflict,
29 | ArgumentException _ => HttpStatusCode.BadRequest,
30 | ValidationException _ => HttpStatusCode.BadRequest,
31 | _ => CustomMap?.Invoke(exception) ?? HttpStatusCode.InternalServerError
32 | };
33 |
34 | return new HttpStatusCodeInfo(code, exception.Message);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api/Migrations/20230205120130_Initial.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | #nullable disable
5 |
6 | namespace Warehouse.Migrations
7 | {
8 | ///
9 | public partial class Initial : Migration
10 | {
11 | ///
12 | protected override void Up(MigrationBuilder migrationBuilder)
13 | {
14 | migrationBuilder.CreateTable(
15 | name: "Product",
16 | columns: table => new
17 | {
18 | Id = table.Column(type: "uuid", nullable: false),
19 | SkuValue = table.Column(name: "Sku_Value", type: "text", nullable: false),
20 | Name = table.Column(type: "text", nullable: false),
21 | Description = table.Column(type: "text", nullable: true)
22 | },
23 | constraints: table =>
24 | {
25 | table.PrimaryKey("PK_Product", x => x.Id);
26 | });
27 | }
28 |
29 | ///
30 | protected override void Down(MigrationBuilder migrationBuilder)
31 | {
32 | migrationBuilder.DropTable(
33 | name: "Product");
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Final/Warehouse/Migrations/20230205120130_Initial.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | #nullable disable
5 |
6 | namespace Warehouse.Migrations
7 | {
8 | ///
9 | public partial class Initial : Migration
10 | {
11 | ///
12 | protected override void Up(MigrationBuilder migrationBuilder)
13 | {
14 | migrationBuilder.CreateTable(
15 | name: "Product",
16 | columns: table => new
17 | {
18 | Id = table.Column(type: "uuid", nullable: false),
19 | SkuValue = table.Column(name: "Sku_Value", type: "text", nullable: false),
20 | Name = table.Column(type: "text", nullable: false),
21 | Description = table.Column(type: "text", nullable: true)
22 | },
23 | constraints: table =>
24 | {
25 | table.PrimaryKey("PK_Product", x => x.Id);
26 | });
27 | }
28 |
29 | ///
30 | protected override void Down(MigrationBuilder migrationBuilder)
31 | {
32 | migrationBuilder.DropTable(
33 | name: "Product");
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Final/Warehouse/Products/GettingProducts/Endpoint.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Http;
3 | using Microsoft.AspNetCore.Mvc;
4 | using Microsoft.AspNetCore.Routing;
5 | using static Microsoft.AspNetCore.Http.Results;
6 |
7 | namespace Warehouse.Products.GettingProducts;
8 |
9 | internal static class GetProductsEndpoint
10 | {
11 | internal static IEndpointRouteBuilder UseGetProductsEndpoint(this IEndpointRouteBuilder endpoints)
12 | {
13 | endpoints
14 | .MapGet(
15 | "/api/products",
16 | async (
17 | IQueryable products,
18 | [FromQuery] string? filter,
19 | [FromQuery] int? page,
20 | [FromQuery] int? pageSize,
21 | CancellationToken ct
22 | ) =>
23 | {
24 | var query = GetProducts.From(filter, page, pageSize);
25 |
26 | var result = await products.Query(query, ct);
27 |
28 | return Ok(result);
29 | })
30 | .Produces>()
31 | .Produces(StatusCodes.Status400BadRequest)
32 | .Produces(StatusCodes.Status404NotFound);
33 |
34 | return endpoints;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Migrations/20230205120130_Initial.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | #nullable disable
5 |
6 | namespace Warehouse.Migrations
7 | {
8 | ///
9 | public partial class Initial : Migration
10 | {
11 | ///
12 | protected override void Up(MigrationBuilder migrationBuilder)
13 | {
14 | migrationBuilder.CreateTable(
15 | name: "Product",
16 | columns: table => new
17 | {
18 | Id = table.Column(type: "uuid", nullable: false),
19 | SkuValue = table.Column(name: "Sku_Value", type: "text", nullable: false),
20 | Name = table.Column(type: "text", nullable: false),
21 | Description = table.Column(type: "text", nullable: true)
22 | },
23 | constraints: table =>
24 | {
25 | table.PrimaryKey("PK_Product", x => x.Id);
26 | });
27 | }
28 |
29 | ///
30 | protected override void Down(MigrationBuilder migrationBuilder)
31 | {
32 | migrationBuilder.DropTable(
33 | name: "Product");
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api/Middlewares/ExceptionHandling/ExceptionToHttpStatusMapper.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using System.Net;
3 |
4 | namespace Warehouse.Api.Middlewares.ExceptionHandling;
5 |
6 | public class HttpStatusCodeInfo
7 | {
8 | public HttpStatusCode Code { get; }
9 | public string Message { get; }
10 |
11 | public HttpStatusCodeInfo(HttpStatusCode code, string message)
12 | {
13 | Code = code;
14 | Message = message;
15 | }
16 | }
17 |
18 | public static class ExceptionToHttpStatusMapper
19 | {
20 | public static Func? CustomMap { get; set; }
21 |
22 | public static HttpStatusCodeInfo Map(Exception exception)
23 | {
24 | var code = exception switch
25 | {
26 | UnauthorizedAccessException _ => HttpStatusCode.Unauthorized,
27 | NotImplementedException _ => HttpStatusCode.NotImplemented,
28 | InvalidOperationException _ => HttpStatusCode.Conflict,
29 | ArgumentException _ => HttpStatusCode.BadRequest,
30 | ValidationException _ => HttpStatusCode.BadRequest,
31 | _ => CustomMap?.Invoke(exception) ?? HttpStatusCode.InternalServerError
32 | };
33 |
34 | return new HttpStatusCodeInfo(code, exception.Message);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Migrations/20230205120130_Initial.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | #nullable disable
5 |
6 | namespace Warehouse.Migrations
7 | {
8 | ///
9 | public partial class Initial : Migration
10 | {
11 | ///
12 | protected override void Up(MigrationBuilder migrationBuilder)
13 | {
14 | migrationBuilder.CreateTable(
15 | name: "Product",
16 | columns: table => new
17 | {
18 | Id = table.Column(type: "uuid", nullable: false),
19 | SkuValue = table.Column(name: "Sku_Value", type: "text", nullable: false),
20 | Name = table.Column(type: "text", nullable: false),
21 | Description = table.Column(type: "text", nullable: true)
22 | },
23 | constraints: table =>
24 | {
25 | table.PrimaryKey("PK_Product", x => x.Id);
26 | });
27 | }
28 |
29 | ///
30 | protected override void Down(MigrationBuilder migrationBuilder)
31 | {
32 | migrationBuilder.DropTable(
33 | name: "Product");
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Warehouse.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | <_Parameter1>$(MSBuildProjectName).Tests
14 |
15 |
16 | <_Parameter1>$(MSBuildProjectName).Api.Tests
17 |
18 |
19 |
20 |
21 |
22 |
23 | all
24 | runtime; build; native; contentfiles; analyzers; buildtransitive
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Warehouse.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | <_Parameter1>$(MSBuildProjectName).Tests
14 |
15 |
16 | <_Parameter1>$(MSBuildProjectName).Api.Tests
17 |
18 |
19 |
20 |
21 |
22 |
23 | all
24 | runtime; build; native; contentfiles; analyzers; buildtransitive
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Configuration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Warehouse.Products;
5 | using Warehouse.Storage;
6 |
7 | namespace Warehouse;
8 |
9 | public static class WarehouseConfiguration
10 | {
11 | public static IServiceCollection AddWarehouseServices(this IServiceCollection services)
12 | {
13 | return services
14 | .AddDbContext(
15 | options => options.UseNpgsql(
16 | "PORT = 5432; HOST = 127.0.0.1; TIMEOUT = 15; POOLING = True; MINPOOLSIZE = 1; MAXPOOLSIZE = 100; COMMANDTIMEOUT = 20; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres'; searchpath = 'public'"))
17 | .AddProductServices();
18 | }
19 |
20 | public static IApplicationBuilder ConfigureWarehouse(this IApplicationBuilder app)
21 | {
22 | var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";
23 |
24 | if (environment == "Development")
25 | {
26 | var dbContext = app.ApplicationServices.CreateScope().ServiceProvider
27 | .GetRequiredService();
28 |
29 | dbContext.Database.Migrate();
30 | }
31 |
32 | return app;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Products/Configuration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using Warehouse.Core;
4 | using Warehouse.Storage;
5 |
6 | namespace Warehouse.Products;
7 |
8 | internal static class Configuration
9 | {
10 | public static IServiceCollection AddProductServices(this IServiceCollection services) =>
11 | services
12 | .AddScoped(s =>
13 | {
14 | var dbContext = s.GetRequiredService();
15 | return new ProductsCommandService(dbContext.AddAndSave, dbContext.ProductWithSKUExists);
16 | })
17 | .AddScoped(s =>
18 | {
19 | var dbContext = s.GetRequiredService();
20 | return new ProductsQueryService(dbContext.Set().AsNoTracking());
21 | });
22 |
23 | public static void SetupProductsModel(this ModelBuilder modelBuilder)
24 | {
25 | var product = modelBuilder.Entity();
26 |
27 | product
28 | .Property(e => e.Id)
29 | .HasConversion(
30 | typed => typed.Value,
31 | plain => new ProductId(plain)
32 | );
33 |
34 | product
35 | .OwnsOne(e => e.Sku);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Final/Warehouse/Products/RegisteringProduct/RegisterProduct.cs:
--------------------------------------------------------------------------------
1 | using Warehouse.Core;
2 | using Warehouse.Products.Primitives;
3 |
4 | namespace Warehouse.Products.RegisteringProduct;
5 |
6 | internal static class RegisterProductHandler
7 | {
8 | internal static async Task Handle(
9 | Func addProduct,
10 | Func> productWithSKUExists,
11 | RegisterProduct command,
12 | CancellationToken ct
13 | )
14 | {
15 | var product = new Product(
16 | command.ProductId,
17 | command.SKU,
18 | command.Name,
19 | command.Description
20 | );
21 |
22 | if (await productWithSKUExists(command.SKU, ct))
23 | throw new InvalidOperationException(
24 | $"Product with SKU `{command.SKU} already exists.");
25 |
26 | await addProduct(product, ct);
27 | }
28 | }
29 |
30 | public record RegisterProduct(
31 | ProductId ProductId,
32 | SKU SKU,
33 | string Name,
34 | string? Description
35 | )
36 | {
37 | public static RegisterProduct From(Guid? id, string sku, string name, string? description) =>
38 | new(
39 | ProductId.From(id),
40 | SKU.From(sku),
41 | name.AssertNotEmpty(),
42 | description.AssertNullOrNotEmpty()
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/Final/Warehouse/Products/Configuration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Routing;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Warehouse.Products.GettingProductDetails;
5 | using Warehouse.Products.GettingProducts;
6 | using Warehouse.Products.Primitives;
7 | using Warehouse.Products.RegisteringProduct;
8 | using Warehouse.Storage;
9 |
10 | namespace Warehouse.Products;
11 |
12 | internal static class Configuration
13 | {
14 | public static IServiceCollection AddProductServices(this IServiceCollection services)
15 | => services
16 | .AddTransient(sp =>
17 | sp.GetRequiredService().Set().AsNoTracking()
18 | );
19 |
20 |
21 | public static IEndpointRouteBuilder UseProductsEndpoints(this IEndpointRouteBuilder endpoints) =>
22 | endpoints
23 | .UseRegisterProductEndpoint()
24 | .UseGetProductsEndpoint()
25 | .UseGetProductDetailsEndpoint();
26 |
27 | public static void SetupProductsModel(this ModelBuilder modelBuilder)
28 | {
29 | var product = modelBuilder.Entity();
30 |
31 | product
32 | .Property(e => e.Id)
33 | .HasConversion(
34 | typed => typed.Value,
35 | plain => new ProductId(plain)
36 | );
37 |
38 | product
39 | .OwnsOne(e => e.Sku);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Configuration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Warehouse.Core;
5 | using Warehouse.Products;
6 | using Warehouse.Storage;
7 |
8 | namespace Warehouse;
9 |
10 | public static class WarehouseConfiguration
11 | {
12 | public static IServiceCollection AddWarehouseServices(this IServiceCollection services)
13 | {
14 | return services
15 | .AddDbContext(
16 | options => options.UseNpgsql(
17 | "PORT = 5432; HOST = 127.0.0.1; TIMEOUT = 15; POOLING = True; MINPOOLSIZE = 1; MAXPOOLSIZE = 100; COMMANDTIMEOUT = 20; DATABASE = 'postgres'; PASSWORD = 'Password12!'; USER ID = 'postgres'; searchpath = 'public'"))
18 | .AddInMemoryCommandBus()
19 | .AddInMemoryQueryBus()
20 | .AddProductServices();
21 | }
22 |
23 | public static IApplicationBuilder ConfigureWarehouse(this IApplicationBuilder app)
24 | {
25 | var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";
26 |
27 | if (environment == "Development")
28 | {
29 | var dbContext = app.ApplicationServices.CreateScope().ServiceProvider
30 | .GetRequiredService();
31 |
32 | dbContext.Database.Migrate();
33 | }
34 |
35 | return app;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Final/Warehouse/Products/GettingProducts/GetProductsEndpoint.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Warehouse.Core;
3 |
4 | namespace Warehouse.Products.GettingProducts;
5 |
6 | public record GetProducts(string? Filter, int Page, int PageSize)
7 | {
8 | private const int DefaultPage = 1;
9 | private const int DefaultPageSize = 10;
10 |
11 | public static GetProducts From(string? filter, int? page, int? pageSize) =>
12 | new(
13 | filter,
14 | (page ?? DefaultPage).AssertPositive(),
15 | (pageSize ?? DefaultPageSize).AssertPositive()
16 | );
17 | }
18 |
19 | public record ProductListItem(
20 | Guid Id,
21 | string Sku,
22 | string Name
23 | );
24 |
25 | internal static class GetProductsQuery
26 | {
27 | internal static async ValueTask> Query(
28 | this IQueryable products,
29 | GetProducts query,
30 | CancellationToken ct
31 | )
32 | {
33 | var (filter, page, pageSize) = query;
34 |
35 | var filteredProducts = string.IsNullOrEmpty(filter)
36 | ? products
37 | : products
38 | .Where(p =>
39 | p.Sku.Value.Contains(query.Filter!) ||
40 | p.Name.Contains(query.Filter!) ||
41 | p.Description!.Contains(query.Filter!)
42 | );
43 |
44 | return await filteredProducts
45 | .Skip(pageSize * (page - 1))
46 | .Take(pageSize)
47 | .Select(p => new ProductListItem(p.Id.Value, p.Sku.Value, p.Name))
48 | .ToListAsync(ct);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api.Tests/WarehouseTestWebApplicationFactory.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.Testing;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Hosting;
6 | using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration;
7 |
8 | namespace Warehouse.Api.Tests;
9 |
10 | public class WarehouseTestWebApplicationFactory: WebApplicationFactory
11 | {
12 | private readonly string schemaName = Guid.NewGuid().ToString("N").ToLower();
13 |
14 | protected override IHost CreateHost(IHostBuilder builder)
15 | {
16 | builder.ConfigureServices(services =>
17 | {
18 | services
19 | .AddTransient(s =>
20 | {
21 | var connectionString = s.GetRequiredService()
22 | .GetConnectionString("WarehouseDB");
23 | var options = new DbContextOptionsBuilder();
24 | options.UseNpgsql(
25 | $"{connectionString}; searchpath = {schemaName.ToLower()}",
26 | x => x.MigrationsHistoryTable("__EFMigrationsHistory", schemaName.ToLower()));
27 | return options.Options;
28 | });
29 | });
30 |
31 | var host = base.CreateHost(builder);
32 |
33 | using var scope = host.Services.CreateScope();
34 | var database = scope.ServiceProvider.GetRequiredService().Database;
35 | database.ExecuteSqlRaw("TRUNCATE TABLE \"Product\"");
36 |
37 | return host;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api.Tests/WarehouseTestWebApplicationFactory.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.Testing;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Hosting;
6 | using Warehouse.Storage;
7 | using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration;
8 |
9 | namespace Warehouse.Api.Tests;
10 |
11 | public class WarehouseTestWebApplicationFactory: WebApplicationFactory
12 | {
13 | private readonly string schemaName = Guid.NewGuid().ToString("N").ToLower();
14 |
15 | protected override IHost CreateHost(IHostBuilder builder)
16 | {
17 | builder.ConfigureServices(services =>
18 | {
19 | services
20 | .AddTransient(s =>
21 | {
22 | var connectionString = s.GetRequiredService()
23 | .GetConnectionString("WarehouseDB");
24 | var options = new DbContextOptionsBuilder();
25 | options.UseNpgsql(
26 | $"{connectionString}; searchpath = {schemaName.ToLower()}",
27 | x => x.MigrationsHistoryTable("__EFMigrationsHistory", schemaName.ToLower()));
28 | return options.Options;
29 | });
30 | });
31 |
32 | var host = base.CreateHost(builder);
33 |
34 | using var scope = host.Services.CreateScope();
35 | var database = scope.ServiceProvider.GetRequiredService().Database;
36 | database.ExecuteSqlRaw("TRUNCATE TABLE \"Product\"");
37 |
38 | return host;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Final/Warehouse/Storage/WarehouseDBContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.EntityFrameworkCore.Design;
3 | using Microsoft.Extensions.Configuration;
4 | using Warehouse.Products;
5 |
6 | namespace Warehouse.Storage;
7 |
8 | public class WarehouseDBContext: DbContext
9 | {
10 | public WarehouseDBContext(DbContextOptions options)
11 | : base(options)
12 | {
13 | }
14 |
15 | protected override void OnModelCreating(ModelBuilder modelBuilder)
16 | {
17 | modelBuilder.SetupProductsModel();
18 | }
19 | }
20 |
21 | public class WarehouseDBContextFactory: IDesignTimeDbContextFactory
22 | {
23 | public WarehouseDBContext CreateDbContext(params string[] args)
24 | {
25 | var optionsBuilder = new DbContextOptionsBuilder();
26 |
27 | if (optionsBuilder.IsConfigured)
28 | return new WarehouseDBContext(optionsBuilder.Options);
29 |
30 | //Called by parameterless ctor Usually Migrations
31 | var environmentName = Environment.GetEnvironmentVariable("EnvironmentName") ?? "Development";
32 |
33 | var connectionString =
34 | new ConfigurationBuilder()
35 | .SetBasePath(AppContext.BaseDirectory)
36 | .AddJsonFile("appsettings.json")
37 | .AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: false)
38 | .AddEnvironmentVariables()
39 | .Build()
40 | .GetConnectionString("WarehouseDB");
41 |
42 | optionsBuilder.UseNpgsql(connectionString);
43 |
44 | return new WarehouseDBContext(optionsBuilder.Options);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api.Tests/WarehouseTestWebApplicationFactory.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.Testing;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Hosting;
6 | using Warehouse.Storage;
7 | using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration;
8 |
9 | namespace Warehouse.Api.Tests;
10 |
11 | public class WarehouseTestWebApplicationFactory: WebApplicationFactory
12 | {
13 | private readonly string schemaName = Guid.NewGuid().ToString("N").ToLower();
14 |
15 | protected override IHost CreateHost(IHostBuilder builder)
16 | {
17 | builder.ConfigureServices(services =>
18 | {
19 | services
20 | .AddTransient(s =>
21 | {
22 | var connectionString = s.GetRequiredService()
23 | .GetConnectionString("WarehouseDB");
24 | var options = new DbContextOptionsBuilder();
25 | options.UseNpgsql(
26 | $"{connectionString}; searchpath = {schemaName.ToLower()}",
27 | x => x.MigrationsHistoryTable("__EFMigrationsHistory", schemaName.ToLower()));
28 | return options.Options;
29 | });
30 | });
31 |
32 | var host = base.CreateHost(builder);
33 |
34 | using var scope = host.Services.CreateScope();
35 | var database = scope.ServiceProvider.GetRequiredService().Database;
36 | database.ExecuteSqlRaw("TRUNCATE TABLE \"Product\"");
37 |
38 | return host;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api.Tests/WarehouseTestWebApplicationFactory.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.Testing;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Hosting;
6 | using Warehouse.Storage;
7 | using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration;
8 |
9 | namespace Warehouse.Api.Tests;
10 |
11 | public class WarehouseTestWebApplicationFactory: WebApplicationFactory
12 | {
13 | private readonly string schemaName = Guid.NewGuid().ToString("N").ToLower();
14 |
15 | protected override IHost CreateHost(IHostBuilder builder)
16 | {
17 | builder.ConfigureServices(services =>
18 | {
19 | services
20 | .AddTransient(s =>
21 | {
22 | var connectionString = s.GetRequiredService()
23 | .GetConnectionString("WarehouseDB");
24 | var options = new DbContextOptionsBuilder();
25 | options.UseNpgsql(
26 | $"{connectionString}; searchpath = {schemaName.ToLower()}",
27 | x => x.MigrationsHistoryTable("__EFMigrationsHistory", schemaName.ToLower()));
28 | return options.Options;
29 | });
30 | });
31 |
32 | var host = base.CreateHost(builder);
33 |
34 | using var scope = host.Services.CreateScope();
35 | var database = scope.ServiceProvider.GetRequiredService().Database;
36 | database.ExecuteSqlRaw("TRUNCATE TABLE \"Product\"");
37 |
38 | return host;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Storage/WarehouseDBContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.EntityFrameworkCore.Design;
3 | using Microsoft.Extensions.Configuration;
4 | using Warehouse.Products;
5 |
6 | namespace Warehouse.Storage;
7 |
8 | public class WarehouseDBContext: DbContext
9 | {
10 | public WarehouseDBContext(DbContextOptions options)
11 | : base(options)
12 | {
13 | }
14 |
15 | protected override void OnModelCreating(ModelBuilder modelBuilder)
16 | {
17 | modelBuilder.SetupProductsModel();
18 | }
19 | }
20 |
21 | public class WarehouseDBContextFactory: IDesignTimeDbContextFactory
22 | {
23 | public WarehouseDBContext CreateDbContext(params string[] args)
24 | {
25 | var optionsBuilder = new DbContextOptionsBuilder();
26 |
27 | if (optionsBuilder.IsConfigured)
28 | return new WarehouseDBContext(optionsBuilder.Options);
29 |
30 | //Called by parameterless ctor Usually Migrations
31 | var environmentName = Environment.GetEnvironmentVariable("EnvironmentName") ?? "Development";
32 |
33 | var connectionString =
34 | new ConfigurationBuilder()
35 | .SetBasePath(AppContext.BaseDirectory)
36 | .AddJsonFile("appsettings.json")
37 | .AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: false)
38 | .AddEnvironmentVariables()
39 | .Build()
40 | .GetConnectionString("WarehouseDB");
41 |
42 | optionsBuilder.UseNpgsql(connectionString);
43 |
44 | return new WarehouseDBContext(optionsBuilder.Options);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Products/ProductsQueryService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | namespace Warehouse.Products;
4 |
5 | public class ProductsQueryService
6 | {
7 | private readonly IQueryable products;
8 |
9 | internal ProductsQueryService(IQueryable products) =>
10 | this.products = products;
11 |
12 | public async ValueTask> Handle(GetProducts query, CancellationToken ct)
13 | {
14 | var (filter, page, pageSize) = query;
15 |
16 | var filteredProducts = string.IsNullOrEmpty(filter)
17 | ? products
18 | : products
19 | .Where(p =>
20 | p.Sku.Value.Contains(query.Filter!) ||
21 | p.Name.Contains(query.Filter!) ||
22 | p.Description!.Contains(query.Filter!)
23 | );
24 |
25 | var result = await filteredProducts
26 | .Skip(pageSize * (page - 1))
27 | .Take(pageSize)
28 | .ToListAsync(ct);
29 |
30 | var sth = result
31 | .Select(p => new ProductListItem(p.Id.Value, p.Sku.Value, p.Name))
32 | .ToList();
33 |
34 | return sth;
35 | }
36 |
37 | public async ValueTask Handle(GetProductDetails query, CancellationToken ct)
38 | {
39 | var product = await products
40 | .SingleOrDefaultAsync(p => p.Id == query.ProductId, ct);
41 |
42 | if (product == null)
43 | return null;
44 |
45 | return new ProductDetails(
46 | product.Id.Value,
47 | product.Sku.Value,
48 | product.Name,
49 | product.Description
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Final/Warehouse/Products/RegisteringProduct/Endpoint.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Http;
3 | using Microsoft.AspNetCore.Routing;
4 | using Warehouse.Core.Entities;
5 | using Warehouse.Storage;
6 | using static Microsoft.AspNetCore.Http.Results;
7 | using static Warehouse.Products.RegisteringProduct.RegisterProductHandler;
8 |
9 | namespace Warehouse.Products.RegisteringProduct;
10 |
11 | public record RegisterProductRequest(
12 | string SKU,
13 | string Name,
14 | string? Description
15 | );
16 |
17 | internal static class RegisterProductEndpoint
18 | {
19 | internal static IEndpointRouteBuilder UseRegisterProductEndpoint(this IEndpointRouteBuilder endpoints)
20 | {
21 | endpoints.MapPost(
22 | "api/products/",
23 | async (
24 | WarehouseDBContext dbContext,
25 | RegisterProductRequest request,
26 | CancellationToken ct
27 | ) =>
28 | {
29 | var (sku, name, description) = request;
30 | var productId = Guid.NewGuid();
31 |
32 | var command = RegisterProduct.From(productId, sku, name, description);
33 |
34 | await Handle(
35 | dbContext.AddAndSave,
36 | dbContext.ProductWithSKUExists,
37 | command,
38 | ct
39 | );
40 |
41 | return Created($"/api/products/{productId}", productId);
42 | })
43 | .Produces(StatusCodes.Status201Created)
44 | .Produces(StatusCodes.Status400BadRequest);
45 |
46 | return endpoints;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Core/Commands.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 |
3 | namespace Warehouse.Core;
4 |
5 | public interface ICommandHandler
6 | {
7 | ValueTask Handle(T command, CancellationToken token);
8 | }
9 |
10 | public interface ICommandBus
11 | {
12 | ValueTask Send(TCommand command, CancellationToken ct);
13 | }
14 |
15 | public class InMemoryCommandBus: ICommandBus
16 | {
17 | private readonly IServiceProvider serviceProvider;
18 |
19 | public InMemoryCommandBus(IServiceProvider serviceProvider) =>
20 | this.serviceProvider = serviceProvider;
21 |
22 | public ValueTask Send(TCommand command, CancellationToken ct) =>
23 | serviceProvider.GetRequiredService>().Handle(command, ct);
24 | }
25 |
26 | public static class CommandHandlerConfiguration
27 | {
28 | public static IServiceCollection AddInMemoryCommandBus(this IServiceCollection services) =>
29 | services.AddScoped();
30 |
31 | public static IServiceCollection AddCommandHandler(
32 | this IServiceCollection services,
33 | Func? configure = null
34 | ) where TCommandHandler : class, ICommandHandler
35 | {
36 | if (configure == null)
37 | {
38 | services.AddTransient();
39 | services.AddTransient, TCommandHandler>();
40 | }
41 | else
42 | {
43 | services.AddTransient(configure);
44 | services.AddTransient, TCommandHandler>(configure);
45 | }
46 |
47 | return services;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Core/Queries.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 |
3 | namespace Warehouse.Core;
4 |
5 | public interface IQueryHandler
6 | {
7 | ValueTask Handle(TQuery query, CancellationToken ct);
8 | }
9 |
10 | public interface IQueryBus
11 | {
12 | ValueTask Query(TQuery query, CancellationToken ct);
13 | }
14 |
15 | public class InMemoryQueryBus: IQueryBus
16 | {
17 | private readonly IServiceProvider serviceProvider;
18 |
19 | public InMemoryQueryBus(IServiceProvider serviceProvider) =>
20 | this.serviceProvider = serviceProvider;
21 |
22 | public ValueTask Query(TQuery query, CancellationToken ct) =>
23 | serviceProvider.GetRequiredService>().Handle(query, ct);
24 | }
25 |
26 | public static class QueryHandlerConfiguration
27 | {
28 | public static IServiceCollection AddInMemoryQueryBus(this IServiceCollection services) =>
29 | services.AddScoped();
30 |
31 | public static IServiceCollection AddQueryHandler(
32 | this IServiceCollection services,
33 | Func? configure = null
34 | ) where TQueryHandler : class, IQueryHandler
35 | {
36 | if (configure == null)
37 | {
38 | services.AddTransient();
39 | services.AddTransient, TQueryHandler>();
40 | }
41 | else
42 | {
43 | services.AddTransient(configure);
44 | services.AddTransient, TQueryHandler>(configure);
45 | }
46 |
47 | return services;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Storage/WarehouseDBContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Microsoft.EntityFrameworkCore.Design;
3 | using Microsoft.Extensions.Configuration;
4 | using Warehouse.Products;
5 |
6 | namespace Warehouse.Storage;
7 |
8 | public class WarehouseDBContext: DbContext
9 | {
10 | public WarehouseDBContext(DbContextOptions options)
11 | : base(options)
12 | {
13 | }
14 |
15 | protected override void OnModelCreating(ModelBuilder modelBuilder)
16 | {
17 | modelBuilder.SetupProductsModel();
18 | }
19 | }
20 |
21 | public class WarehouseDBContextFactory: IDesignTimeDbContextFactory
22 | {
23 | public WarehouseDBContext CreateDbContext(params string[] args)
24 | {
25 | var optionsBuilder = new DbContextOptionsBuilder();
26 |
27 | if (optionsBuilder.IsConfigured)
28 | return new WarehouseDBContext(optionsBuilder.Options);
29 |
30 | //Called by parameterless ctor Usually Migrations
31 | var environmentName = Environment.GetEnvironmentVariable("EnvironmentName") ?? "Development";
32 |
33 | var connectionString =
34 | new ConfigurationBuilder()
35 | .SetBasePath(AppContext.BaseDirectory)
36 | .AddJsonFile("appsettings.json")
37 | .AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: false)
38 | .AddEnvironmentVariables()
39 | .Build()
40 | .GetConnectionString("WarehouseDB");
41 |
42 | optionsBuilder.UseNpgsql(connectionString);
43 |
44 | return new WarehouseDBContext(optionsBuilder.Options);
45 | }
46 |
47 | public static WarehouseDBContext From()
48 | => new WarehouseDBContextFactory().CreateDbContext();
49 | }
50 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api.Tests/Products/GettingProductDetails/GetProductDetailsTests.cs:
--------------------------------------------------------------------------------
1 | using Ogooreck.API;
2 | using static Ogooreck.API.ApiSpecification;
3 | using Xunit;
4 |
5 | namespace Warehouse.Api.Tests.Products.GettingProductDetails;
6 |
7 | public class GetProductDetailsTests: IClassFixture
8 | {
9 | private readonly GetProductDetailsFixture API;
10 |
11 | public GetProductDetailsTests(GetProductDetailsFixture api) => API = api;
12 |
13 | [Fact]
14 | public Task ValidRequest_With_NoParams_ShouldReturn_200() =>
15 | API.Given(URI($"/api/products/{API.ExistingProduct.Id}"))
16 | .When(GET)
17 | .Then(OK, RESPONSE_BODY(API.ExistingProduct));
18 |
19 | [Theory]
20 | [InlineData(12)]
21 | [InlineData("not-a-guid")]
22 | public Task InvalidGuidId_ShouldReturn_404(object invalidId) =>
23 | API.Given(URI($"/api/products/{invalidId}"))
24 | .When(GET)
25 | .Then(NOT_FOUND);
26 |
27 | [Fact]
28 | public Task NotExistingId_ShouldReturn_404() =>
29 | API.Given(URI($"/api/products/{Guid.NewGuid()}"))
30 | .When(GET)
31 | .Then(NOT_FOUND);
32 | }
33 |
34 | public class GetProductDetailsFixture: ApiSpecification, IAsyncLifetime
35 | {
36 | public ProductDetails ExistingProduct = default!;
37 |
38 | public GetProductDetailsFixture(): base(new WarehouseTestWebApplicationFactory()) { }
39 |
40 | public async Task InitializeAsync()
41 | {
42 | var registerProduct = new RegisterProductRequest("IN11111", "ValidName", "ValidDescription");
43 | var registerResponse = await Send(
44 | new ApiRequest(POST, URI("/api/products"), BODY(registerProduct))
45 | );
46 |
47 | await CREATED(registerResponse);
48 |
49 | var (sku, name, description) = registerProduct;
50 | ExistingProduct = new ProductDetails(registerResponse.GetCreatedId(), sku!, name!, description);
51 | }
52 |
53 | public Task DisposeAsync() => Task.CompletedTask;
54 | }
55 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ########################################
2 | # First stage of multistage build
3 | ########################################
4 | # Use Build image with label `builder
5 | ########################################
6 | FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS builder
7 |
8 | # Setup working directory for project
9 | WORKDIR /app
10 |
11 | COPY ./.editorconfig ./
12 | COPY ./Directory.Build.props ./
13 | COPY ./Core.Build.props ./
14 | COPY ./Final/Warehouse/Warehouse.csproj ./Final/Warehouse/
15 | COPY ./Final/Warehouse.Api/Warehouse.Api.csproj ./Final/Warehouse.Api/
16 |
17 | # Restore nuget packages
18 | RUN dotnet restore ./Final/Warehouse.Api/Warehouse.Api.csproj
19 |
20 | # Copy project files
21 | COPY ./Final/Warehouse ./Final/Warehouse
22 | COPY ./Final/Warehouse.Api/ ./Final/Warehouse.Api
23 |
24 | # Build project with Release configuration
25 | # and no restore, as we did it already
26 | RUN dotnet build -c Release --no-restore ./Final/Warehouse.Api/Warehouse.Api.csproj
27 |
28 | ## Test project with Release configuration
29 | ## and no build, as we did it already
30 | #RUN dotnet test -c Release --no-build ./Final/Warehouse.Api/Warehouse.Api.Tests.csproj
31 |
32 |
33 | # Publish project to output folder
34 | # and no build, as we did it already
35 | WORKDIR /app/Final/Warehouse.Api
36 | RUN ls
37 | RUN dotnet publish -c Release --no-build -o out
38 |
39 | ########################################
40 | # Second stage of multistage build
41 | ########################################
42 | # Use other build image as the final one
43 | # that won't have source codes
44 | ########################################
45 | FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine
46 |
47 | # Setup working directory for project
48 | WORKDIR /app
49 |
50 | # Copy published in previous stage binaries
51 | # from the `builder` image
52 | COPY --from=builder /app/Final/Warehouse.Api/out .
53 |
54 | # Set URL that App will be exposed
55 | ENV ASPNETCORE_URLS="http://*:5000"
56 |
57 | # sets entry point command to automatically
58 | # run application on `docker run`
59 | ENTRYPOINT ["dotnet", "Warehouse.Api.dll"]
60 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api/Core/Validation.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Runtime.CompilerServices;
3 | using System.Text.RegularExpressions;
4 |
5 | namespace Warehouse.Api.Core;
6 |
7 | public static class Validation
8 | {
9 | public static Guid AssertNotEmpty(
10 | [NotNull] this Guid? value,
11 | [CallerArgumentExpression("value")] string? argumentName = null
12 | ) =>
13 | (value != null && value.Value != Guid.Empty)
14 | ? value.Value
15 | : throw new ArgumentOutOfRangeException(argumentName);
16 |
17 |
18 | public static string AssertNotEmpty(
19 | [NotNull] this string? value,
20 | [CallerArgumentExpression("value")] string? argumentName = null
21 | ) =>
22 | !string.IsNullOrWhiteSpace(value)
23 | ? value
24 | : throw new ArgumentOutOfRangeException(argumentName);
25 |
26 |
27 | public static string? AssertNullOrNotEmpty(
28 | this string? value,
29 | [CallerArgumentExpression("value")] string? argumentName = null
30 | ) =>
31 | value?.AssertNotEmpty(argumentName);
32 |
33 | public static string AssertMatchesRegex(
34 | [NotNull] this string? value,
35 | [StringSyntax(StringSyntaxAttribute.Regex)]
36 | string pattern,
37 | [CallerArgumentExpression("value")] string? argumentName = null
38 | ) =>
39 | Regex.IsMatch(value.AssertNotEmpty(), pattern)
40 | ? value
41 | : throw new ArgumentOutOfRangeException(argumentName);
42 |
43 | public static int AssertPositive(
44 | [NotNull] this int? value,
45 | [CallerArgumentExpression("value")] string? argumentName = null
46 | ) =>
47 | value?.AssertPositive() ?? throw new ArgumentOutOfRangeException(argumentName);
48 |
49 | public static int AssertPositive(
50 | this int value,
51 | [CallerArgumentExpression("value")] string? argumentName = null
52 | ) =>
53 | value > 0
54 | ? value
55 | : throw new ArgumentOutOfRangeException(argumentName);
56 | }
57 |
--------------------------------------------------------------------------------
/Final/Warehouse/Core/Validation.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Runtime.CompilerServices;
3 | using System.Text.RegularExpressions;
4 |
5 | namespace Warehouse.Core;
6 |
7 | public static class Validation
8 | {
9 | public static Guid AssertNotEmpty(
10 | [NotNull] this Guid? value,
11 | [CallerArgumentExpression("value")] string? argumentName = null
12 | ) =>
13 | (value != null && value.Value != Guid.Empty)
14 | ? value.Value
15 | : throw new ArgumentOutOfRangeException(argumentName);
16 |
17 |
18 | public static string AssertNotEmpty(
19 | [NotNull] this string? value,
20 | [CallerArgumentExpression("value")] string? argumentName = null
21 | ) =>
22 | !string.IsNullOrWhiteSpace(value)
23 | ? value
24 | : throw new ArgumentOutOfRangeException(argumentName);
25 |
26 |
27 | public static string? AssertNullOrNotEmpty(
28 | this string? value,
29 | [CallerArgumentExpression("value")] string? argumentName = null
30 | ) =>
31 | value?.AssertNotEmpty(argumentName);
32 |
33 | public static string AssertMatchesRegex(
34 | [NotNull] this string? value,
35 | [StringSyntax(StringSyntaxAttribute.Regex)]
36 | string pattern,
37 | [CallerArgumentExpression("value")] string? argumentName = null
38 | )
39 | {
40 | return Regex.IsMatch(value.AssertNotEmpty(), pattern)
41 | ? value
42 | : throw new ArgumentOutOfRangeException(argumentName);
43 | }
44 |
45 | public static int AssertPositive(
46 | [NotNull] this int? value,
47 | [CallerArgumentExpression("value")] string? argumentName = null
48 | ) =>
49 | value?.AssertPositive() ?? throw new ArgumentOutOfRangeException(argumentName);
50 |
51 | public static int AssertPositive(
52 | this int value,
53 | [CallerArgumentExpression("value")] string? argumentName = null
54 | ) =>
55 | value > 0
56 | ? value
57 | : throw new ArgumentOutOfRangeException(argumentName);
58 | }
59 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api.Tests/Products/GettingProductDetails/GetProductDetailsTests.cs:
--------------------------------------------------------------------------------
1 | using Ogooreck.API;
2 | using Warehouse.Api.Requests;
3 | using Warehouse.Products;
4 | using static Ogooreck.API.ApiSpecification;
5 | using Xunit;
6 |
7 | namespace Warehouse.Api.Tests.Products.GettingProductDetails;
8 |
9 | public class GetProductDetailsTests: IClassFixture
10 | {
11 | private readonly GetProductDetailsFixture API;
12 |
13 | public GetProductDetailsTests(GetProductDetailsFixture api) => API = api;
14 |
15 | [Fact]
16 | public Task ValidRequest_With_NoParams_ShouldReturn_200() =>
17 | API.Given(URI($"/api/products/{API.ExistingProduct.Id}"))
18 | .When(GET)
19 | .Then(OK, RESPONSE_BODY(API.ExistingProduct));
20 |
21 | [Theory]
22 | [InlineData(12)]
23 | [InlineData("not-a-guid")]
24 | public Task InvalidGuidId_ShouldReturn_404(object invalidId) =>
25 | API.Given(URI($"/api/products/{invalidId}"))
26 | .When(GET)
27 | .Then(NOT_FOUND);
28 |
29 | [Fact]
30 | public Task NotExistingId_ShouldReturn_404() =>
31 | API.Given(URI($"/api/products/{Guid.NewGuid()}"))
32 | .When(GET)
33 | .Then(NOT_FOUND);
34 | }
35 |
36 | public class GetProductDetailsFixture: ApiSpecification, IAsyncLifetime
37 | {
38 | public ProductDetails ExistingProduct = default!;
39 |
40 | public GetProductDetailsFixture(): base(new WarehouseTestWebApplicationFactory()) { }
41 |
42 | public async Task InitializeAsync()
43 | {
44 | var registerProduct = new RegisterProductRequest("IN11111", "ValidName", "ValidDescription");
45 | var registerResponse = await Send(
46 | new ApiRequest(POST, URI("/api/products"), BODY(registerProduct))
47 | );
48 |
49 | await CREATED(registerResponse);
50 |
51 | var (sku, name, description) = registerProduct;
52 | ExistingProduct = new ProductDetails(registerResponse.GetCreatedId(), sku!, name!, description);
53 | }
54 |
55 | public Task DisposeAsync() => Task.CompletedTask;
56 | }
57 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Core/Validation.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Runtime.CompilerServices;
3 | using System.Text.RegularExpressions;
4 |
5 | namespace Warehouse.Core;
6 |
7 | public static class Validation
8 | {
9 | public static Guid AssertNotEmpty(
10 | [NotNull] this Guid? value,
11 | [CallerArgumentExpression("value")] string? argumentName = null
12 | ) =>
13 | (value != null && value.Value != Guid.Empty)
14 | ? value.Value
15 | : throw new ArgumentOutOfRangeException(argumentName);
16 |
17 |
18 | public static string AssertNotEmpty(
19 | [NotNull] this string? value,
20 | [CallerArgumentExpression("value")] string? argumentName = null
21 | ) =>
22 | !string.IsNullOrWhiteSpace(value)
23 | ? value
24 | : throw new ArgumentOutOfRangeException(argumentName);
25 |
26 |
27 | public static string? AssertNullOrNotEmpty(
28 | this string? value,
29 | [CallerArgumentExpression("value")] string? argumentName = null
30 | ) =>
31 | value?.AssertNotEmpty(argumentName);
32 |
33 | public static string AssertMatchesRegex(
34 | [NotNull] this string? value,
35 | [StringSyntax(StringSyntaxAttribute.Regex)]
36 | string pattern,
37 | [CallerArgumentExpression("value")] string? argumentName = null
38 | )
39 | {
40 | return Regex.IsMatch(value.AssertNotEmpty(), pattern)
41 | ? value
42 | : throw new ArgumentOutOfRangeException(argumentName);
43 | }
44 |
45 | public static int AssertPositive(
46 | [NotNull] this int? value,
47 | [CallerArgumentExpression("value")] string? argumentName = null
48 | ) =>
49 | value?.AssertPositive() ?? throw new ArgumentOutOfRangeException(argumentName);
50 |
51 | public static int AssertPositive(
52 | this int value,
53 | [CallerArgumentExpression("value")] string? argumentName = null
54 | ) =>
55 | value > 0
56 | ? value
57 | : throw new ArgumentOutOfRangeException(argumentName);
58 | }
59 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api.Tests/Products/GettingProductDetails/GetProductDetailsTests.cs:
--------------------------------------------------------------------------------
1 | using Ogooreck.API;
2 | using Warehouse.Api.Requests;
3 | using Warehouse.Products;
4 | using static Ogooreck.API.ApiSpecification;
5 | using Xunit;
6 |
7 | namespace Warehouse.Api.Tests.Products.GettingProductDetails;
8 |
9 | public class GetProductDetailsTests: IClassFixture
10 | {
11 | private readonly GetProductDetailsFixture API;
12 |
13 | public GetProductDetailsTests(GetProductDetailsFixture api) => API = api;
14 |
15 | [Fact]
16 | public Task ValidRequest_With_NoParams_ShouldReturn_200() =>
17 | API.Given(URI($"/api/products/{API.ExistingProduct.Id}"))
18 | .When(GET)
19 | .Then(OK, RESPONSE_BODY(API.ExistingProduct));
20 |
21 | [Theory]
22 | [InlineData(12)]
23 | [InlineData("not-a-guid")]
24 | public Task InvalidGuidId_ShouldReturn_404(object invalidId) =>
25 | API.Given(URI($"/api/products/{invalidId}"))
26 | .When(GET)
27 | .Then(NOT_FOUND);
28 |
29 | [Fact]
30 | public Task NotExistingId_ShouldReturn_404() =>
31 | API.Given(URI($"/api/products/{Guid.NewGuid()}"))
32 | .When(GET)
33 | .Then(NOT_FOUND);
34 | }
35 |
36 | public class GetProductDetailsFixture: ApiSpecification, IAsyncLifetime
37 | {
38 | public ProductDetails ExistingProduct = default!;
39 |
40 | public GetProductDetailsFixture(): base(new WarehouseTestWebApplicationFactory()) { }
41 |
42 | public async Task InitializeAsync()
43 | {
44 | var registerProduct = new RegisterProductRequest("IN11111", "ValidName", "ValidDescription");
45 | var registerResponse = await Send(
46 | new ApiRequest(POST, URI("/api/products"), BODY(registerProduct))
47 | );
48 |
49 | await CREATED(registerResponse);
50 |
51 | var (sku, name, description) = registerProduct;
52 | ExistingProduct = new ProductDetails(registerResponse.GetCreatedId(), sku!, name!, description);
53 | }
54 |
55 | public Task DisposeAsync() => Task.CompletedTask;
56 | }
57 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Core/Validation.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Runtime.CompilerServices;
3 | using System.Text.RegularExpressions;
4 |
5 | namespace Warehouse.Core;
6 |
7 | public static class Validation
8 | {
9 | public static Guid AssertNotEmpty(
10 | [NotNull] this Guid? value,
11 | [CallerArgumentExpression("value")] string? argumentName = null
12 | ) =>
13 | (value != null && value.Value != Guid.Empty)
14 | ? value.Value
15 | : throw new ArgumentOutOfRangeException(argumentName);
16 |
17 |
18 | public static string AssertNotEmpty(
19 | [NotNull] this string? value,
20 | [CallerArgumentExpression("value")] string? argumentName = null
21 | ) =>
22 | !string.IsNullOrWhiteSpace(value)
23 | ? value
24 | : throw new ArgumentOutOfRangeException(argumentName);
25 |
26 |
27 | public static string? AssertNullOrNotEmpty(
28 | this string? value,
29 | [CallerArgumentExpression("value")] string? argumentName = null
30 | ) =>
31 | value?.AssertNotEmpty(argumentName);
32 |
33 | public static string AssertMatchesRegex(
34 | [NotNull] this string? value,
35 | [StringSyntax(StringSyntaxAttribute.Regex)]
36 | string pattern,
37 | [CallerArgumentExpression("value")] string? argumentName = null
38 | )
39 | {
40 | return Regex.IsMatch(value.AssertNotEmpty(), pattern)
41 | ? value
42 | : throw new ArgumentOutOfRangeException(argumentName);
43 | }
44 |
45 | public static int AssertPositive(
46 | [NotNull] this int? value,
47 | [CallerArgumentExpression("value")] string? argumentName = null
48 | ) =>
49 | value?.AssertPositive() ?? throw new ArgumentOutOfRangeException(argumentName);
50 |
51 | public static int AssertPositive(
52 | this int value,
53 | [CallerArgumentExpression("value")] string? argumentName = null
54 | ) =>
55 | value > 0
56 | ? value
57 | : throw new ArgumentOutOfRangeException(argumentName);
58 | }
59 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api.Tests/Products/GettingProductDetails/GetProductDetailsTests.cs:
--------------------------------------------------------------------------------
1 | using Ogooreck.API;
2 | using static Ogooreck.API.ApiSpecification;
3 | using Warehouse.Products.GettingProductDetails;
4 | using Warehouse.Products.RegisteringProduct;
5 | using Xunit;
6 |
7 | namespace Warehouse.Api.Tests.Products.GettingProductDetails;
8 |
9 | public class GetProductDetailsTests: IClassFixture
10 | {
11 | private readonly GetProductDetailsFixture API;
12 |
13 | public GetProductDetailsTests(GetProductDetailsFixture api) => API = api;
14 |
15 | [Fact]
16 | public Task ValidRequest_With_NoParams_ShouldReturn_200() =>
17 | API.Given(URI($"/api/products/{API.ExistingProduct.Id}"))
18 | .When(GET)
19 | .Then(OK, RESPONSE_BODY(API.ExistingProduct));
20 |
21 | [Theory]
22 | [InlineData(12)]
23 | [InlineData("not-a-guid")]
24 | public Task InvalidGuidId_ShouldReturn_404(object invalidId) =>
25 | API.Given(URI($"/api/products/{invalidId}"))
26 | .When(GET)
27 | .Then(NOT_FOUND);
28 |
29 | [Fact]
30 | public Task NotExistingId_ShouldReturn_404() =>
31 | API.Given(URI($"/api/products/{Guid.NewGuid()}"))
32 | .When(GET)
33 | .Then(NOT_FOUND);
34 | }
35 |
36 |
37 | public class GetProductDetailsFixture: ApiSpecification, IAsyncLifetime
38 | {
39 | public ProductDetails ExistingProduct = default!;
40 |
41 | public GetProductDetailsFixture(): base(new WarehouseTestWebApplicationFactory()) { }
42 |
43 | public async Task InitializeAsync()
44 | {
45 | var registerProduct = new RegisterProductRequest("IN11111", "ValidName", "ValidDescription");
46 | var registerResponse = await Send(
47 | new ApiRequest(POST, URI("/api/products"), BODY(registerProduct))
48 | );
49 |
50 | await CREATED(registerResponse);
51 |
52 | var (sku, name, description) = registerProduct;
53 | ExistingProduct = new ProductDetails(registerResponse.GetCreatedId(), sku!, name!, description);
54 | }
55 |
56 | public Task DisposeAsync() => Task.CompletedTask;
57 | }
58 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api.Tests/Warehouse.Api.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | runtime; build; native; contentfiles; analyzers; buildtransitive
13 | all
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | true
38 | PreserveNewest
39 | PreserveNewest
40 |
41 |
42 | true
43 | PreserveNewest
44 | PreserveNewest
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api.Tests/Warehouse.Api.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | runtime; build; native; contentfiles; analyzers; buildtransitive
13 | all
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | true
39 | PreserveNewest
40 | PreserveNewest
41 |
42 |
43 | true
44 | PreserveNewest
45 | PreserveNewest
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api.Tests/Warehouse.Api.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | runtime; build; native; contentfiles; analyzers; buildtransitive
13 | all
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | true
39 | PreserveNewest
40 | PreserveNewest
41 |
42 |
43 | true
44 | PreserveNewest
45 | PreserveNewest
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://twitter.com/oskar_at_net)  [](https://event-driven.io/?utm_source=event_sourcing_net) [](https://www.architecture-weekly.com/?utm_source=event_sourcing_net)
2 |
3 | # CQRS is simpler with .NET 7 and C# 11
4 |
5 | Watch the talk [below](https://www.youtube.com/watch?v=iY7LO289qnQ):
6 |
7 |
8 |
9 | Repository with backing code for my talk. For more samples like that see [EventSourcing in .NET repository](https://github.com/oskardudycz/EventSourcing.NetCore).
10 |
11 | ## Read also
12 | - 📝 [CQRS facts and myths explained](https://event-driven.io/en/cqrs_facts_and_myths_explained/?utm_source=event_sourcing_net)
13 | - 📝 [What onion has to do with Clean Code?](https://event-driven.io/pl/onion_clean_code/?utm_source=event_sourcing_net)
14 | - 📝 [How to slice the codebase effectively?](https://event-driven.io/en/how_to_slice_the_codebase_effectively/?utm_source=event_sourcing_net)
15 | - 📝 [Generic does not mean Simple?](https://event-driven.io/en/generic_does_not_mean_simple/?utm_source=event_sourcing_net)
16 | - 📝 [Can command return a value?](https://event-driven.io/en/can_command_return_a_value/?utm_source=event_sourcing_net)
17 | - 📝 [CQRS is simpler than you think with .NET 6 and C# 10](https://event-driven.io/en/cqrs_is_simpler_than_you_think_with_net6/?utm_source=event_sourcing_net)
18 | - 📝 [How to register all CQRS handlers by convention](https://event-driven.io/en/how_to_register_all_mediatr_handlers_by_convention/?utm_source=event_sourcing_net)
19 | - 📝 [Notes about C# records and Nullable Reference Types](https://event-driven.io/en/notes_about_csharp_records_and_nullable_reference_types/?utm_source=event_sourcing_net)
20 |
21 | See also [Java version of those samples](https://github.com/oskardudycz/cqrs-is-simpler-with-java).
22 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api.Tests/Warehouse.Api.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net7.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | runtime; build; native; contentfiles; analyzers; buildtransitive
13 | all
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | true
39 | PreserveNewest
40 | PreserveNewest
41 |
42 |
43 | true
44 | PreserveNewest
45 | PreserveNewest
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api/Migrations/WarehouseDBContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
7 |
8 | #nullable disable
9 |
10 | namespace Warehouse.Migrations
11 | {
12 | [DbContext(typeof(WarehouseDBContext))]
13 | partial class WarehouseDBContextModelSnapshot : ModelSnapshot
14 | {
15 | protected override void BuildModel(ModelBuilder modelBuilder)
16 | {
17 | #pragma warning disable 612, 618
18 | modelBuilder
19 | .HasAnnotation("ProductVersion", "7.0.2")
20 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
21 |
22 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
23 |
24 | modelBuilder.Entity("Warehouse.Products.Product", b =>
25 | {
26 | b.Property("Id")
27 | .HasColumnType("uuid");
28 |
29 | b.Property("Description")
30 | .HasColumnType("text");
31 |
32 | b.Property("Name")
33 | .IsRequired()
34 | .HasColumnType("text");
35 |
36 | b.HasKey("Id");
37 |
38 | b.ToTable("Product");
39 | });
40 |
41 | modelBuilder.Entity("Warehouse.Products.Product", b =>
42 | {
43 | b.OwnsOne("Warehouse.Products.SKU", "Sku", b1 =>
44 | {
45 | b1.Property("ProductId")
46 | .HasColumnType("uuid");
47 |
48 | b1.Property("Value")
49 | .IsRequired()
50 | .HasColumnType("text");
51 |
52 | b1.HasKey("ProductId");
53 |
54 | b1.ToTable("Product");
55 |
56 | b1.WithOwner()
57 | .HasForeignKey("ProductId");
58 | });
59 |
60 | b.Navigation("Sku")
61 | .IsRequired();
62 | });
63 | #pragma warning restore 612, 618
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Final/Warehouse/Migrations/WarehouseDBContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
7 | using Warehouse.Storage;
8 |
9 | #nullable disable
10 |
11 | namespace Warehouse.Migrations
12 | {
13 | [DbContext(typeof(WarehouseDBContext))]
14 | partial class WarehouseDBContextModelSnapshot : ModelSnapshot
15 | {
16 | protected override void BuildModel(ModelBuilder modelBuilder)
17 | {
18 | #pragma warning disable 612, 618
19 | modelBuilder
20 | .HasAnnotation("ProductVersion", "7.0.2")
21 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
22 |
23 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
24 |
25 | modelBuilder.Entity("Warehouse.Products.Product", b =>
26 | {
27 | b.Property("Id")
28 | .HasColumnType("uuid");
29 |
30 | b.Property("Description")
31 | .HasColumnType("text");
32 |
33 | b.Property("Name")
34 | .IsRequired()
35 | .HasColumnType("text");
36 |
37 | b.HasKey("Id");
38 |
39 | b.ToTable("Product");
40 | });
41 |
42 | modelBuilder.Entity("Warehouse.Products.Product", b =>
43 | {
44 | b.OwnsOne("Warehouse.Products.SKU", "Sku", b1 =>
45 | {
46 | b1.Property("ProductId")
47 | .HasColumnType("uuid");
48 |
49 | b1.Property("Value")
50 | .IsRequired()
51 | .HasColumnType("text");
52 |
53 | b1.HasKey("ProductId");
54 |
55 | b1.ToTable("Product");
56 |
57 | b1.WithOwner()
58 | .HasForeignKey("ProductId");
59 | });
60 |
61 | b.Navigation("Sku")
62 | .IsRequired();
63 | });
64 | #pragma warning restore 612, 618
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Migrations/WarehouseDBContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
7 | using Warehouse.Storage;
8 |
9 | #nullable disable
10 |
11 | namespace Warehouse.Migrations
12 | {
13 | [DbContext(typeof(WarehouseDBContext))]
14 | partial class WarehouseDBContextModelSnapshot : ModelSnapshot
15 | {
16 | protected override void BuildModel(ModelBuilder modelBuilder)
17 | {
18 | #pragma warning disable 612, 618
19 | modelBuilder
20 | .HasAnnotation("ProductVersion", "7.0.2")
21 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
22 |
23 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
24 |
25 | modelBuilder.Entity("Warehouse.Products.Product", b =>
26 | {
27 | b.Property("Id")
28 | .HasColumnType("uuid");
29 |
30 | b.Property("Description")
31 | .HasColumnType("text");
32 |
33 | b.Property("Name")
34 | .IsRequired()
35 | .HasColumnType("text");
36 |
37 | b.HasKey("Id");
38 |
39 | b.ToTable("Product");
40 | });
41 |
42 | modelBuilder.Entity("Warehouse.Products.Product", b =>
43 | {
44 | b.OwnsOne("Warehouse.Products.SKU", "Sku", b1 =>
45 | {
46 | b1.Property("ProductId")
47 | .HasColumnType("uuid");
48 |
49 | b1.Property("Value")
50 | .IsRequired()
51 | .HasColumnType("text");
52 |
53 | b1.HasKey("ProductId");
54 |
55 | b1.ToTable("Product");
56 |
57 | b1.WithOwner()
58 | .HasForeignKey("ProductId");
59 | });
60 |
61 | b.Navigation("Sku")
62 | .IsRequired();
63 | });
64 | #pragma warning restore 612, 618
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Migrations/WarehouseDBContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
7 | using Warehouse.Storage;
8 |
9 | #nullable disable
10 |
11 | namespace Warehouse.Migrations
12 | {
13 | [DbContext(typeof(WarehouseDBContext))]
14 | partial class WarehouseDBContextModelSnapshot : ModelSnapshot
15 | {
16 | protected override void BuildModel(ModelBuilder modelBuilder)
17 | {
18 | #pragma warning disable 612, 618
19 | modelBuilder
20 | .HasAnnotation("ProductVersion", "7.0.2")
21 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
22 |
23 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
24 |
25 | modelBuilder.Entity("Warehouse.Products.Product", b =>
26 | {
27 | b.Property("Id")
28 | .HasColumnType("uuid");
29 |
30 | b.Property("Description")
31 | .HasColumnType("text");
32 |
33 | b.Property("Name")
34 | .IsRequired()
35 | .HasColumnType("text");
36 |
37 | b.HasKey("Id");
38 |
39 | b.ToTable("Product");
40 | });
41 |
42 | modelBuilder.Entity("Warehouse.Products.Product", b =>
43 | {
44 | b.OwnsOne("Warehouse.Products.SKU", "Sku", b1 =>
45 | {
46 | b1.Property("ProductId")
47 | .HasColumnType("uuid");
48 |
49 | b1.Property("Value")
50 | .IsRequired()
51 | .HasColumnType("text");
52 |
53 | b1.HasKey("ProductId");
54 |
55 | b1.ToTable("Product");
56 |
57 | b1.WithOwner()
58 | .HasForeignKey("ProductId");
59 | });
60 |
61 | b.Navigation("Sku")
62 | .IsRequired();
63 | });
64 | #pragma warning restore 612, 618
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api/Middlewares/ExceptionHandling/ExceptionHandlingMiddleware.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Text.Json;
3 |
4 | namespace Warehouse.Api.Middlewares.ExceptionHandling;
5 |
6 | public class ExceptionHandlingMiddleware
7 | {
8 | private readonly RequestDelegate next;
9 |
10 | private readonly ILogger logger;
11 |
12 | public ExceptionHandlingMiddleware(
13 | RequestDelegate next,
14 | ILoggerFactory loggerFactory
15 | )
16 | {
17 | this.next = next;
18 | logger = loggerFactory.CreateLogger();
19 | }
20 |
21 | public async Task Invoke(HttpContext context /* other scoped dependencies */)
22 | {
23 | try
24 | {
25 | await next(context).ConfigureAwait(false);
26 | }
27 | catch (Exception ex)
28 | {
29 | await HandleExceptionAsync(context, ex).ConfigureAwait(false);
30 | }
31 | }
32 |
33 | private Task HandleExceptionAsync(HttpContext context, Exception exception)
34 | {
35 | logger.LogError(exception, exception.Message);
36 | Console.WriteLine("ERROR:" + exception.Message + exception.StackTrace);
37 |
38 | if (exception.InnerException != null)
39 | Console.WriteLine("INNER DETAILS:" + exception.InnerException.Message +
40 | exception.InnerException.StackTrace);
41 |
42 | var codeInfo = ExceptionToHttpStatusMapper.Map(exception);
43 |
44 | var result = JsonSerializer.Serialize(new HttpExceptionWrapper((int)codeInfo.Code, codeInfo.Message));
45 | context.Response.ContentType = "application/json";
46 | context.Response.StatusCode = (int)codeInfo.Code;
47 | return context.Response.WriteAsync(result);
48 | }
49 | }
50 |
51 | public class HttpExceptionWrapper
52 | {
53 | public int StatusCode { get; }
54 |
55 | public string Error { get; }
56 |
57 | public HttpExceptionWrapper(int statusCode, string error)
58 | {
59 | StatusCode = statusCode;
60 | Error = error;
61 | }
62 | }
63 |
64 | public static class ExceptionHandlingMiddlewareExtensions
65 | {
66 | public static IApplicationBuilder UseExceptionHandlingMiddleware(
67 | this IApplicationBuilder app,
68 | Func? customMap = null
69 | )
70 | {
71 | ExceptionToHttpStatusMapper.CustomMap = customMap;
72 | return app.UseMiddleware();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api/Middlewares/ExceptionHandling/ExceptionHandlingMiddleware.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Text.Json;
3 |
4 | namespace Warehouse.Api.Middlewares.ExceptionHandling;
5 |
6 | public class ExceptionHandlingMiddleware
7 | {
8 | private readonly RequestDelegate next;
9 |
10 | private readonly ILogger logger;
11 |
12 | public ExceptionHandlingMiddleware(
13 | RequestDelegate next,
14 | ILoggerFactory loggerFactory
15 | )
16 | {
17 | this.next = next;
18 | logger = loggerFactory.CreateLogger();
19 | }
20 |
21 | public async Task Invoke(HttpContext context /* other scoped dependencies */)
22 | {
23 | try
24 | {
25 | await next(context).ConfigureAwait(false);
26 | }
27 | catch (Exception ex)
28 | {
29 | await HandleExceptionAsync(context, ex).ConfigureAwait(false);
30 | }
31 | }
32 |
33 | private Task HandleExceptionAsync(HttpContext context, Exception exception)
34 | {
35 | logger.LogError(exception, exception.Message);
36 | Console.WriteLine("ERROR:" + exception.Message + exception.StackTrace);
37 |
38 | if (exception.InnerException != null)
39 | Console.WriteLine("INNER DETAILS:" + exception.InnerException.Message +
40 | exception.InnerException.StackTrace);
41 |
42 | var codeInfo = ExceptionToHttpStatusMapper.Map(exception);
43 |
44 | var result = JsonSerializer.Serialize(new HttpExceptionWrapper((int)codeInfo.Code, codeInfo.Message));
45 | context.Response.ContentType = "application/json";
46 | context.Response.StatusCode = (int)codeInfo.Code;
47 | return context.Response.WriteAsync(result);
48 | }
49 | }
50 |
51 | public class HttpExceptionWrapper
52 | {
53 | public int StatusCode { get; }
54 |
55 | public string Error { get; }
56 |
57 | public HttpExceptionWrapper(int statusCode, string error)
58 | {
59 | StatusCode = statusCode;
60 | Error = error;
61 | }
62 | }
63 |
64 | public static class ExceptionHandlingMiddlewareExtensions
65 | {
66 | public static IApplicationBuilder UseExceptionHandlingMiddleware(
67 | this IApplicationBuilder app,
68 | Func? customMap = null
69 | )
70 | {
71 | ExceptionToHttpStatusMapper.CustomMap = customMap;
72 | return app.UseMiddleware();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api/Middlewares/ExceptionHandling/ExceptionHandlingMiddleware.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Text.Json;
3 |
4 | namespace Warehouse.Api.Middlewares.ExceptionHandling;
5 |
6 | public class ExceptionHandlingMiddleware
7 | {
8 | private readonly RequestDelegate next;
9 |
10 | private readonly ILogger logger;
11 |
12 | public ExceptionHandlingMiddleware(
13 | RequestDelegate next,
14 | ILoggerFactory loggerFactory
15 | )
16 | {
17 | this.next = next;
18 | logger = loggerFactory.CreateLogger();
19 | }
20 |
21 | public async Task Invoke(HttpContext context /* other scoped dependencies */)
22 | {
23 | try
24 | {
25 | await next(context).ConfigureAwait(false);
26 | }
27 | catch (Exception ex)
28 | {
29 | await HandleExceptionAsync(context, ex).ConfigureAwait(false);
30 | }
31 | }
32 |
33 | private Task HandleExceptionAsync(HttpContext context, Exception exception)
34 | {
35 | logger.LogError(exception, exception.Message);
36 | Console.WriteLine("ERROR:" + exception.Message + exception.StackTrace);
37 |
38 | if (exception.InnerException != null)
39 | Console.WriteLine("INNER DETAILS:" + exception.InnerException.Message +
40 | exception.InnerException.StackTrace);
41 |
42 | var codeInfo = ExceptionToHttpStatusMapper.Map(exception);
43 |
44 | var result = JsonSerializer.Serialize(new HttpExceptionWrapper((int)codeInfo.Code, codeInfo.Message));
45 | context.Response.ContentType = "application/json";
46 | context.Response.StatusCode = (int)codeInfo.Code;
47 | return context.Response.WriteAsync(result);
48 | }
49 | }
50 |
51 | public class HttpExceptionWrapper
52 | {
53 | public int StatusCode { get; }
54 |
55 | public string Error { get; }
56 |
57 | public HttpExceptionWrapper(int statusCode, string error)
58 | {
59 | StatusCode = statusCode;
60 | Error = error;
61 | }
62 | }
63 |
64 | public static class ExceptionHandlingMiddlewareExtensions
65 | {
66 | public static IApplicationBuilder UseExceptionHandlingMiddleware(
67 | this IApplicationBuilder app,
68 | Func? customMap = null
69 | )
70 | {
71 | ExceptionToHttpStatusMapper.CustomMap = customMap;
72 | return app.UseMiddleware();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api/Middlewares/ExceptionHandling/ExceptionHandlingMiddleware.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Text.Json;
3 |
4 | namespace Warehouse.Api.Middlewares.ExceptionHandling;
5 |
6 | public class ExceptionHandlingMiddleware
7 | {
8 | private readonly RequestDelegate next;
9 |
10 | private readonly ILogger logger;
11 |
12 | public ExceptionHandlingMiddleware(
13 | RequestDelegate next,
14 | ILoggerFactory loggerFactory
15 | )
16 | {
17 | this.next = next;
18 | logger = loggerFactory.CreateLogger();
19 | }
20 |
21 | public async Task Invoke(HttpContext context /* other scoped dependencies */)
22 | {
23 | try
24 | {
25 | await next(context).ConfigureAwait(false);
26 | }
27 | catch (Exception ex)
28 | {
29 | await HandleExceptionAsync(context, ex).ConfigureAwait(false);
30 | }
31 | }
32 |
33 | private Task HandleExceptionAsync(HttpContext context, Exception exception)
34 | {
35 | logger.LogError(exception, exception.Message);
36 | Console.WriteLine("ERROR:" + exception.Message + exception.StackTrace);
37 |
38 | if (exception.InnerException != null)
39 | Console.WriteLine("INNER DETAILS:" + exception.InnerException.Message +
40 | exception.InnerException.StackTrace);
41 |
42 | var codeInfo = ExceptionToHttpStatusMapper.Map(exception);
43 |
44 | var result = JsonSerializer.Serialize(new HttpExceptionWrapper((int)codeInfo.Code, codeInfo.Message));
45 | context.Response.ContentType = "application/json";
46 | context.Response.StatusCode = (int)codeInfo.Code;
47 | return context.Response.WriteAsync(result);
48 | }
49 | }
50 |
51 | public class HttpExceptionWrapper
52 | {
53 | public int StatusCode { get; }
54 |
55 | public string Error { get; }
56 |
57 | public HttpExceptionWrapper(int statusCode, string error)
58 | {
59 | StatusCode = statusCode;
60 | Error = error;
61 | }
62 | }
63 |
64 | public static class ExceptionHandlingMiddlewareExtensions
65 | {
66 | public static IApplicationBuilder UseExceptionHandlingMiddleware(
67 | this IApplicationBuilder app,
68 | Func? customMap = null
69 | )
70 | {
71 | ExceptionToHttpStatusMapper.CustomMap = customMap;
72 | return app.UseMiddleware();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api/Migrations/20230205120130_Initial.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Migrations;
6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
8 |
9 | #nullable disable
10 |
11 | namespace Warehouse.Migrations
12 | {
13 | [DbContext(typeof(WarehouseDBContext))]
14 | [Migration("20230205120130_Initial")]
15 | partial class Initial
16 | {
17 | ///
18 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
19 | {
20 | #pragma warning disable 612, 618
21 | modelBuilder
22 | .HasAnnotation("ProductVersion", "7.0.2")
23 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
24 |
25 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
26 |
27 | modelBuilder.Entity("Warehouse.Products.Product", b =>
28 | {
29 | b.Property("Id")
30 | .HasColumnType("uuid");
31 |
32 | b.Property("Description")
33 | .HasColumnType("text");
34 |
35 | b.Property("Name")
36 | .IsRequired()
37 | .HasColumnType("text");
38 |
39 | b.HasKey("Id");
40 |
41 | b.ToTable("Product");
42 | });
43 |
44 | modelBuilder.Entity("Warehouse.Products.Product", b =>
45 | {
46 | b.OwnsOne("Warehouse.Products.SKU", "Sku", b1 =>
47 | {
48 | b1.Property("ProductId")
49 | .HasColumnType("uuid");
50 |
51 | b1.Property("Value")
52 | .IsRequired()
53 | .HasColumnType("text");
54 |
55 | b1.HasKey("ProductId");
56 |
57 | b1.ToTable("Product");
58 |
59 | b1.WithOwner()
60 | .HasForeignKey("ProductId");
61 | });
62 |
63 | b.Navigation("Sku")
64 | .IsRequired();
65 | });
66 | #pragma warning restore 612, 618
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Final/Warehouse/Migrations/20230205120130_Initial.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Migrations;
6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
8 | using Warehouse.Storage;
9 |
10 | #nullable disable
11 |
12 | namespace Warehouse.Migrations
13 | {
14 | [DbContext(typeof(WarehouseDBContext))]
15 | [Migration("20230205120130_Initial")]
16 | partial class Initial
17 | {
18 | ///
19 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
20 | {
21 | #pragma warning disable 612, 618
22 | modelBuilder
23 | .HasAnnotation("ProductVersion", "7.0.2")
24 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
25 |
26 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
27 |
28 | modelBuilder.Entity("Warehouse.Products.Product", b =>
29 | {
30 | b.Property("Id")
31 | .HasColumnType("uuid");
32 |
33 | b.Property("Description")
34 | .HasColumnType("text");
35 |
36 | b.Property("Name")
37 | .IsRequired()
38 | .HasColumnType("text");
39 |
40 | b.HasKey("Id");
41 |
42 | b.ToTable("Product");
43 | });
44 |
45 | modelBuilder.Entity("Warehouse.Products.Product", b =>
46 | {
47 | b.OwnsOne("Warehouse.Products.SKU", "Sku", b1 =>
48 | {
49 | b1.Property("ProductId")
50 | .HasColumnType("uuid");
51 |
52 | b1.Property("Value")
53 | .IsRequired()
54 | .HasColumnType("text");
55 |
56 | b1.HasKey("ProductId");
57 |
58 | b1.ToTable("Product");
59 |
60 | b1.WithOwner()
61 | .HasForeignKey("ProductId");
62 | });
63 |
64 | b.Navigation("Sku")
65 | .IsRequired();
66 | });
67 | #pragma warning restore 612, 618
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Migrations/20230205120130_Initial.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Migrations;
6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
8 | using Warehouse.Storage;
9 |
10 | #nullable disable
11 |
12 | namespace Warehouse.Migrations
13 | {
14 | [DbContext(typeof(WarehouseDBContext))]
15 | [Migration("20230205120130_Initial")]
16 | partial class Initial
17 | {
18 | ///
19 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
20 | {
21 | #pragma warning disable 612, 618
22 | modelBuilder
23 | .HasAnnotation("ProductVersion", "7.0.2")
24 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
25 |
26 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
27 |
28 | modelBuilder.Entity("Warehouse.Products.Product", b =>
29 | {
30 | b.Property("Id")
31 | .HasColumnType("uuid");
32 |
33 | b.Property("Description")
34 | .HasColumnType("text");
35 |
36 | b.Property("Name")
37 | .IsRequired()
38 | .HasColumnType("text");
39 |
40 | b.HasKey("Id");
41 |
42 | b.ToTable("Product");
43 | });
44 |
45 | modelBuilder.Entity("Warehouse.Products.Product", b =>
46 | {
47 | b.OwnsOne("Warehouse.Products.SKU", "Sku", b1 =>
48 | {
49 | b1.Property("ProductId")
50 | .HasColumnType("uuid");
51 |
52 | b1.Property("Value")
53 | .IsRequired()
54 | .HasColumnType("text");
55 |
56 | b1.HasKey("ProductId");
57 |
58 | b1.ToTable("Product");
59 |
60 | b1.WithOwner()
61 | .HasForeignKey("ProductId");
62 | });
63 |
64 | b.Navigation("Sku")
65 | .IsRequired();
66 | });
67 | #pragma warning restore 612, 618
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse/Migrations/20230205120130_Initial.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.EntityFrameworkCore.Infrastructure;
5 | using Microsoft.EntityFrameworkCore.Migrations;
6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
8 | using Warehouse.Storage;
9 |
10 | #nullable disable
11 |
12 | namespace Warehouse.Migrations
13 | {
14 | [DbContext(typeof(WarehouseDBContext))]
15 | [Migration("20230205120130_Initial")]
16 | partial class Initial
17 | {
18 | ///
19 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
20 | {
21 | #pragma warning disable 612, 618
22 | modelBuilder
23 | .HasAnnotation("ProductVersion", "7.0.2")
24 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
25 |
26 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
27 |
28 | modelBuilder.Entity("Warehouse.Products.Product", b =>
29 | {
30 | b.Property("Id")
31 | .HasColumnType("uuid");
32 |
33 | b.Property("Description")
34 | .HasColumnType("text");
35 |
36 | b.Property("Name")
37 | .IsRequired()
38 | .HasColumnType("text");
39 |
40 | b.HasKey("Id");
41 |
42 | b.ToTable("Product");
43 | });
44 |
45 | modelBuilder.Entity("Warehouse.Products.Product", b =>
46 | {
47 | b.OwnsOne("Warehouse.Products.SKU", "Sku", b1 =>
48 | {
49 | b1.Property("ProductId")
50 | .HasColumnType("uuid");
51 |
52 | b1.Property("Value")
53 | .IsRequired()
54 | .HasColumnType("text");
55 |
56 | b1.HasKey("ProductId");
57 |
58 | b1.ToTable("Product");
59 |
60 | b1.WithOwner()
61 | .HasForeignKey("ProductId");
62 | });
63 |
64 | b.Navigation("Sku")
65 | .IsRequired();
66 | });
67 | #pragma warning restore 612, 618
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api.Tests/Products/RegisteringProduct/RegisterProductTests.cs:
--------------------------------------------------------------------------------
1 | using Ogooreck.API;
2 | using static Ogooreck.API.ApiSpecification;
3 | using Xunit;
4 |
5 | namespace Warehouse.Api.Tests.Products.RegisteringProduct;
6 |
7 | public class RegisterProductTests: IClassFixture
8 | {
9 | private readonly ApiSpecification API;
10 |
11 | public RegisterProductTests(WarehouseTestWebApplicationFactory webApplicationFactory) =>
12 | API = ApiSpecification.Setup(webApplicationFactory);
13 |
14 | [Theory]
15 | [MemberData(nameof(ValidRequests))]
16 | public Task ValidRequest_ShouldReturn_201(RegisterProductRequest validRequest) =>
17 | API.Given(
18 | URI("/api/products/"),
19 | BODY(validRequest)
20 | )
21 | .When(POST)
22 | .Then(CREATED);
23 |
24 | [Theory]
25 | [MemberData(nameof(InvalidRequests))]
26 | public Task InvalidRequest_ShouldReturn_400(RegisterProductRequest invalidRequest) =>
27 | API.Given(
28 | URI("/api/products"),
29 | BODY(invalidRequest)
30 | )
31 | .When(POST)
32 | .Then(BAD_REQUEST);
33 |
34 | [Fact]
35 | public async Task RequestForExistingSKUShouldFail_ShouldReturn_409()
36 | {
37 | // Given
38 | var request = new RegisterProductRequest("AA2039485", ValidName, ValidDescription);
39 |
40 | // first one should succeed
41 | await API.Given(
42 | URI("/api/products/"),
43 | BODY(request)
44 | )
45 | .When(POST)
46 | .Then(CREATED);
47 |
48 | // second one will fail with conflict
49 | await API.Given(
50 | URI("/api/products/"),
51 | BODY(request)
52 | )
53 | .When(POST)
54 | .Then(CONFLICT);
55 | }
56 |
57 | private const string ValidName = "VALID_NAME";
58 | private static string ValidSKU => $"CC{DateTime.Now.Ticks}";
59 | private const string ValidDescription = "VALID_DESCRIPTION";
60 |
61 | public static TheoryData ValidRequests = new()
62 | {
63 | new RegisterProductRequest(ValidSKU, ValidName, ValidDescription),
64 | new RegisterProductRequest(ValidSKU, ValidName, null)
65 | };
66 |
67 | public static TheoryData InvalidRequests = new()
68 | {
69 | new RegisterProductRequest(null!, ValidName, ValidDescription),
70 | new RegisterProductRequest("INVALID_SKU", ValidName, ValidDescription),
71 | new RegisterProductRequest(ValidSKU, null!, ValidDescription),
72 | };
73 | }
74 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api.Tests/Products/RegisteringProduct/RegisterProductTests.cs:
--------------------------------------------------------------------------------
1 | using Ogooreck.API;
2 | using Warehouse.Api.Requests;
3 | using static Ogooreck.API.ApiSpecification;
4 | using Xunit;
5 |
6 | namespace Warehouse.Api.Tests.Products.RegisteringProduct;
7 |
8 | public class RegisterProductTests: IClassFixture
9 | {
10 | private readonly ApiSpecification API;
11 |
12 | public RegisterProductTests(WarehouseTestWebApplicationFactory webApplicationFactory) =>
13 | API = ApiSpecification.Setup(webApplicationFactory);
14 |
15 | [Theory]
16 | [MemberData(nameof(ValidRequests))]
17 | public Task ValidRequest_ShouldReturn_201(RegisterProductRequest validRequest) =>
18 | API.Given(
19 | URI("/api/products/"),
20 | BODY(validRequest)
21 | )
22 | .When(POST)
23 | .Then(CREATED);
24 |
25 | [Theory]
26 | [MemberData(nameof(InvalidRequests))]
27 | public Task InvalidRequest_ShouldReturn_400(RegisterProductRequest invalidRequest) =>
28 | API.Given(
29 | URI("/api/products"),
30 | BODY(invalidRequest)
31 | )
32 | .When(POST)
33 | .Then(BAD_REQUEST);
34 |
35 | [Fact]
36 | public async Task RequestForExistingSKUShouldFail_ShouldReturn_409()
37 | {
38 | // Given
39 | var request = new RegisterProductRequest("AA2039485", ValidName, ValidDescription);
40 |
41 | // first one should succeed
42 | await API.Given(
43 | URI("/api/products/"),
44 | BODY(request)
45 | )
46 | .When(POST)
47 | .Then(CREATED);
48 |
49 | // second one will fail with conflict
50 | await API.Given(
51 | URI("/api/products/"),
52 | BODY(request)
53 | )
54 | .When(POST)
55 | .Then(CONFLICT);
56 | }
57 |
58 | private const string ValidName = "VALID_NAME";
59 | private static string ValidSKU => $"CC{DateTime.Now.Ticks}";
60 | private const string ValidDescription = "VALID_DESCRIPTION";
61 |
62 | public static TheoryData ValidRequests = new()
63 | {
64 | new RegisterProductRequest(ValidSKU, ValidName, ValidDescription),
65 | new RegisterProductRequest(ValidSKU, ValidName, null)
66 | };
67 |
68 | public static TheoryData InvalidRequests = new()
69 | {
70 | new RegisterProductRequest(null!, ValidName, ValidDescription),
71 | new RegisterProductRequest("INVALID_SKU", ValidName, ValidDescription),
72 | new RegisterProductRequest(ValidSKU, null!, ValidDescription),
73 | };
74 | }
75 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api.Tests/Products/RegisteringProduct/RegisterProductTests.cs:
--------------------------------------------------------------------------------
1 | using Ogooreck.API;
2 | using Warehouse.Api.Requests;
3 | using static Ogooreck.API.ApiSpecification;
4 | using Xunit;
5 |
6 | namespace Warehouse.Api.Tests.Products.RegisteringProduct;
7 |
8 | public class RegisterProductTests: IClassFixture
9 | {
10 | private readonly ApiSpecification API;
11 |
12 | public RegisterProductTests(WarehouseTestWebApplicationFactory webApplicationFactory) =>
13 | API = ApiSpecification.Setup(webApplicationFactory);
14 |
15 | [Theory]
16 | [MemberData(nameof(ValidRequests))]
17 | public Task ValidRequest_ShouldReturn_201(RegisterProductRequest validRequest) =>
18 | API.Given(
19 | URI("/api/products/"),
20 | BODY(validRequest)
21 | )
22 | .When(POST)
23 | .Then(CREATED);
24 |
25 | [Theory]
26 | [MemberData(nameof(InvalidRequests))]
27 | public Task InvalidRequest_ShouldReturn_400(RegisterProductRequest invalidRequest) =>
28 | API.Given(
29 | URI("/api/products"),
30 | BODY(invalidRequest)
31 | )
32 | .When(POST)
33 | .Then(BAD_REQUEST);
34 |
35 | [Fact]
36 | public async Task RequestForExistingSKUShouldFail_ShouldReturn_409()
37 | {
38 | // Given
39 | var request = new RegisterProductRequest("AA2039485", ValidName, ValidDescription);
40 |
41 | // first one should succeed
42 | await API.Given(
43 | URI("/api/products/"),
44 | BODY(request)
45 | )
46 | .When(POST)
47 | .Then(CREATED);
48 |
49 | // second one will fail with conflict
50 | await API.Given(
51 | URI("/api/products/"),
52 | BODY(request)
53 | )
54 | .When(POST)
55 | .Then(CONFLICT);
56 | }
57 |
58 | private const string ValidName = "VALID_NAME";
59 | private static string ValidSKU => $"CC{DateTime.Now.Ticks}";
60 | private const string ValidDescription = "VALID_DESCRIPTION";
61 |
62 | public static TheoryData ValidRequests = new()
63 | {
64 | new RegisterProductRequest(ValidSKU, ValidName, ValidDescription),
65 | new RegisterProductRequest(ValidSKU, ValidName, null)
66 | };
67 |
68 | public static TheoryData InvalidRequests = new()
69 | {
70 | new RegisterProductRequest(null!, ValidName, ValidDescription),
71 | new RegisterProductRequest("INVALID_SKU", ValidName, ValidDescription),
72 | new RegisterProductRequest(ValidSKU, null!, ValidDescription),
73 | };
74 | }
75 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api.Tests/Products/RegisteringProduct/RegisterProductTests.cs:
--------------------------------------------------------------------------------
1 | using Ogooreck.API;
2 | using static Ogooreck.API.ApiSpecification;
3 | using Warehouse.Products.RegisteringProduct;
4 | using Xunit;
5 |
6 | namespace Warehouse.Api.Tests.Products.RegisteringProduct;
7 |
8 | public class RegisterProductTests: IClassFixture
9 | {
10 | private readonly ApiSpecification API;
11 |
12 | public RegisterProductTests(WarehouseTestWebApplicationFactory webApplicationFactory) =>
13 | API = ApiSpecification.Setup(webApplicationFactory);
14 |
15 | [Theory]
16 | [MemberData(nameof(ValidRequests))]
17 | public Task ValidRequest_ShouldReturn_201(RegisterProductRequest validRequest) =>
18 | API.Given(
19 | URI("/api/products/"),
20 | BODY(validRequest)
21 | )
22 | .When(POST)
23 | .Then(CREATED);
24 |
25 | [Theory]
26 | [MemberData(nameof(InvalidRequests))]
27 | public Task InvalidRequest_ShouldReturn_400(RegisterProductRequest invalidRequest) =>
28 | API.Given(
29 | URI("/api/products"),
30 | BODY(invalidRequest)
31 | )
32 | .When(POST)
33 | .Then(BAD_REQUEST);
34 |
35 | [Fact]
36 | public async Task RequestForExistingSKUShouldFail_ShouldReturn_409()
37 | {
38 | // Given
39 | var request = new RegisterProductRequest("AA2039485", ValidName, ValidDescription);
40 |
41 | // first one should succeed
42 | await API.Given(
43 | URI("/api/products/"),
44 | BODY(request)
45 | )
46 | .When(POST)
47 | .Then(CREATED);
48 |
49 | // second one will fail with conflict
50 | await API.Given(
51 | URI("/api/products/"),
52 | BODY(request)
53 | )
54 | .When(POST)
55 | .Then(CONFLICT);
56 | }
57 |
58 | private const string ValidName = "VALID_NAME";
59 | private static string ValidSKU => $"CC{DateTime.Now.Ticks}";
60 | private const string ValidDescription = "VALID_DESCRIPTION";
61 |
62 | public static TheoryData ValidRequests = new()
63 | {
64 | new RegisterProductRequest(ValidSKU, ValidName, ValidDescription),
65 | new RegisterProductRequest(ValidSKU, ValidName, null)
66 | };
67 |
68 | public static TheoryData InvalidRequests = new()
69 | {
70 | new RegisterProductRequest(null!, ValidName, ValidDescription),
71 | new RegisterProductRequest("INVALID_SKU", ValidName, ValidDescription),
72 | new RegisterProductRequest(ValidSKU, null!, ValidDescription),
73 | };
74 | }
75 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api.Tests/Products/GettingProducts/GetProductsTests.cs:
--------------------------------------------------------------------------------
1 | using Ogooreck.API;
2 | using static Ogooreck.API.ApiSpecification;
3 | using Xunit;
4 |
5 | namespace Warehouse.Api.Tests.Products.GettingProducts;
6 |
7 | public class GetProductsTests: IClassFixture
8 | {
9 | private readonly GetProductsFixture API;
10 |
11 | public GetProductsTests(GetProductsFixture api) =>
12 | API = api;
13 |
14 | [Fact]
15 | public Task ValidRequest_With_NoParams_ShouldReturn_200() =>
16 | API.Given(URI("/api/products/"))
17 | .When(GET)
18 | .Then(OK, RESPONSE_BODY(API.RegisteredProducts));
19 |
20 | [Fact]
21 | public Task ValidRequest_With_Filter_ShouldReturn_SubsetOfRecords()
22 | {
23 | var registeredProduct = API.RegisteredProducts.First();
24 | var filter = registeredProduct.Sku[1..];
25 |
26 | return API.Given(URI($"/api/products/?filter={filter}"))
27 | .When(GET)
28 | .Then(OK, RESPONSE_BODY(new List { registeredProduct }));
29 | }
30 |
31 | [Fact]
32 | public Task ValidRequest_With_Paging_ShouldReturn_PageOfRecords()
33 | {
34 | // Given
35 | const int page = 2;
36 | const int pageSize = 1;
37 | var pagedRecords = API.RegisteredProducts
38 | .Skip(page - 1)
39 | .Take(pageSize)
40 | .ToList();
41 |
42 | return API.Given(URI($"/api/products/?page={page}&pageSize={pageSize}"))
43 | .When(GET)
44 | .Then(OK, RESPONSE_BODY(pagedRecords));
45 | }
46 |
47 | [Fact]
48 | public Task NegativePage_ShouldReturn_400() =>
49 | API.Given(URI($"/api/products/?page={-20}"))
50 | .When(GET)
51 | .Then(BAD_REQUEST);
52 |
53 | [Theory]
54 | [InlineData(0)]
55 | [InlineData(-20)]
56 | public Task NegativeOrZeroPageSize_ShouldReturn_400(int pageSize) =>
57 | API.Given(URI($"/api/products/?pageSize={pageSize}"))
58 | .When(GET)
59 | .Then(BAD_REQUEST);
60 | }
61 |
62 | public class GetProductsFixture: ApiSpecification, IAsyncLifetime
63 | {
64 | public List RegisteredProducts { get; } = new();
65 |
66 | public GetProductsFixture(): base(new WarehouseTestWebApplicationFactory()) { }
67 |
68 | public async Task InitializeAsync()
69 | {
70 | var productsToRegister = new[]
71 | {
72 | new RegisterProductRequest("ZX1234", "ValidName", "ValidDescription"),
73 | new RegisterProductRequest("AD5678", "OtherValidName", "OtherValidDescription"),
74 | new RegisterProductRequest("BH90210", "AnotherValid", "AnotherValidDescription")
75 | };
76 |
77 | foreach (var registerProduct in productsToRegister)
78 | {
79 | var registerResponse = await Send(
80 | new ApiRequest(POST, URI("/api/products"), BODY(registerProduct))
81 | );
82 |
83 | await CREATED(registerResponse);
84 |
85 | var createdId = registerResponse.GetCreatedId();
86 |
87 | var (sku, name, _) = registerProduct;
88 | RegisteredProducts.Add(new ProductListItem(createdId, sku!, name!));
89 | }
90 | }
91 |
92 | public Task DisposeAsync() => Task.CompletedTask;
93 | }
94 |
--------------------------------------------------------------------------------
/Mediator/Warehouse.Api.Tests/Products/GettingProducts/GetProductsTests.cs:
--------------------------------------------------------------------------------
1 | using Ogooreck.API;
2 | using Warehouse.Api.Requests;
3 | using Warehouse.Products;
4 | using static Ogooreck.API.ApiSpecification;
5 | using Xunit;
6 |
7 | namespace Warehouse.Api.Tests.Products.GettingProducts;
8 |
9 | public class GetProductsTests: IClassFixture
10 | {
11 | private readonly GetProductsFixture API;
12 |
13 | public GetProductsTests(GetProductsFixture api) =>
14 | API = api;
15 |
16 | [Fact]
17 | public Task ValidRequest_With_NoParams_ShouldReturn_200() =>
18 | API.Given(URI("/api/products/"))
19 | .When(GET)
20 | .Then(OK, RESPONSE_BODY(API.RegisteredProducts));
21 |
22 | [Fact]
23 | public Task ValidRequest_With_Filter_ShouldReturn_SubsetOfRecords()
24 | {
25 | var registeredProduct = API.RegisteredProducts.First();
26 | var filter = registeredProduct.Sku[1..];
27 |
28 | return API.Given(URI($"/api/products/?filter={filter}"))
29 | .When(GET)
30 | .Then(OK, RESPONSE_BODY(new List { registeredProduct }));
31 | }
32 |
33 | [Fact]
34 | public Task ValidRequest_With_Paging_ShouldReturn_PageOfRecords()
35 | {
36 | // Given
37 | const int page = 2;
38 | const int pageSize = 1;
39 | var pagedRecords = API.RegisteredProducts
40 | .Skip(page - 1)
41 | .Take(pageSize)
42 | .ToList();
43 |
44 | return API.Given(URI($"/api/products/?page={page}&pageSize={pageSize}"))
45 | .When(GET)
46 | .Then(OK, RESPONSE_BODY(pagedRecords));
47 | }
48 |
49 | [Fact]
50 | public Task NegativePage_ShouldReturn_400() =>
51 | API.Given(URI($"/api/products/?page={-20}"))
52 | .When(GET)
53 | .Then(BAD_REQUEST);
54 |
55 | [Theory]
56 | [InlineData(0)]
57 | [InlineData(-20)]
58 | public Task NegativeOrZeroPageSize_ShouldReturn_400(int pageSize) =>
59 | API.Given(URI($"/api/products/?pageSize={pageSize}"))
60 | .When(GET)
61 | .Then(BAD_REQUEST);
62 | }
63 |
64 | public class GetProductsFixture: ApiSpecification, IAsyncLifetime
65 | {
66 | public List RegisteredProducts { get; } = new();
67 |
68 | public GetProductsFixture(): base(new WarehouseTestWebApplicationFactory()) { }
69 |
70 | public async Task InitializeAsync()
71 | {
72 | var productsToRegister = new[]
73 | {
74 | new RegisterProductRequest("ZX1234", "ValidName", "ValidDescription"),
75 | new RegisterProductRequest("AD5678", "OtherValidName", "OtherValidDescription"),
76 | new RegisterProductRequest("BH90210", "AnotherValid", "AnotherValidDescription")
77 | };
78 |
79 | foreach (var registerProduct in productsToRegister)
80 | {
81 | var registerResponse = await Send(
82 | new ApiRequest(POST, URI("/api/products"), BODY(registerProduct))
83 | );
84 |
85 | await CREATED(registerResponse);
86 |
87 | var createdId = registerResponse.GetCreatedId();
88 |
89 | var (sku, name, _) = registerProduct;
90 | RegisteredProducts.Add(new ProductListItem(createdId, sku!, name!));
91 | }
92 | }
93 |
94 | public Task DisposeAsync() => Task.CompletedTask;
95 | }
96 |
--------------------------------------------------------------------------------
/ApplicationService/Warehouse.Api.Tests/Products/GettingProducts/GetProductsTests.cs:
--------------------------------------------------------------------------------
1 | using Ogooreck.API;
2 | using Warehouse.Api.Requests;
3 | using Warehouse.Products;
4 | using static Ogooreck.API.ApiSpecification;
5 | using Xunit;
6 |
7 | namespace Warehouse.Api.Tests.Products.GettingProducts;
8 |
9 | public class GetProductsTests: IClassFixture
10 | {
11 | private readonly GetProductsFixture API;
12 |
13 | public GetProductsTests(GetProductsFixture api) =>
14 | API = api;
15 |
16 | [Fact]
17 | public Task ValidRequest_With_NoParams_ShouldReturn_200() =>
18 | API.Given(URI("/api/products/"))
19 | .When(GET)
20 | .Then(OK, RESPONSE_BODY(API.RegisteredProducts));
21 |
22 | [Fact]
23 | public Task ValidRequest_With_Filter_ShouldReturn_SubsetOfRecords()
24 | {
25 | var registeredProduct = API.RegisteredProducts.First();
26 | var filter = registeredProduct.Sku[1..];
27 |
28 | return API.Given(URI($"/api/products/?filter={filter}"))
29 | .When(GET)
30 | .Then(OK, RESPONSE_BODY(new List { registeredProduct }));
31 | }
32 |
33 | [Fact]
34 | public Task ValidRequest_With_Paging_ShouldReturn_PageOfRecords()
35 | {
36 | // Given
37 | const int page = 2;
38 | const int pageSize = 1;
39 | var pagedRecords = API.RegisteredProducts
40 | .Skip(page - 1)
41 | .Take(pageSize)
42 | .ToList();
43 |
44 | return API.Given(URI($"/api/products/?page={page}&pageSize={pageSize}"))
45 | .When(GET)
46 | .Then(OK, RESPONSE_BODY(pagedRecords));
47 | }
48 |
49 | [Fact]
50 | public Task NegativePage_ShouldReturn_400() =>
51 | API.Given(URI($"/api/products/?page={-20}"))
52 | .When(GET)
53 | .Then(BAD_REQUEST);
54 |
55 | [Theory]
56 | [InlineData(0)]
57 | [InlineData(-20)]
58 | public Task NegativeOrZeroPageSize_ShouldReturn_400(int pageSize) =>
59 | API.Given(URI($"/api/products/?pageSize={pageSize}"))
60 | .When(GET)
61 | .Then(BAD_REQUEST);
62 | }
63 |
64 | public class GetProductsFixture: ApiSpecification, IAsyncLifetime
65 | {
66 | public List RegisteredProducts { get; } = new();
67 |
68 | public GetProductsFixture(): base(new WarehouseTestWebApplicationFactory()) { }
69 |
70 | public async Task InitializeAsync()
71 | {
72 | var productsToRegister = new[]
73 | {
74 | new RegisterProductRequest("ZX1234", "ValidName", "ValidDescription"),
75 | new RegisterProductRequest("AD5678", "OtherValidName", "OtherValidDescription"),
76 | new RegisterProductRequest("BH90210", "AnotherValid", "AnotherValidDescription")
77 | };
78 |
79 | foreach (var registerProduct in productsToRegister)
80 | {
81 | var registerResponse = await Send(
82 | new ApiRequest(POST, URI("/api/products"), BODY(registerProduct))
83 | );
84 |
85 | await CREATED(registerResponse);
86 |
87 | var createdId = registerResponse.GetCreatedId();
88 |
89 | var (sku, name, _) = registerProduct;
90 | RegisteredProducts.Add(new ProductListItem(createdId, sku!, name!));
91 | }
92 | }
93 |
94 | public Task DisposeAsync() => Task.CompletedTask;
95 | }
96 |
--------------------------------------------------------------------------------
/Final/Warehouse.Api.Tests/Products/GettingProducts/GetProductsTests.cs:
--------------------------------------------------------------------------------
1 | using Ogooreck.API;
2 | using static Ogooreck.API.ApiSpecification;
3 | using Warehouse.Products.GettingProducts;
4 | using Warehouse.Products.RegisteringProduct;
5 | using Xunit;
6 |
7 | namespace Warehouse.Api.Tests.Products.GettingProducts;
8 |
9 | public class GetProductsTests: IClassFixture
10 | {
11 | private readonly GetProductsFixture API;
12 |
13 | public GetProductsTests(GetProductsFixture api) =>
14 | API = api;
15 |
16 | [Fact]
17 | public Task ValidRequest_With_NoParams_ShouldReturn_200() =>
18 | API.Given(URI("/api/products/"))
19 | .When(GET)
20 | .Then(OK, RESPONSE_BODY(API.RegisteredProducts));
21 |
22 | [Fact]
23 | public Task ValidRequest_With_Filter_ShouldReturn_SubsetOfRecords()
24 | {
25 | var registeredProduct = API.RegisteredProducts.First();
26 | var filter = registeredProduct.Sku[1..];
27 |
28 | return API.Given(URI($"/api/products/?filter={filter}"))
29 | .When(GET)
30 | .Then(OK, RESPONSE_BODY(new List { registeredProduct }));
31 | }
32 |
33 | [Fact]
34 | public Task ValidRequest_With_Paging_ShouldReturn_PageOfRecords()
35 | {
36 | // Given
37 | const int page = 2;
38 | const int pageSize = 1;
39 | var pagedRecords = API.RegisteredProducts
40 | .Skip(page - 1)
41 | .Take(pageSize)
42 | .ToList();
43 |
44 | return API.Given(URI($"/api/products/?page={page}&pageSize={pageSize}"))
45 | .When(GET)
46 | .Then(OK, RESPONSE_BODY(pagedRecords));
47 | }
48 |
49 | [Fact]
50 | public Task NegativePage_ShouldReturn_400() =>
51 | API.Given(URI($"/api/products/?page={-20}"))
52 | .When(GET)
53 | .Then(BAD_REQUEST);
54 |
55 | [Theory]
56 | [InlineData(0)]
57 | [InlineData(-20)]
58 | public Task NegativeOrZeroPageSize_ShouldReturn_400(int pageSize) =>
59 | API.Given(URI($"/api/products/?pageSize={pageSize}"))
60 | .When(GET)
61 | .Then(BAD_REQUEST);
62 | }
63 |
64 |
65 | public class GetProductsFixture: ApiSpecification, IAsyncLifetime
66 | {
67 | public List RegisteredProducts { get; } = new();
68 |
69 | public GetProductsFixture(): base(new WarehouseTestWebApplicationFactory()) { }
70 |
71 | public async Task InitializeAsync()
72 | {
73 | var productsToRegister = new[]
74 | {
75 | new RegisterProductRequest("ZX1234", "ValidName", "ValidDescription"),
76 | new RegisterProductRequest("AD5678", "OtherValidName", "OtherValidDescription"),
77 | new RegisterProductRequest("BH90210", "AnotherValid", "AnotherValidDescription")
78 | };
79 |
80 | foreach (var registerProduct in productsToRegister)
81 | {
82 | var registerResponse = await Send(
83 | new ApiRequest(POST, URI("/api/products"), BODY(registerProduct))
84 | );
85 |
86 | await CREATED(registerResponse);
87 |
88 | var createdId = registerResponse.GetCreatedId();
89 |
90 | var (sku, name, _) = registerProduct;
91 | RegisteredProducts.Add(new ProductListItem(createdId, sku!, name!));
92 | }
93 | }
94 |
95 | public Task DisposeAsync() => Task.CompletedTask;
96 | }
97 |
--------------------------------------------------------------------------------
/Mediator/Warehouse/Products/Repositories/ProductsRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Warehouse.Storage;
3 |
4 | namespace Warehouse.Products;
5 |
6 | internal interface IProductRepository
7 | {
8 | ValueTask Find(ProductId productId, CancellationToken ct);
9 |
10 |
11 | Task Add(Product product, CancellationToken ct);
12 |
13 |
14 | Task Update(Product product, CancellationToken ct);
15 |
16 |
17 | Task Delete(Product product, CancellationToken ct);
18 |
19 | ValueTask ProductWithSKUExists(SKU productSKU, CancellationToken ct);
20 |
21 | ValueTask GetProductDetails(ProductId productId, CancellationToken ct);
22 |
23 | ValueTask> GetProducts(
24 | string? filter,
25 | int page,
26 | int pageSize,
27 | CancellationToken ct
28 | );
29 | }
30 |
31 | internal class ProductsRepository: IProductRepository
32 | {
33 | private readonly WarehouseDBContext dbContext;
34 |
35 | public ProductsRepository(WarehouseDBContext dbContext) =>
36 | this.dbContext = dbContext;
37 |
38 | public ValueTask ProductWithSKUExists(
39 | SKU productSKU,
40 | CancellationToken ct
41 | ) =>
42 | new(
43 | dbContext.Set().AnyAsync(
44 | product => product.Sku.Value == productSKU.Value, ct
45 | ));
46 |
47 | public ValueTask Find(ProductId productId, CancellationToken ct) =>
48 | dbContext.FindAsync(new[] { productId }, ct);
49 |
50 | public async Task Add(Product product, CancellationToken ct)
51 | {
52 | await dbContext.AddAsync(product, ct);
53 | await dbContext.SaveChangesAsync(ct);
54 | }
55 |
56 | public async Task Update(Product product, CancellationToken ct)
57 | {
58 | dbContext.Update(product);
59 | await dbContext.SaveChangesAsync(ct);
60 | }
61 |
62 | public async Task Delete(Product product, CancellationToken ct)
63 | {
64 | dbContext.Update(product);
65 | await dbContext.SaveChangesAsync(ct);
66 | }
67 |
68 | public async ValueTask GetProductDetails(ProductId productId, CancellationToken ct)
69 | {
70 | var product = await dbContext.Set().AsNoTracking()
71 | .SingleOrDefaultAsync(p => p.Id == productId, ct);
72 |
73 | if (product == null)
74 | return null;
75 |
76 | return new ProductDetails(
77 | product.Id.Value,
78 | product.Sku.Value,
79 | product.Name,
80 | product.Description
81 | );
82 | }
83 |
84 | public async ValueTask> GetProducts(
85 | string? filter,
86 | int page,
87 | int pageSize,
88 | CancellationToken ct
89 | )
90 | {
91 | var products = dbContext.Set().AsNoTracking();
92 |
93 | var filteredProducts = string.IsNullOrEmpty(filter)
94 | ? products
95 | : products
96 | .Where(p =>
97 | p.Sku.Value.Contains(filter!) ||
98 | p.Name.Contains(filter!) ||
99 | p.Description!.Contains(filter!)
100 | );
101 |
102 | var result = await filteredProducts
103 | .Skip(pageSize * (page - 1))
104 | .Take(pageSize)
105 | .ToListAsync(ct);
106 |
107 | var sth = result
108 | .Select(p => new ProductListItem(p.Id.Value, p.Sku.Value, p.Name))
109 | .ToList();
110 |
111 | return sth;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | artifacts/
46 |
47 | *_i.c
48 | *_p.c
49 | *_i.h
50 | *.ilk
51 | *.meta
52 | *.obj
53 | *.pch
54 | *.pdb
55 | *.pgc
56 | *.pgd
57 | *.rsp
58 | *.sbr
59 | *.tlb
60 | *.tli
61 | *.tlh
62 | *.tmp
63 | *.tmp_proj
64 | *.log
65 | *.vspscc
66 | *.vssscc
67 | .builds
68 | *.pidb
69 | *.svclog
70 | *.scc
71 |
72 | # Chutzpah Test files
73 | _Chutzpah*
74 |
75 | # Visual C++ cache files
76 | ipch/
77 | *.aps
78 | *.ncb
79 | *.opendb
80 | *.opensdf
81 | *.sdf
82 | *.cachefile
83 | *.VC.db
84 | *.VC.VC.opendb
85 |
86 | # Visual Studio profiler
87 | *.psess
88 | *.vsp
89 | *.vspx
90 | *.sap
91 |
92 | # TFS 2012 Local Workspace
93 | $tf/
94 |
95 | # Guidance Automation Toolkit
96 | *.gpState
97 |
98 | # ReSharper is a .NET coding add-in
99 | _ReSharper*/
100 | *.[Rr]e[Ss]harper
101 | *.DotSettings.user
102 |
103 | # JustCode is a .NET coding add-in
104 | .JustCode
105 |
106 | # TeamCity is a build add-in
107 | _TeamCity*
108 |
109 | # DotCover is a Code Coverage Tool
110 | *.dotCover
111 |
112 | # NCrunch
113 | _NCrunch_*
114 | .*crunch*.local.xml
115 | nCrunchTemp_*
116 |
117 | # MightyMoose
118 | *.mm.*
119 | AutoTest.Net/
120 |
121 | # Web workbench (sass)
122 | .sass-cache/
123 |
124 | # Installshield output folder
125 | [Ee]xpress/
126 |
127 | # DocProject is a documentation generator add-in
128 | DocProject/buildhelp/
129 | DocProject/Help/*.HxT
130 | DocProject/Help/*.HxC
131 | DocProject/Help/*.hhc
132 | DocProject/Help/*.hhk
133 | DocProject/Help/*.hhp
134 | DocProject/Help/Html2
135 | DocProject/Help/html
136 |
137 | # Click-Once directory
138 | publish/
139 |
140 | # Publish Web Output
141 | *.[Pp]ublish.xml
142 | *.azurePubxml
143 | # TODO: Comment the next line if you want to checkin your web deploy settings
144 | # but database connection strings (with potential passwords) will be unencrypted
145 | *.pubxml
146 | *.publishproj
147 |
148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
149 | # checkin your Azure Web App publish settings, but sensitive information contained
150 | # in these scripts will be unencrypted
151 | PublishScripts/
152 |
153 | # NuGet Packages
154 | *.nupkg
155 | # The packages folder can be ignored because of Package Restore
156 | # **/packages/*
157 | # except build/, which is used as an MSBuild target.
158 | !**/packages/build/
159 | # Uncomment if necessary however generally it will be regenerated when needed
160 | #!**/packages/repositories.config
161 | # NuGet v3's project.json files produces more ignoreable files
162 | *.nuget.props
163 | *.nuget.targets
164 |
165 | # Microsoft Azure Build Output
166 | csx/
167 | *.build.csdef
168 |
169 | # Microsoft Azure Emulator
170 | ecf/
171 | rcf/
172 |
173 | # Windows Store app package directories and files
174 | AppPackages/
175 | BundleArtifacts/
176 | Package.StoreAssociation.xml
177 | _pkginfo.txt
178 |
179 | # Visual Studio cache files
180 | # files ending in .cache can be ignored
181 | *.[Cc]ache
182 | # but keep track of directories ending in .cache
183 | !*.[Cc]ache/
184 |
185 | # Others
186 | ClientBin/
187 | ~$*
188 | *~
189 | *.dbmdl
190 | *.dbproj.schemaview
191 | *.pfx
192 | *.publishsettings
193 | node_modules/
194 | orleans.codegen.cs
195 |
196 | # Since there are multiple workflows, uncomment next line to ignore bower_components
197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
198 | #bower_components/
199 |
200 | # RIA/Silverlight projects
201 | Generated_Code/
202 |
203 | # Backup & report files from converting an old project file
204 | # to a newer Visual Studio version. Backup files are not needed,
205 | # because we have git ;-)
206 | _UpgradeReport_Files/
207 | Backup*/
208 | UpgradeLog*.XML
209 | UpgradeLog*.htm
210 |
211 | # SQL Server files
212 | *.mdf
213 | *.ldf
214 |
215 | # Business Intelligence projects
216 | *.rdl.data
217 | *.bim.layout
218 | *.bim_*.settings
219 |
220 | # Microsoft Fakes
221 | FakesAssemblies/
222 |
223 | # GhostDoc plugin setting file
224 | *.GhostDoc.xml
225 |
226 | # Node.js Tools for Visual Studio
227 | .ntvs_analysis.dat
228 |
229 | # Visual Studio 6 build log
230 | *.plg
231 |
232 | # Visual Studio 6 workspace options file
233 | *.opt
234 |
235 | # Visual Studio LightSwitch build output
236 | **/*.HTMLClient/GeneratedArtifacts
237 | **/*.DesktopClient/GeneratedArtifacts
238 | **/*.DesktopClient/ModelManifest.xml
239 | **/*.Server/GeneratedArtifacts
240 | **/*.Server/ModelManifest.xml
241 | _Pvt_Extensions
242 |
243 | # Paket dependency manager
244 | .paket/paket.exe
245 | paket-files/
246 |
247 | # FAKE - F# Make
248 | .fake/
249 |
250 | # JetBrains Rider
251 | .idea/
252 | *.sln.iml
253 |
254 | **/out
255 |
256 | # MACOS
257 | .DS_STORE
258 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 4
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
15 | [*.cs]
16 | csharp_indent_case_contents = true:warning
17 | csharp_indent_switch_labels = true:warning
18 | csharp_space_after_cast = false:warning
19 | csharp_space_after_keywords_in_control_flow_statements = true:warning
20 | csharp_space_between_method_declaration_parameter_list_parentheses = false:warning
21 | csharp_space_between_method_call_parameter_list_parentheses = false:warning
22 | csharp_space_before_colon_in_inheritance_clause = false:warning
23 | csharp_space_after_colon_in_inheritance_clause = true:warning
24 | csharp_space_around_binary_operators = before_and_after:warning
25 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false:warning
26 | csharp_space_between_method_call_name_and_opening_parenthesis = false:warning
27 | csharp_space_between_method_call_empty_parameter_list_parentheses = false:warning
28 | csharp_preserve_single_line_statements = false:warning
29 | csharp_preserve_single_line_blocks = true:warning
30 |
31 | ###############################
32 | # .NET Coding Conventions #
33 | ###############################
34 |
35 | [*.cs]
36 | # Organize usings
37 | dotnet_sort_system_directives_first = true:warning
38 | dotnet_separate_import_directive_groups = false:warning
39 |
40 | # this. preferences
41 | dotnet_style_qualification_for_field = false:warning
42 | dotnet_style_qualification_for_property = false:warning
43 | dotnet_style_qualification_for_method = false:warning
44 | dotnet_style_qualification_for_event = false:warning
45 |
46 | # Language keywords vs BCL types preferences
47 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning
48 | dotnet_style_predefined_type_for_member_access = true:warning
49 |
50 | # Parentheses preferences
51 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
52 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
53 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
54 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
55 |
56 | # Modifier preferences
57 | dotnet_style_require_accessibility_modifiers = always:warning
58 | dotnet_style_readonly_field = true:warning
59 |
60 | # Expression-level preferences
61 | dotnet_style_object_initializer = true:warning
62 | dotnet_style_collection_initializer = true:warning
63 | dotnet_style_explicit_tuple_names = true:suggestion
64 | dotnet_style_null_propagation = true
65 | dotnet_style_coalesce_expression = true:warning
66 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
67 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
68 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
69 | dotnet_style_prefer_auto_properties = true:warning
70 | dotnet_style_prefer_conditional_expression_over_assignment = true:warning
71 | dotnet_style_prefer_conditional_expression_over_return = true:silent
72 |
73 | ###############################
74 | # Naming Conventions #
75 | ###############################
76 |
77 | # Style Definitions
78 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case
79 |
80 | # Use PascalCase for constant fields
81 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
82 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
83 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
84 | dotnet_naming_symbols.constant_fields.applicable_kinds = field
85 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
86 | dotnet_naming_symbols.constant_fields.required_modifiers = const
87 |
88 | ###############################
89 | # C# Code Style Rules #
90 | ###############################
91 |
92 | [*.cs]
93 | # var preferences
94 | csharp_style_var_for_built_in_types = true:warning
95 | csharp_style_var_when_type_is_apparent = true:warning
96 | csharp_style_var_elsewhere = true:warning
97 |
98 | # Expression-bodied members
99 | csharp_style_expression_bodied_methods = false:silent
100 | csharp_style_expression_bodied_constructors = false:silent
101 | csharp_style_expression_bodied_operators = false:silent
102 | csharp_style_expression_bodied_properties = true:silent
103 | csharp_style_expression_bodied_indexers = true:silent
104 | csharp_style_expression_bodied_accessors = true:silent
105 |
106 | # Pattern-matching preferences
107 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning
108 | csharp_style_pattern_matching_over_as_with_null_check = true:warning
109 |
110 | # Null-checking preferences
111 | csharp_style_throw_expression = true:warning
112 | csharp_style_conditional_delegate_call = true:suggestion
113 |
114 | # Modifier preferences
115 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
116 |
117 | # Expression-level preferences
118 | csharp_prefer_braces = true:information
119 | csharp_style_deconstructed_variable_declaration = true:suggestion
120 | csharp_prefer_simple_default_expression = true:warning
121 | csharp_style_pattern_local_over_anonymous_function = true:warning
122 | csharp_style_inlined_variable_declaration = true:warning
123 |
124 | ###############################
125 | # C# Formatting Rules #
126 | ###############################
127 |
128 | # New line preferences
129 | csharp_new_line_before_open_brace = all
130 | csharp_new_line_before_else = true
131 | csharp_new_line_before_catch = true
132 | csharp_new_line_before_finally = true
133 | csharp_new_line_before_members_in_object_initializers = true
134 | csharp_new_line_before_members_in_anonymous_types = true
135 | csharp_new_line_between_query_expression_clauses = true
136 |
137 | # Indentation preferences
138 | csharp_indent_case_contents = true
139 | csharp_indent_switch_labels = true
140 | csharp_indent_labels = flush_left
141 |
142 | # Space preferences
143 | csharp_space_after_cast = false
144 | csharp_space_after_keywords_in_control_flow_statements = true
145 | csharp_space_between_method_declaration_parameter_list_parentheses = false
146 | csharp_space_between_method_call_parameter_list_parentheses = false
147 | csharp_space_between_parentheses = false
148 | csharp_space_before_colon_in_inheritance_clause = false
149 | csharp_space_after_colon_in_inheritance_clause = true
150 |
151 | csharp_space_around_binary_operators = before_and_after
152 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
153 | csharp_space_between_method_call_name_and_opening_parenthesis = false
154 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
155 |
156 | # Wrapping preferences
157 | csharp_preserve_single_line_statements = false
158 | csharp_preserve_single_line_blocks = true
159 |
160 |
161 | ###############################
162 | # Reliability Inspections #
163 | ###############################
164 |
165 | # CA2012: Use ValueTasks correctly
166 | dotnet_diagnostic.CA2012.severity = error
167 |
168 | # VSTHRD002 Avoid problematic synchronous waits
169 | dotnet_diagnostic.VSTHRD002.severity = warning
170 |
171 | # VSTHRD011 Use AsyncLazy
172 | dotnet_diagnostic.VSTHRD011.severity = warning
173 |
174 | # VSTHRD100 Avoid async void methods
175 | dotnet_diagnostic.VSTHRD100.severity = error
176 |
177 | # VSTHRD101 Avoid unsupported async delegates
178 | dotnet_diagnostic.VSTHRD101.severity = error
179 |
180 | # VSTHRD102 Implement internal logic asynchronously
181 | dotnet_diagnostic.VSTHRD102.severity = error
182 |
183 | # VSTHRD103 Call async methods when in an async method
184 | dotnet_diagnostic.VSTHRD103.severity = error
185 |
186 | # VSTHRD110 Observe result of async calls
187 | dotnet_diagnostic.VSTHRD110.severity = warning
188 |
189 | # VSTHRD111 Use .ConfigureAwait(bool)
190 | dotnet_diagnostic.VSTHRD111.severity = error
191 |
192 | # VSTHRD112 Implement System.IAsyncDisposable
193 | dotnet_diagnostic.VSTHRD112.severity = error
194 |
195 | # VSTHRD200 Use Async suffix for async methods
196 | dotnet_diagnostic.VSTHRD200.severity = none
197 |
--------------------------------------------------------------------------------
/Warehouse.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{DB50DBC4-CB42-48A9-BC9A-37E9546B481F}"
4 | ProjectSection(SolutionItems) = preProject
5 | Directory.Build.props = Directory.Build.props
6 | Core.Build.props = Core.Build.props
7 | docker-compose.yml = docker-compose.yml
8 | Dockerfile = Dockerfile
9 | EndProjectSection
10 | EndProject
11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{69AC90E1-2A1B-44AF-8E67-E30C0416D54B}"
12 | ProjectSection(SolutionItems) = preProject
13 | .github\workflows\build.dotnet.yml = .github\workflows\build.dotnet.yml
14 | .github\workflows\build.docker.yml = .github\workflows\build.docker.yml
15 | EndProjectSection
16 | EndProject
17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Final", "Final", "{5E927C43-8A4B-4D5E-A377-7638D00A1FC5}"
18 | EndProject
19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse", "Final\Warehouse\Warehouse.csproj", "{ED70BFCA-58F2-4114-A2FA-B4D85AC80DA4}"
20 | EndProject
21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api", "Final\Warehouse.Api\Warehouse.Api.csproj", "{1EE9783C-8F60-4E96-B03C-C1FE28BB9D2C}"
22 | EndProject
23 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api.Tests", "Final\Warehouse.Api.Tests\Warehouse.Api.Tests.csproj", "{1C3F40F8-FBD6-4D82-9942-4DD98F35F8AC}"
24 | EndProject
25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Empty", "Empty", "{6EBC6A17-D1D8-4B36-A55D-E0F1E0C43880}"
26 | EndProject
27 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ApplicationService", "ApplicationService", "{44AFE231-CB31-40F6-AEE3-50DF3881B1FA}"
28 | EndProject
29 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse", "ApplicationService\Warehouse\Warehouse.csproj", "{AE6EA47C-5FFA-47F9-9812-1EB4A7B36C05}"
30 | EndProject
31 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api", "ApplicationService\Warehouse.Api\Warehouse.Api.csproj", "{621EE4A5-4FE4-42AA-9E8C-CA649809A794}"
32 | EndProject
33 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api.Tests", "ApplicationService\Warehouse.Api.Tests\Warehouse.Api.Tests.csproj", "{325D2D84-9642-4B32-9B0D-617F084DCB1B}"
34 | EndProject
35 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api", "Empty\Warehouse.Api\Warehouse.Api.csproj", "{7D93DADA-1105-4DCC-868D-23AB99D3567C}"
36 | EndProject
37 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api.Tests", "Empty\Warehouse.Api.Tests\Warehouse.Api.Tests.csproj", "{4F9AF79B-950F-4598-843F-A7B725229F43}"
38 | EndProject
39 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mediator", "Mediator", "{5E939E77-ABDD-4C99-B3BD-C4C73B3B108B}"
40 | EndProject
41 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse", "Mediator\Warehouse\Warehouse.csproj", "{283547B2-B3D5-4246-A20A-EF05C63AC883}"
42 | EndProject
43 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api", "Mediator\Warehouse.Api\Warehouse.Api.csproj", "{C40B9900-8F05-4F23-ADE3-103333308BB9}"
44 | EndProject
45 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api.Tests", "Mediator\Warehouse.Api.Tests\Warehouse.Api.Tests.csproj", "{F9074808-3CF0-4CA6-BBF8-32055BE86362}"
46 | EndProject
47 | Global
48 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
49 | Debug|Any CPU = Debug|Any CPU
50 | Release|Any CPU = Release|Any CPU
51 | EndGlobalSection
52 | GlobalSection(NestedProjects) = preSolution
53 | {69AC90E1-2A1B-44AF-8E67-E30C0416D54B} = {DB50DBC4-CB42-48A9-BC9A-37E9546B481F}
54 | {ED70BFCA-58F2-4114-A2FA-B4D85AC80DA4} = {5E927C43-8A4B-4D5E-A377-7638D00A1FC5}
55 | {1EE9783C-8F60-4E96-B03C-C1FE28BB9D2C} = {5E927C43-8A4B-4D5E-A377-7638D00A1FC5}
56 | {1C3F40F8-FBD6-4D82-9942-4DD98F35F8AC} = {5E927C43-8A4B-4D5E-A377-7638D00A1FC5}
57 | {AE6EA47C-5FFA-47F9-9812-1EB4A7B36C05} = {44AFE231-CB31-40F6-AEE3-50DF3881B1FA}
58 | {621EE4A5-4FE4-42AA-9E8C-CA649809A794} = {44AFE231-CB31-40F6-AEE3-50DF3881B1FA}
59 | {325D2D84-9642-4B32-9B0D-617F084DCB1B} = {44AFE231-CB31-40F6-AEE3-50DF3881B1FA}
60 | {7D93DADA-1105-4DCC-868D-23AB99D3567C} = {6EBC6A17-D1D8-4B36-A55D-E0F1E0C43880}
61 | {4F9AF79B-950F-4598-843F-A7B725229F43} = {6EBC6A17-D1D8-4B36-A55D-E0F1E0C43880}
62 | {283547B2-B3D5-4246-A20A-EF05C63AC883} = {5E939E77-ABDD-4C99-B3BD-C4C73B3B108B}
63 | {C40B9900-8F05-4F23-ADE3-103333308BB9} = {5E939E77-ABDD-4C99-B3BD-C4C73B3B108B}
64 | {F9074808-3CF0-4CA6-BBF8-32055BE86362} = {5E939E77-ABDD-4C99-B3BD-C4C73B3B108B}
65 | EndGlobalSection
66 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
67 | {ED70BFCA-58F2-4114-A2FA-B4D85AC80DA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
68 | {ED70BFCA-58F2-4114-A2FA-B4D85AC80DA4}.Debug|Any CPU.Build.0 = Debug|Any CPU
69 | {ED70BFCA-58F2-4114-A2FA-B4D85AC80DA4}.Release|Any CPU.ActiveCfg = Release|Any CPU
70 | {ED70BFCA-58F2-4114-A2FA-B4D85AC80DA4}.Release|Any CPU.Build.0 = Release|Any CPU
71 | {1EE9783C-8F60-4E96-B03C-C1FE28BB9D2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
72 | {1EE9783C-8F60-4E96-B03C-C1FE28BB9D2C}.Debug|Any CPU.Build.0 = Debug|Any CPU
73 | {1EE9783C-8F60-4E96-B03C-C1FE28BB9D2C}.Release|Any CPU.ActiveCfg = Release|Any CPU
74 | {1EE9783C-8F60-4E96-B03C-C1FE28BB9D2C}.Release|Any CPU.Build.0 = Release|Any CPU
75 | {1C3F40F8-FBD6-4D82-9942-4DD98F35F8AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
76 | {1C3F40F8-FBD6-4D82-9942-4DD98F35F8AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
77 | {1C3F40F8-FBD6-4D82-9942-4DD98F35F8AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
78 | {1C3F40F8-FBD6-4D82-9942-4DD98F35F8AC}.Release|Any CPU.Build.0 = Release|Any CPU
79 | {AE6EA47C-5FFA-47F9-9812-1EB4A7B36C05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
80 | {AE6EA47C-5FFA-47F9-9812-1EB4A7B36C05}.Debug|Any CPU.Build.0 = Debug|Any CPU
81 | {AE6EA47C-5FFA-47F9-9812-1EB4A7B36C05}.Release|Any CPU.ActiveCfg = Release|Any CPU
82 | {AE6EA47C-5FFA-47F9-9812-1EB4A7B36C05}.Release|Any CPU.Build.0 = Release|Any CPU
83 | {621EE4A5-4FE4-42AA-9E8C-CA649809A794}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
84 | {621EE4A5-4FE4-42AA-9E8C-CA649809A794}.Debug|Any CPU.Build.0 = Debug|Any CPU
85 | {621EE4A5-4FE4-42AA-9E8C-CA649809A794}.Release|Any CPU.ActiveCfg = Release|Any CPU
86 | {621EE4A5-4FE4-42AA-9E8C-CA649809A794}.Release|Any CPU.Build.0 = Release|Any CPU
87 | {325D2D84-9642-4B32-9B0D-617F084DCB1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
88 | {325D2D84-9642-4B32-9B0D-617F084DCB1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
89 | {325D2D84-9642-4B32-9B0D-617F084DCB1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
90 | {325D2D84-9642-4B32-9B0D-617F084DCB1B}.Release|Any CPU.Build.0 = Release|Any CPU
91 | {7D93DADA-1105-4DCC-868D-23AB99D3567C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
92 | {7D93DADA-1105-4DCC-868D-23AB99D3567C}.Debug|Any CPU.Build.0 = Debug|Any CPU
93 | {7D93DADA-1105-4DCC-868D-23AB99D3567C}.Release|Any CPU.ActiveCfg = Release|Any CPU
94 | {7D93DADA-1105-4DCC-868D-23AB99D3567C}.Release|Any CPU.Build.0 = Release|Any CPU
95 | {4F9AF79B-950F-4598-843F-A7B725229F43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
96 | {4F9AF79B-950F-4598-843F-A7B725229F43}.Debug|Any CPU.Build.0 = Debug|Any CPU
97 | {4F9AF79B-950F-4598-843F-A7B725229F43}.Release|Any CPU.ActiveCfg = Release|Any CPU
98 | {4F9AF79B-950F-4598-843F-A7B725229F43}.Release|Any CPU.Build.0 = Release|Any CPU
99 | {283547B2-B3D5-4246-A20A-EF05C63AC883}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
100 | {283547B2-B3D5-4246-A20A-EF05C63AC883}.Debug|Any CPU.Build.0 = Debug|Any CPU
101 | {283547B2-B3D5-4246-A20A-EF05C63AC883}.Release|Any CPU.ActiveCfg = Release|Any CPU
102 | {283547B2-B3D5-4246-A20A-EF05C63AC883}.Release|Any CPU.Build.0 = Release|Any CPU
103 | {C40B9900-8F05-4F23-ADE3-103333308BB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
104 | {C40B9900-8F05-4F23-ADE3-103333308BB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
105 | {C40B9900-8F05-4F23-ADE3-103333308BB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
106 | {C40B9900-8F05-4F23-ADE3-103333308BB9}.Release|Any CPU.Build.0 = Release|Any CPU
107 | {F9074808-3CF0-4CA6-BBF8-32055BE86362}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
108 | {F9074808-3CF0-4CA6-BBF8-32055BE86362}.Debug|Any CPU.Build.0 = Debug|Any CPU
109 | {F9074808-3CF0-4CA6-BBF8-32055BE86362}.Release|Any CPU.ActiveCfg = Release|Any CPU
110 | {F9074808-3CF0-4CA6-BBF8-32055BE86362}.Release|Any CPU.Build.0 = Release|Any CPU
111 | EndGlobalSection
112 | EndGlobal
113 |
--------------------------------------------------------------------------------
/Empty/Warehouse.Api/Program.cs:
--------------------------------------------------------------------------------
1 | // ReSharper disable ArrangeTypeModifiers
2 | // ReSharper disable ClassNeverInstantiated.Global
3 |
4 | using System.Diagnostics.CodeAnalysis;
5 | using Microsoft.AspNetCore.Http.HttpResults;
6 | using Microsoft.EntityFrameworkCore;
7 | using Warehouse.Api.Core;
8 | using Warehouse.Api.Middlewares.ExceptionHandling;
9 |
10 | Console.WriteLine("🇸🇪 👋 Hey Swetugg! 🇸🇪 ");
11 |
12 | var builder = WebApplication.CreateBuilder(args);
13 |
14 | builder.Services
15 | .AddEndpointsApiExplorer()
16 | .AddSwaggerGen()
17 | .AddDbContext(
18 | options => options.UseNpgsql("name=ConnectionStrings:WarehouseDB")
19 | );
20 |
21 | var app = builder.Build();
22 |
23 | if (app.Environment.IsDevelopment())
24 | {
25 | app.UseSwagger()
26 | .UseSwaggerUI();
27 |
28 | app.UseExceptionHandlingMiddleware();
29 |
30 | // Kids, do not try this at home!
31 | using var scope = app.Services.CreateScope();
32 | await using var db = scope.ServiceProvider.GetRequiredService();
33 | await db.Database.MigrateAsync();
34 | }
35 |
36 | // Register new product
37 | app.MapPost(
38 | "api/products/",
39 | async Task