ASP.NET Core backend implementation for RealWorld
See the code
ASP.NET Core codebase containing real world examples (CRUD, auth, advanced patterns, etc.) that adheres to the RealWorld spec and API.
This codebase demonstrates a fully fledged application built with ASP.NET Core and feature-oriented vertical slices, including CRUD operations, authentication, routing, pagination, and more.
The implementation follows ASP.NET Core community style guides and best practices where they fit the RealWorld contract.
For information on how this works with other frontends and backends, see the RealWorld repository.
This uses ASP.NET Core with:
.editorconfig to enforce usage patternsThe basic architecture is based on this reference architecture: ContosoUniversityCore.
Install the .NET SDK specified in global.json, currently 10.0.401.
rollForward: latestFeature allows newer .NET 10 feature bands and patches; CI reads this file too.
To start locally on a fresh SQLite database:
make run-local
The Makefile generates a local-development signing key using OpenSSL and reuses it
from .jwt-signing-key (excluded from Git and Docker images). An explicitly supplied
Jwt__SigningKey takes precedence. Keep the key file to preserve sessions between runs.
Local runs use conduit-local.db in the repository root, leaving older realworld.db
files untouched. Override the path with make run-local LOCAL_DB=/path/to/conduit.db,
or supply ConnectionStrings__Conduit to use your own connection string.
The API is at http://localhost:5050/api and Swagger at http://localhost:5050/swagger.
Existing databases created by older versions
must be backed up and baselined first: see database migrations.
The main validation target formats the repository, builds the solution in Release mode, and runs the integration tests:
dotnet run --project build/build.csproj -- test
To format without building:
dotnet run --project build/build.csproj -- format
The full CI-equivalent build, test, and publish pipeline is:
dotnet run --project build/build.csproj
CI uses locked restores and checks formatting without modifying files:
dotnet restore Conduit.slnx --locked-mode
dotnet run --project build/build.csproj --no-restore -- --check-format
After changing dependencies, run dotnet restore Conduit.slnx and include the updated
packages.lock.json files. The build then enforces those lock files.
See AGENTS.md for repository layout, development conventions, and complete validation guidance.
The Makefile also sets up the local-development signing key for Docker:
make build
make run
When running directly without Make, supply Jwt__SigningKey yourself (for example,
export Jwt__SigningKey="$(openssl rand -base64 32)" for local development).
Each deployment must use its own cryptographically random key (at least 32 bytes,
base64-encoded), provided through protected configuration such as a secret manager
or the Jwt__SigningKey environment variable. Never commit it or bake it into an
image. Startup fails for missing, malformed, short, or obvious placeholder keys.
Issuer and audience are identifiers, not secrets.
Only version-2 tokens with conduit_token_version: "2" and a stable user:<id>
subject are accepted. Legacy username-subject and unversioned tokens are rejected,
even when their subject resembles a new user ID. There is no compatibility fallback.
All existing sessions must sign in again after this upgrade. New sessions survive
username changes; authentication rejects tokens belonging to deleted users.
Keep the key stable across restarts and shared only among instances of the same deployment. To rotate a compromised key, replace it on every instance and restart; all previously issued tokens become invalid and users must sign in again. Deployments using the former public default key should rotate immediately. Managed API test targets generate a fresh ephemeral key when one is not supplied.
There is a Makefile for macOS and Linux:
make build executes docker compose buildmake run executes docker compose upDocker exposes http://localhost:8080/api and /swagger, not port 5050.
The container runs as the non-root app user. Compose stores SQLite data in the
conduit-data named volume mounted at /data; data survives container replacement.
For custom bind mounts, make the database directory writable by the container user.
Local databases and the RealWorld submodule are excluded from the image build context.
The above might work for Docker on Windows.
The build is a C# project:
dotnet run --project build/build.csproj -- test
Run the API with make run-local. Swagger is available at:
http://localhost:5050/swagger
The official RealWorld API spec test collections (Hurl and Bruno) run against this implementation. The spec repository is vendored as the realworld git submodule:
make submodule fetches the spec (git submodule update --init realworld)make test-hurl-with-managed-server starts the API on a fresh SQLite database, runs the Hurl suite, and shuts it down (requires Hurl)make test-bruno-with-managed-server does the same with the Bruno collection (requires Bun); the wrapper pins Bruno CLI 4.2.1make test-hurl / make test-bruno run the suites against an already running server (make run-local)Both suites run in CI via the "RealWorld API Tests" workflow.
Managed targets use isolated temporary databases and generated signing keys, require a
successful readiness response, and clean up the server and database on exit. They do
not delete your local database. Override the port with, for example,
make test-hurl-with-managed-server API_URL=http://localhost:5098.
They require Bash, curl, OpenSSL, and .NET, but not GNU timeout.
Hurl 8.0.1 is used in CI. The pinned submodule is tested on pushes and pull requests;
a weekly Hurl canary also tests upstream HEAD without changing the committed pin.
All endpoints are rooted under /api as the spec requires; the prefix can be changed through the ApiPrefix configuration key (appsettings or an environment variable).
Configuration can be supplied through appsettings or environment variables:
| Setting | Environment variable | Default |
|---|---|---|
| Database provider | Database__Provider | sqlite (sqlserver also supported) |
| Connection string | ConnectionStrings__Conduit | Data Source=realworld.db locally; /data/realworld.db in Docker |
| Apply migrations on startup | Database__ApplyMigrations | true |
SQLite example:
export Database__Provider=sqlite
export ConnectionStrings__Conduit='Data Source=/absolute/writable/path/conduit.db'
make run-local
SQL Server example (use a secret manager for real credentials):
export Database__Provider=sqlserver
export ConnectionStrings__Conduit='Server=localhost,1433;Database=Conduit;User ID=conduit;Password=<secret>;Encrypt=True;TrustServerCertificate=True'
make run-local
TrustServerCertificate=True is for local development only. SQL Server requires an
explicit connection string and a reachable server; it does not require a Windows API
container. The old ASPNETCORE_Conduit_DatabaseProvider and
ASPNETCORE_Conduit_ConnectionString variables remain fallback aliases.
For managed deployments, apply reviewed migration scripts before starting instances
and set Database__ApplyMigrations=false. See migration guidance.
New and changed passwords use ASP.NET Core Identity's versioned PBKDF2-HMAC-SHA512 format with 210,000 iterations and a cryptographically random embedded salt. Existing HMAC passwords remain usable and are upgraded after successful login; failed logins never rewrite hashes. Rollback to an older application version will not support upgraded passwords. Database uniqueness protects usernames, emails, and article slugs, including concurrent writes. Usernames are limited to 256 characters and emails to 320 characters for SQL Server index compatibility.
430 followers · starred Jul 2020
2,035 followers · starred Nov 2020
477 followers · starred Nov 2019
60 followers · starred Jun 2020
ASP.NET Core backend implementation for RealWorld
See the code
ASP.NET Core codebase containing real world examples (CRUD, auth, advanced patterns, etc.) that adheres to the RealWorld spec and API.
This codebase demonstrates a fully fledged application built with ASP.NET Core and feature-oriented vertical slices, including CRUD operations, authentication, routing, pagination, and more.
The implementation follows ASP.NET Core community style guides and best practices where they fit the RealWorld contract.
For information on how this works with other frontends and backends, see the RealWorld repository.
This uses ASP.NET Core with:
.editorconfig to enforce usage patternsThe basic architecture is based on this reference architecture: ContosoUniversityCore.
Install the .NET SDK specified in global.json, currently 10.0.401.
rollForward: latestFeature allows newer .NET 10 feature bands and patches; CI reads this file too.
To start locally on a fresh SQLite database:
make run-local
The Makefile generates a local-development signing key using OpenSSL and reuses it
from .jwt-signing-key (excluded from Git and Docker images). An explicitly supplied
Jwt__SigningKey takes precedence. Keep the key file to preserve sessions between runs.
Local runs use conduit-local.db in the repository root, leaving older realworld.db
files untouched. Override the path with make run-local LOCAL_DB=/path/to/conduit.db,
or supply ConnectionStrings__Conduit to use your own connection string.
The API is at http://localhost:5050/api and Swagger at http://localhost:5050/swagger.
Existing databases created by older versions
must be backed up and baselined first: see database migrations.
The main validation target formats the repository, builds the solution in Release mode, and runs the integration tests:
dotnet run --project build/build.csproj -- test
To format without building:
dotnet run --project build/build.csproj -- format
The full CI-equivalent build, test, and publish pipeline is:
dotnet run --project build/build.csproj
CI uses locked restores and checks formatting without modifying files:
dotnet restore Conduit.slnx --locked-mode
dotnet run --project build/build.csproj --no-restore -- --check-format
After changing dependencies, run dotnet restore Conduit.slnx and include the updated
packages.lock.json files. The build then enforces those lock files.
See AGENTS.md for repository layout, development conventions, and complete validation guidance.
The Makefile also sets up the local-development signing key for Docker:
make build
make run
When running directly without Make, supply Jwt__SigningKey yourself (for example,
export Jwt__SigningKey="$(openssl rand -base64 32)" for local development).
Each deployment must use its own cryptographically random key (at least 32 bytes,
base64-encoded), provided through protected configuration such as a secret manager
or the Jwt__SigningKey environment variable. Never commit it or bake it into an
image. Startup fails for missing, malformed, short, or obvious placeholder keys.
Issuer and audience are identifiers, not secrets.
Only version-2 tokens with conduit_token_version: "2" and a stable user:<id>
subject are accepted. Legacy username-subject and unversioned tokens are rejected,
even when their subject resembles a new user ID. There is no compatibility fallback.
All existing sessions must sign in again after this upgrade. New sessions survive
username changes; authentication rejects tokens belonging to deleted users.
Keep the key stable across restarts and shared only among instances of the same deployment. To rotate a compromised key, replace it on every instance and restart; all previously issued tokens become invalid and users must sign in again. Deployments using the former public default key should rotate immediately. Managed API test targets generate a fresh ephemeral key when one is not supplied.
There is a Makefile for macOS and Linux:
make build executes docker compose buildmake run executes docker compose upDocker exposes http://localhost:8080/api and /swagger, not port 5050.
The container runs as the non-root app user. Compose stores SQLite data in the
conduit-data named volume mounted at /data; data survives container replacement.
For custom bind mounts, make the database directory writable by the container user.
Local databases and the RealWorld submodule are excluded from the image build context.
The above might work for Docker on Windows.
The build is a C# project:
dotnet run --project build/build.csproj -- test
Run the API with make run-local. Swagger is available at:
http://localhost:5050/swagger
The official RealWorld API spec test collections (Hurl and Bruno) run against this implementation. The spec repository is vendored as the realworld git submodule:
make submodule fetches the spec (git submodule update --init realworld)make test-hurl-with-managed-server starts the API on a fresh SQLite database, runs the Hurl suite, and shuts it down (requires Hurl)make test-bruno-with-managed-server does the same with the Bruno collection (requires Bun); the wrapper pins Bruno CLI 4.2.1make test-hurl / make test-bruno run the suites against an already running server (make run-local)Both suites run in CI via the "RealWorld API Tests" workflow.
Managed targets use isolated temporary databases and generated signing keys, require a
successful readiness response, and clean up the server and database on exit. They do
not delete your local database. Override the port with, for example,
make test-hurl-with-managed-server API_URL=http://localhost:5098.
They require Bash, curl, OpenSSL, and .NET, but not GNU timeout.
Hurl 8.0.1 is used in CI. The pinned submodule is tested on pushes and pull requests;
a weekly Hurl canary also tests upstream HEAD without changing the committed pin.
All endpoints are rooted under /api as the spec requires; the prefix can be changed through the ApiPrefix configuration key (appsettings or an environment variable).
Configuration can be supplied through appsettings or environment variables:
| Setting | Environment variable | Default |
|---|---|---|
| Database provider | Database__Provider | sqlite (sqlserver also supported) |
| Connection string | ConnectionStrings__Conduit | Data Source=realworld.db locally; /data/realworld.db in Docker |
| Apply migrations on startup | Database__ApplyMigrations | true |
SQLite example:
export Database__Provider=sqlite
export ConnectionStrings__Conduit='Data Source=/absolute/writable/path/conduit.db'
make run-local
SQL Server example (use a secret manager for real credentials):
export Database__Provider=sqlserver
export ConnectionStrings__Conduit='Server=localhost,1433;Database=Conduit;User ID=conduit;Password=<secret>;Encrypt=True;TrustServerCertificate=True'
make run-local
TrustServerCertificate=True is for local development only. SQL Server requires an
explicit connection string and a reachable server; it does not require a Windows API
container. The old ASPNETCORE_Conduit_DatabaseProvider and
ASPNETCORE_Conduit_ConnectionString variables remain fallback aliases.
For managed deployments, apply reviewed migration scripts before starting instances
and set Database__ApplyMigrations=false. See migration guidance.
New and changed passwords use ASP.NET Core Identity's versioned PBKDF2-HMAC-SHA512 format with 210,000 iterations and a cryptographically random embedded salt. Existing HMAC passwords remain usable and are upgraded after successful login; failed logins never rewrite hashes. Rollback to an older application version will not support upgraded passwords. Database uniqueness protects usernames, emails, and article slugs, including concurrent writes. Usernames are limited to 256 characters and emails to 320 characters for SQL Server index compatibility.
430 followers · starred Jul 2020
2,035 followers · starred Nov 2020
477 followers · starred Nov 2019
60 followers · starred Jun 2020