Inventory Service is a clean ASP.NET Core REST API for product inventory management.
The goal is to build one well-structured backend service first. This is not a full microservices system yet. The service focuses on REST API design, PostgreSQL persistence, Entity Framework Core, validation, error handling, Swagger, health checks, Docker, and basic testing.
- Product CRUD
- Soft delete for products
- PostgreSQL persistence
- Entity Framework Core
- EF Core migrations
- FluentValidation request validation
- Problem Details error responses
- Swagger / OpenAPI documentation
- Health endpoint
- Dockerfile for the API
- Docker Compose for local development
- Basic unit tests
- Basic integration tests with PostgreSQL
The main domain entity is Product.
Fields:
IdNameDescriptionSkuPriceQuantityInStockIsActiveCreatedAtUpdatedAt
Important rules:
IdusesGuid.Priceusesdecimal.- PostgreSQL stores
Priceasnumeric(18,2). Skuis required and unique.Nameis required.Pricemust be greater than or equal to0.QuantityInStockmust be greater than or equal to0.- Timestamps are stored in UTC.
- Products are not physically deleted.
DELETE /api/products/{id}setsIsActivetofalse.
The solution uses a simple Clean Architecture / layered architecture approach.
src/
+¦¦ InventoryService.Api/
+¦¦ InventoryService.Application/
+¦¦ InventoryService.Domain/
L¦¦ InventoryService.Infrastructure/
HTTP/API layer.
Responsibilities:
- Controllers
- Swagger setup
- Dependency injection composition
- Problem Details configuration
- Global exception handling
- Health endpoint setup
- API configuration
The API layer references:
InventoryService.ApplicationInventoryService.Infrastructure
Controllers should stay thin and should not contain database or business logic.
Application layer.
Responsibilities:
- DTOs
- request models
- response models
- validators
- application services
- application interfaces
- application-level business logic
The Application layer references:
InventoryService.Domain
The Application layer does not depend on Infrastructure.
Domain layer.
Responsibilities:
- domain entities
- domain constants
- simple domain rules
The Domain layer does not depend on any other project.
Infrastructure layer.
Responsibilities:
- EF Core
DbContext - PostgreSQL persistence
- entity configuration
- EF Core migrations
- repository implementations
- system clock implementation
The Infrastructure layer references:
InventoryService.ApplicationInventoryService.Domain
- .NET 10
- ASP.NET Core Web API
- Controllers
- PostgreSQL
- Entity Framework Core
- EF Core migrations
- FluentValidation
- Problem Details
- Swagger / OpenAPI
- Health checks
- Docker
- Docker Compose
- xUnit
- Testcontainers for integration tests
- ASP.NET Core Web API
- Controllers
- REST API
- Clean Architecture / layered architecture
- PostgreSQL
- Entity Framework Core
- EF Core migrations
- DTOs
- FluentValidation
- Problem Details
- Swagger / OpenAPI
- Health checks
- Docker
- Docker Compose
- Dependency Injection
- Unit tests
- Integration tests
Install:
- .NET 10 SDK
- Docker Desktop or another Docker-compatible runtime
- EF Core CLI tool
Check .NET:
dotnet --versionInstall EF Core CLI tool if needed:
dotnet tool install --global dotnet-efUpdate EF Core CLI tool if already installed:
dotnet tool update --global dotnet-efRestore and build the solution:
dotnet restore
dotnet buildStart PostgreSQL only:
docker compose up -d postgresApply EF Core migrations:
dotnet ef database update \
--project src/InventoryService.Infrastructure/InventoryService.Infrastructure.csproj \
--startup-project src/InventoryService.Api/InventoryService.Api.csproj \
--context InventoryDbContextRun the API locally:
dotnet run --project src/InventoryService.Api/InventoryService.Api.csprojThe local API should be available at:
http://localhost:5080
Swagger should be available at:
http://localhost:5080/swagger
Health endpoint:
http://localhost:5080/health
Run the full local stack:
docker compose up --buildThis starts:
- PostgreSQL
- Inventory Service API
The API should be available at:
http://localhost:8080
Swagger:
http://localhost:8080/swagger
Health endpoint:
http://localhost:8080/health
Stop containers:
docker compose downStop containers and remove the PostgreSQL volume:
docker compose down -vUse -v only when you intentionally want to delete local database data.
PostgreSQL runs through Docker Compose.
Default local database values:
Database: inventorydb
Username: inventory_user
Password: inventory_password
Port: 5432
The API uses different host values depending on where it runs:
Local dotnet run:
Host=localhost
Docker Compose:
Host=postgres
Inside Docker Compose, the API connects to PostgreSQL by service name:
postgres
Migrations are stored in the Infrastructure project:
src/InventoryService.Infrastructure/Persistence/Migrations
Create a new migration:
dotnet ef migrations add MigrationName \
--project src/InventoryService.Infrastructure/InventoryService.Infrastructure.csproj \
--startup-project src/InventoryService.Api/InventoryService.Api.csproj \
--context InventoryDbContext \
--output-dir Persistence/MigrationsApply migrations:
dotnet ef database update \
--project src/InventoryService.Infrastructure/InventoryService.Infrastructure.csproj \
--startup-project src/InventoryService.Api/InventoryService.Api.csproj \
--context InventoryDbContextMigrations are not automatically applied on application startup in this project.
That is intentional for the first version. It keeps database changes explicit and easier to understand.
Main configuration files:
src/InventoryService.Api/appsettings.json
src/InventoryService.Api/appsettings.Development.json
docker-compose.yml
Local development connection string:
ConnectionStrings:InventoryDatabase
Docker Compose overrides it with:
ConnectionStrings__InventoryDatabase
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/products |
List products |
GET |
/api/products/{id} |
Get product by ID |
POST |
/api/products |
Create product |
PUT |
/api/products/{id} |
Update product |
DELETE |
/api/products/{id} |
Soft delete product |
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
API and PostgreSQL health check |
GET /api/products supports:
- pagination
- filtering
- sorting
Query parameters:
| Parameter | Description |
|---|---|
page |
Page number |
pageSize |
Number of items per page |
name |
Filter by product name |
sku |
Filter by SKU |
isActive |
Filter by active/inactive status |
sortBy |
Sort field |
sortDirection |
Sort direction |
Supported sortBy values:
Name
Price
CreatedAt
QuantityInStock
Supported sortDirection values:
Asc
Desc
Example:
curl "http://localhost:8080/api/products?page=1&pageSize=10&sortBy=CreatedAt&sortDirection=Desc"curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{
"name": "Mechanical Keyboard",
"description": "Compact keyboard with hot-swappable switches",
"sku": "KEYBOARD-001",
"price": 129.99,
"quantityInStock": 25
}'curl http://localhost:8080/api/products/YOUR_PRODUCT_IDcurl "http://localhost:8080/api/products?page=1&pageSize=10"curl -X PUT http://localhost:8080/api/products/YOUR_PRODUCT_ID \
-H "Content-Type: application/json" \
-d '{
"name": "Mechanical Keyboard Pro",
"description": "Updated keyboard",
"price": 149.99,
"quantityInStock": 20,
"isActive": true
}'curl -X DELETE http://localhost:8080/api/products/YOUR_PRODUCT_IDThis does not remove the database row.
It sets:
IsActive = false
Request validation is implemented with FluentValidation.
Create product validation:
Nameis required.Namehas a maximum length.Skuis required.Skuhas a maximum length.Pricemust be greater than or equal to0.QuantityInStockmust be greater than or equal to0.
Update product validation:
Nameis required.Namehas a maximum length.Pricemust be greater than or equal to0.QuantityInStockmust be greater than or equal to0.
SKU is not updateable in the initial version.
The API uses global exception handling and Problem Details responses.
Handled cases:
| Case | Status code |
|---|---|
| Validation error | 400 Bad Request |
| Product not found | 404 Not Found |
| Duplicate SKU | 409 Conflict |
| Unexpected server error | 500 Internal Server Error |
Validation errors return ValidationProblemDetails.
Other errors return ProblemDetails.
Unexpected errors do not expose internal exception details.
The API exposes:
GET /health
The health endpoint checks:
- API process
- PostgreSQL connection through EF Core
Example:
curl http://localhost:8080/healthExpected status:
Healthy
The solution contains:
tests/
+¦¦ InventoryService.Tests.Unit/
L¦¦ InventoryService.Tests.Integration/
Run all tests:
dotnet testRun unit tests only:
dotnet test tests/InventoryService.Tests.Unit/InventoryService.Tests.Unit.csprojRun integration tests only:
dotnet test tests/InventoryService.Tests.Integration/InventoryService.Tests.Integration.csprojIntegration tests use a temporary PostgreSQL container.
Docker must be running before integration tests are executed.
This project intentionally uses ASP.NET Core Controllers to practice classic REST API structure.
The project uses a simple Clean Architecture style:
API � Application � Domain
API � Infrastructure � Application � Domain
This keeps responsibilities clear without adding unnecessary complexity.
The project uses product-specific abstractions instead of a generic repository.
This keeps the code explicit and easier to understand.
Migrations are applied manually.
This keeps database changes visible and avoids surprising startup behavior.
Products are deactivated with IsActive = false.
This preserves product history and avoids accidental data loss.
Mappings are explicit.
This keeps the project simple and avoids adding extra abstraction too early.
This project intentionally avoids advanced microservice patterns.
Not included:
- authentication
- authorization
- message brokers
- distributed tracing
- service discovery
- API gateway
- Redis
- OpenSearch
- Kubernetes
- cloud services
- CQRS
- MediatR
- event sourcing
- background jobs
These are useful topics, but they are intentionally left for future projects.
The goal here is to build one clean REST service first.
Possible next improvements:
- authentication and authorization
- API versioning
- separate readiness and liveness health endpoints
- structured request logging improvements
- CI pipeline
- test coverage report
- more integration tests
- pagination metadata headers
- product activation endpoint
- optimistic concurrency
- production deployment configuration
- container health check for the API
- OpenTelemetry tracing
Build:
dotnet buildRun API locally:
dotnet run --project src/InventoryService.Api/InventoryService.Api.csprojRun Docker Compose:
docker compose up --buildApply migrations:
dotnet ef database update \
--project src/InventoryService.Infrastructure/InventoryService.Infrastructure.csproj \
--startup-project src/InventoryService.Api/InventoryService.Api.csproj \
--context InventoryDbContextRun tests:
dotnet testThis project currently represents a single clean REST service.
It is intentionally not a full microservices system yet.
The service can later become one part of a larger microservices portfolio.