A modular .NET 10 Web API starter where every feature is independently removable.
Each feature is a self-contained class library registered via extension methods in Program.cs. Removing a feature is two steps:
- Delete the extension method call(s) in
Program.cs - Remove the project reference
No cascading breakage. The remaining modules continue to build and run without modification. This is verified by architecture tests and 19-module removal smoke tests.
- Structured logging via Serilog with two-stage bootstrap (captures startup crashes)
- Configurable sinks: Console, File, Seq, OpenTelemetry (OTLP / Azure Application Insights)
- Request logging with correlation IDs, client IP, user agent enrichment
- All sink configuration driven entirely by
appsettings.json-- no code changes to enable/disable
- ASP.NET Identity user/role/claim store backed by EF Core
- JWT Bearer token issuance and validation
- Google OAuth external authentication (no-op when credentials absent)
- PolicyScheme with
ForwardDefaultSelectorrouting JWT vs cookie auth - All three auth layers enabled by default; each independently removable
- EF Core 10 with SQLite as the zero-config development default
- Swappable to SQL Server or PostgreSQL via configuration
- Separate migration assemblies per provider (no conflicts)
- Repository pattern wrapping DbContext
- Auto-migrate on startup (configurable)
- OpenAPI 3.1 document generation with Scalar interactive API docs
- API versioning via URL segment (
/api/v1/,/api/v2/) with sample controllers - FluentValidation with RFC 7807 Problem Details integration
- CORS policies configurable via
appsettings.json(dev/prod profiles)
- Rate limiting: fixed window, sliding window, and token bucket policies (global + per-endpoint)
- Caching:
IMemoryCache+IDistributedCache(in-memory default, swappable to Redis) - Response compression: Gzip/Brotli (opt-in, disabled by default for HTTPS security)
- Standardized response envelope: opt-in via
[WrapResponse]attribute (not global middleware)
- Global exception handler using
IExceptionHandlerreturning RFC 7807 Problem Details - Stack traces included in Development, hidden in Production
/health-- aggregate status/health/ready-- readiness probe/health/live-- liveness probe- Database connectivity check and sample external service check included
- Integration tests using
WebApplicationFactory<Program> - Unit tests with sample service-layer coverage
- Architecture tests enforcing module isolation via NetArchTest
- Module removal smoke tests proving all 19 modules can be independently removed
No global.json is included -- the project builds with any .NET 10+ SDK.
# Clone the repository
git clone <repo-url> && cd web-api
# Set the JWT signing key (required)
dotnet user-secrets set "Jwt:SecretKey" "your-256-bit-secret-key-here-min-32-chars" \
--project src/Starter.WebApi
# Run the application
dotnet run --project src/Starter.WebApiThe application starts at https://localhost:5101 (HTTPS) or http://localhost:5100 (HTTP).
Visit the interactive API documentation at https://localhost:5101/scalar/v1 (v2 also available at /scalar/v2).
The SQLite database (starter.db) is created and migrated automatically on first run -- no database setup required.
This is a template repository. After cloning, rename the Starter prefix to your own project name (e.g., Acme) using the provided script. It updates all namespaces, project references, folder names, file names, configuration values, and scripts in one command.
PowerShell (Windows):
./scripts/rename-project.ps1 -NewPrefix AcmeBash (Linux/macOS):
./scripts/rename-project.sh AcmeThe script will:
- Clean build artifacts (
bin/,obj/,.vs/) - Replace
Starterandstarterin all source files (.cs,.csproj,.slnx,.json,.ps1,.sh,.md) - Rename
.csprojfiles, project directories, and the.slnxsolution file - Run a verification build to confirm everything compiles
After the rename, delete the old SQLite database if it exists (starter.db) and reopen your IDE to refresh caches.
Options:
| Parameter | Description |
|---|---|
-NewPrefix / $1 |
New prefix name (required, must be a valid C# identifier) |
-OldPrefix / $2 |
Previous prefix to replace (default: Starter, useful for re-renaming) |
-SkipBuild / --skip-build |
Skip the verification build after renaming |
The solution uses the newer .slnx format (Starter.WebApi.slnx).
web-api/
|-- src/
| |-- Starter.WebApi/ # Host application (Program.cs, controllers)
| |-- Starter.Shared/ # Cross-module contracts (response envelope, interfaces)
| |
| |-- Starter.Auth.Shared/ # PolicyScheme + ForwardDefaultSelector
| |-- Starter.Auth.Identity/ # ASP.NET Identity with EF Core stores
| |-- Starter.Auth.Jwt/ # JWT Bearer validation + token service
| |-- Starter.Auth.Google/ # Google OAuth external provider
| |
| |-- Starter.Data/ # EF Core DbContext, repository pattern
| |-- Starter.Data.Migrations.Sqlite/ # SQLite migration assembly
| |-- Starter.Data.Migrations.SqlServer/ # SQL Server migration assembly
| |-- Starter.Data.Migrations.PostgreSql/ # PostgreSQL migration assembly
| |
| |-- Starter.Logging/ # Serilog pipeline + request logging
| |-- Starter.ExceptionHandling/ # Global exception handler + Problem Details
| |-- Starter.HealthChecks/ # Health check registrations + endpoints
| |
| |-- Starter.OpenApi/ # OpenAPI 3.1 + Scalar UI
| |-- Starter.Versioning/ # API versioning (URL segment)
| |-- Starter.Validation/ # FluentValidation integration
| |-- Starter.Cors/ # CORS policies from config
| |
| |-- Starter.RateLimiting/ # Rate limiting policies from config
| |-- Starter.Caching/ # IMemoryCache + IDistributedCache
| |-- Starter.Compression/ # Gzip/Brotli response compression
| |-- Starter.Responses/ # Standardized response envelope filter
|
|-- tests/
| |-- Starter.WebApi.Tests.Integration/ # WebApplicationFactory integration tests
| |-- Starter.WebApi.Tests.Unit/ # Service-layer unit tests
| |-- Starter.WebApi.Tests.Architecture/ # NetArchTest module isolation + removal smoke tests
|
|-- scripts/
| |-- rename-project.sh / .ps1 # Rename project prefix (Starter -> YourName)
| |-- add-migration.sh / .ps1 # EF Core migration helper
| |-- update-database.sh / .ps1 # Database update helper
All runtime behavior is configured via appsettings.json using strongly-typed IOptions<T> bindings with ValidateDataAnnotations and ValidateOnStart. Misconfiguration is caught at startup, not at request time.
| Section | Purpose |
|---|---|
Database |
Provider selection (Sqlite, SqlServer, PostgreSql), auto-migrate, retry policy |
ConnectionStrings |
Connection strings keyed by provider name |
Jwt |
Signing key, issuer, audience, token lifetime |
Authentication:Google |
Google OAuth client credentials (optional) |
Cors |
Allowed origins, methods, headers, credentials |
OpenApi |
API doc title, description, Scalar toggle |
RateLimiting |
Global limits, fixed/sliding/token bucket policy parameters |
Caching |
Default expiration, sliding expiration, Redis connection |
Compression |
HTTPS compression toggle, Brotli/Gzip compression levels |
HealthChecks |
External service URI, timeout |
Serilog |
Minimum levels, enrichers, sink configuration (Console, File, Seq, OpenTelemetry) |
ExceptionHandling |
Stack trace visibility in development |
See src/Starter.WebApi/appsettings.json for the full configuration with inline documentation.
The default provider is SQLite with zero configuration. To switch providers:
- Set
Database:Providerinappsettings.jsontoSqlite,SqlServer, orPostgreSql - Update the corresponding connection string in
ConnectionStrings - Run
dotnet run-- auto-migrate applies pending migrations on startup
{
"Database": {
"Provider": "SqlServer"
},
"ConnectionStrings": {
"SqlServer": "Server=myserver;Database=MyDb;Trusted_Connection=True;"
}
}Migration helper scripts are provided in the scripts/ directory:
# Add a new migration
./scripts/add-migration.sh MigrationName
# Update the database manually
./scripts/update-database.shEach provider has its own migration assembly (Starter.Data.Migrations.Sqlite, .SqlServer, .PostgreSql), so migrations for different providers never conflict.
This is the key differentiator. Every module follows the same pattern for removal.
- In
src/Starter.WebApi/Program.cs, delete the extension method call:
// Delete this line:
builder.AddAppGoogle(); // Google OAuth (no-op when credentials absent)- Remove the project reference from
src/Starter.WebApi/Starter.WebApi.csproj:
<!-- Delete this line: -->
<ProjectReference Include="..\Starter.Auth.Google\Starter.Auth.Google.csproj" />- Remove the using directive from
Program.cs:
// Delete this line:
using Starter.Auth.Google;- Build and run:
dotnet build # Succeeds
dotnet run --project src/Starter.WebApi # Runs without Google authAll modules follow this same pattern. No other files need modification.
| Module | Extension Method(s) | Config Section |
|---|---|---|
| Starter.Auth.Shared | AddAppAuthShared() |
-- |
| Starter.Auth.Identity | AddAppIdentity() |
-- |
| Starter.Auth.Jwt | AddAppJwt() |
Jwt |
| Starter.Auth.Google | AddAppGoogle() |
Authentication:Google |
| Starter.Caching | AddAppCaching() |
Caching |
| Starter.Compression | AddAppCompression() / UseAppCompression() |
Compression |
| Starter.Cors | AddAppCors() |
Cors |
| Starter.Data | AddAppData() / UseAppData() |
Database, ConnectionStrings |
| Starter.Data.Migrations.Sqlite | -- | ConnectionStrings |
| Starter.Data.Migrations.SqlServer | -- | ConnectionStrings |
| Starter.Data.Migrations.PostgreSql | -- | ConnectionStrings |
| Starter.ExceptionHandling | AddAppExceptionHandling() / UseAppExceptionHandling() |
ExceptionHandling |
| Starter.HealthChecks | AddAppHealthChecks() / UseAppHealthChecks() |
HealthChecks |
| Starter.Logging | AddAppLogging() / UseAppRequestLogging() |
Serilog |
| Starter.OpenApi | AddAppOpenApi() / UseAppOpenApi() |
OpenApi |
| Starter.RateLimiting | AddAppRateLimiting() / UseAppRateLimiting() |
RateLimiting |
| Starter.Responses | AddAppResponses() |
-- |
| Starter.Validation | AddAppValidation() |
-- |
| Starter.Versioning | AddAppVersioning() |
-- |
Note: Modules without a config section are zero-configuration. Modules with Use*() methods register middleware in the HTTP pipeline. Migration assemblies are provider-specific and have no extension methods. Compression is opt-in -- uncomment in Program.cs to enable.
# Run all tests from the solution root
dotnet test| Test Project | Coverage |
|---|---|
Starter.WebApi.Tests.Integration |
Health checks, auth flows, CRUD operations via WebApplicationFactory |
Starter.WebApi.Tests.Unit |
Service-layer logic, validators, isolated component tests |
Starter.WebApi.Tests.Architecture |
Module isolation enforcement (NetArchTest), 19-module removal smoke tests |
Development -- Use .NET User Secrets (never committed to source control):
dotnet user-secrets set "Jwt:SecretKey" "your-256-bit-secret-key-here-min-32-chars" \
--project src/Starter.WebApi
dotnet user-secrets set "Authentication:Google:ClientId" "your-client-id" \
--project src/Starter.WebApiEnvironment Variables -- Use double-underscore for nested keys:
export Jwt__SecretKey="your-secret-key"
export Authentication__Google__ClientId="your-client-id"
export Caching__RedisConnectionString="localhost:6379"Production -- Use Azure Key Vault or environment variables. Never commit secrets to appsettings.json.
See the comments in src/Starter.WebApi/appsettings.json for per-section guidance.