realworld-apps/aspnetcore-realworld-example-app

ASP.NET Core backend implementation for RealWorld

C#

2,093

345 commits

updated Oct 14, 2026

See the code

README

RealWorld Example App

ASP.NET Core codebase containing real world examples (CRUD, auth, advanced patterns, etc.) that adheres to the RealWorld spec and API.

RealWorld

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.

How it works

This uses ASP.NET Core with:

The basic architecture is based on this reference architecture: ContosoUniversityCore.

Getting started

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.

Docker Build

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 build
  • make run executes docker compose up

Docker 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.

Local building

The build is a C# project:

dotnet run --project build/build.csproj -- test

Local API

Run the API with make run-local. Swagger is available at:

http://localhost:5050/swagger

RealWorld API spec tests

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.1
  • make 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).

Database configuration

Configuration can be supplied through appsettings or environment variables:

SettingEnvironment variableDefault
Database providerDatabase__Providersqlite (sqlserver also supported)
Connection stringConnectionStrings__ConduitData Source=realworld.db locally; /data/realworld.db in Docker
Apply migrations on startupDatabase__ApplyMigrationstrue

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.

Password storage

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.

GitHub Actions build

Build and Test

asp-net-core
csharp
entity-framework-core
net-core
realworld

realworld-apps/aspnetcore-realworld-example-app

ASP.NET Core backend implementation for RealWorld

C#

2,093

345 commits

updated Oct 14, 2026

See the code

README

RealWorld Example App

ASP.NET Core codebase containing real world examples (CRUD, auth, advanced patterns, etc.) that adheres to the RealWorld spec and API.

RealWorld

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.

How it works

This uses ASP.NET Core with:

The basic architecture is based on this reference architecture: ContosoUniversityCore.

Getting started

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.

Docker Build

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 build
  • make run executes docker compose up

Docker 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.

Local building

The build is a C# project:

dotnet run --project build/build.csproj -- test

Local API

Run the API with make run-local. Swagger is available at:

http://localhost:5050/swagger

RealWorld API spec tests

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.1
  • make 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).

Database configuration

Configuration can be supplied through appsettings or environment variables:

SettingEnvironment variableDefault
Database providerDatabase__Providersqlite (sqlserver also supported)
Connection stringConnectionStrings__ConduitData Source=realworld.db locally; /data/realworld.db in Docker
Apply migrations on startupDatabase__ApplyMigrationstrue

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.

Password storage

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.

GitHub Actions build

Build and Test

asp-net-core
csharp
entity-framework-core
net-core
realworld

Significant stargazers

Youssef Fahmy

430 followers · starred Jul 2020

Ben {chmark} Adams

2,035 followers · starred Nov 2020

Giorgi Dalakishvili

477 followers · starred Nov 2019

Anthony Scotti

60 followers · starred Jun 2020