A production-ready ASP.NET Core API built with Clean Architecture, CQRS with MediatR, OpenIddict (OAuth 2.0 / OpenID Connect), PostgreSQL, and the Repository pattern.
src/
├── NetApi.Domain/ # Innermost layer — zero external dependencies
│ ├── Common/ # BaseEntity, shared abstractions
│ ├── Entities/ # Product, Customer, Order, OrderItem
│ ├── Enums/ # OrderStatus
│ └── Interfaces/ # IRepository<T>
├── NetApi.Application/ # Use cases — depends on Domain
│ ├── Common/
│ │ ├── Interfaces/ # IApplicationDbContext
│ │ └── Behaviors/ # ValidationBehavior (MediatR pipeline)
│ ├── Products/ # CQRS: Commands + Queries + DTOs
│ ├── Customers/ # CQRS: Commands + Queries + DTOs
│ ├── Orders/ # CQRS: Commands + Queries + DTOs
│ └── DependencyInjection.cs
├── NetApi.Infrastructure/ # EF Core, Identity, OpenIddict — depends on Application
│ ├── Data/
│ │ ├── AppDbContext.cs
│ │ ├── Configurations/ # EF Fluent API configs
│ │ └── Migrations/ # EF migrations
│ ├── Repositories/ # Repository<T> implementation
│ ├── Identity/ # ApplicationUser, OpenIddictSeeder
│ └── DependencyInjection.cs
└── NetApi.Api/ # Composition root — depends on Infrastructure
├── Controllers/ # ProductsController, CustomersController, OrdersController
└── Program.cs # DI wiring, middleware, OpenIddict config
Domain ← Application ← Infrastructure ← Api
Each layer can only reference layers inward. The Domain layer has zero dependencies.
| Component | Library |
|---|---|
| Runtime | .NET 10.0 |
| Architecture | Clean Architecture (Hexagonal) |
| CQRS | MediatR 14.1 |
| Validation | FluentValidation 12.1 |
| Mapping | Mapster 10.0 |
| ORM | Entity Framework Core 10.0 (PostgreSQL) |
| Auth | OpenIddict 7.5 (OAuth 2.0 + OpenID Connect) |
| Identity | ASP.NET Core Identity |
| API Docs | Swashbuckle 10.2 (Swagger) |
- .NET 10 SDK
- PostgreSQL 16+
dotnet efCLI tool:
dotnet tool install --global dotnet-efgit clone <repo-url> NetApi
cd NetApi
dotnet buildEdit src/NetApi.Api/appsettings.json:
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=netapi;Username=postgres;Password=postgres"
}dotnet ef database update --project src/NetApi.Infrastructure --startup-project src/NetApi.ApiThis creates all tables for:
- Domain entities (
Products,Customers,Orders,OrderItems) - ASP.NET Core Identity (
AspNetUsers,AspNetRoles, etc.) - OpenIddict stores (
OpenIddictApplications,OpenIddictTokens, etc.)
dotnet run --project src/NetApi.ApiThe API starts at https://localhost:5001 (or the port in launchSettings.json).
Swagger UI: https://localhost:5001/swagger
This project uses OpenIddict as the OAuth 2.0 / OpenID Connect server. On first startup, the seeder creates:
| Client ID | Secret | Grant Type | Description |
|---|---|---|---|
password-client |
password-secret-456 |
password + refresh_token |
First-party apps (mobile/web) |
api-client |
api-secret-123 |
client_credentials |
Machine-to-machine |
| Password | Role | |
|---|---|---|
admin@netapi.dev |
Admin@123! |
Admin |
Password flow (user login):
curl -X POST https://localhost:5001/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=password-client" \
-d "client_secret=password-secret-456" \
-d "grant_type=password" \
-d "username=admin@netapi.dev" \
-d "password=Admin@123!" \
-d "scope=api"Client credentials flow (machine-to-machine):
curl -X POST https://localhost:5001/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=api-client" \
-d "client_secret=api-secret-123" \
-d "grant_type=client_credentials" \
-d "scope=api"Refresh token:
curl -X POST https://localhost:5001/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=password-client" \
-d "client_secret=password-secret-456" \
-d "grant_type=refresh_token" \
-d "refresh_token=<your-refresh-token>"curl -X GET https://localhost:5001/api/products \
-H "Authorization: Bearer <access-token>"All endpoints require Authorization: Bearer <token>.
| Method | Path | Description |
|---|---|---|
GET |
/api/products |
List all products |
GET |
/api/products/{id} |
Get product by ID |
POST |
/api/products |
Create product |
PUT |
/api/products/{id} |
Update product |
DELETE |
/api/products/{id} |
Delete product |
POST/PUT body:
{
"name": "Widget",
"description": "A useful widget",
"price": 19.99,
"stock": 100
}| Method | Path | Description |
|---|---|---|
GET |
/api/customers |
List all customers |
GET |
/api/customers/{id} |
Get customer by ID |
POST |
/api/customers |
Create customer |
POST body:
{
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phone": "+1234567890"
}| Method | Path | Description |
|---|---|---|
GET |
/api/orders |
List all orders (includes items) |
GET |
/api/orders/{id} |
Get order by ID |
POST |
/api/orders |
Create order |
POST body:
{
"customerId": "guid-here",
"items": [
{ "productId": "guid-here", "quantity": 2 }
]
}# Build
dotnet build
# Run
dotnet run --project src/NetApi.Api
# Add migration
dotnet ef migrations add <Name> --project src/NetApi.Infrastructure --startup-project src/NetApi.Api -o Data/Migrations
# Apply migrations
dotnet ef database update --project src/NetApi.Infrastructure --startup-project src/NetApi.Api
# Remove last migration
dotnet ef migrations remove --project src/NetApi.Infrastructure --startup-project src/NetApi.Api- Create entity in
NetApi.Domain/Entities/ - Add
DbSet<T>toIApplicationDbContextandAppDbContext - Add EF configuration in
NetApi.Infrastructure/Data/Configurations/ - Create CQRS in
NetApi.Application/{Entity}/ - Create controller in
NetApi.Api/Controllers/ - Add migration
The OpenIddict setup is in NetApi.Infrastructure/DependencyInjection.cs. Token lifetimes, grant types, and endpoint URIs can be modified there.
Replace AddDevelopmentEncryptionCertificate() / AddDevelopmentSigningCertificate() with AddEncryptionCertificate() / AddSigningCertificate() using production X.509 certificates stored in the machine store or Azure Key Vault.
MIT