{"id":42746051,"url":"https://github.com/marcellusfarias/dotnet-webapi-template","last_synced_at":"2026-04-01T19:05:05.499Z","repository":{"id":195579757,"uuid":"627602867","full_name":"marcellusfarias/dotnet-webapi-template","owner":"marcellusfarias","description":".NET 8 WebAPI template. Using Postgres, Docker, EF Core, Swagger, JWT, xUnit.","archived":false,"fork":false,"pushed_at":"2026-03-24T01:24:15.000Z","size":1327,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-24T19:06:37.168Z","etag":null,"topics":["api","api-rest","csharp","dotnet","dotnet-core","postgresql"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/marcellusfarias.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-04-13T20:08:30.000Z","updated_at":"2026-03-14T16:47:02.000Z","dependencies_parsed_at":"2026-02-16T21:02:58.018Z","dependency_job_id":"45408c5f-75b1-4e0a-a968-db8cc710d6bb","html_url":"https://github.com/marcellusfarias/dotnet-webapi-template","commit_stats":null,"previous_names":["marcellusfarias/my-buying-list","marcellusfarias/dotnet-webapi-template"],"tags_count":48,"template":false,"template_full_name":null,"purl":"pkg:github/marcellusfarias/dotnet-webapi-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcellusfarias%2Fdotnet-webapi-template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcellusfarias%2Fdotnet-webapi-template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcellusfarias%2Fdotnet-webapi-template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcellusfarias%2Fdotnet-webapi-template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/marcellusfarias","download_url":"https://codeload.github.com/marcellusfarias/dotnet-webapi-template/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcellusfarias%2Fdotnet-webapi-template/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31291041,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","api-rest","csharp","dotnet","dotnet-core","postgresql"],"created_at":"2026-01-29T19:18:53.415Z","updated_at":"2026-04-01T19:05:05.477Z","avatar_url":"https://github.com/marcellusfarias.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Dotnet WebAPI Template\n\nThis project aims to be a template for new WebAPI projects. It already sets up:\n\nTech stack:\n* .NET 8\n* Swagger\n* Postgres with Entity Framework Core\n* Docker and Docker Swarm\n* xUnit, AutoFixture, NSubstitute and TestContainers\n* FluentValidation\n* BCrypt.Net\n\n## Features implemented\n\n### Architecture and DDD\n\nThis project adopts what many .NET influencers commonly refer to as \"Clean Architecture\" with Domain-Driven Design (DDD). The organizational structure draws inspiration from the accumulated knowledge of various projects developed over time.\n\nThe solution is split into four projects:\n\n* **Domain** — entities, value objects, domain constants and domain exceptions.\n* **Application** — use-case services, DTOs, validators, mappers and interfaces. No framework dependencies.\n* **Infrastructure** — EF Core, JWT, repositories and seeders. Implements the interfaces defined in Application.\n* **Web** — ASP.NET controllers, middlewares, filters and startup configuration.\n\n### API Contracts and Documentation\n\nThis template comes pre-configured with Swagger UI for API documentation. It maps all possible status codes that may be returned from each endpoint, along with the required input payloads and the corresponding response objects. Swagger is served at the application root (`/`).\n\n### Docker\n\nThis project comes pre-configured with Dockerfiles, aiming to deploy to production in Docker Swarm mode and develop locally with Docker Compose. See `docker-compose.yml`, `docker-compose.development.yml` and `docker-compose.production.yml`.\n\n### Middlewares, Filters and Exception/Error Handling\n\n**Request validation filter**\n\nFluentValidation is used via the `RequestBodyValidationFilter` class to validate user input data (represented as DTOs). This filter was created to replace FluentValidation.AspNetCore automatic validation, since FluentValidation itself [does not recommend using it](https://docs.fluentvalidation.net/en/latest/aspnet.html). The filter adds async support for automatic validation.\n\nRequest buffering is also enabled globally so the filter can read the request body more than once.\n\n**Exception handling middleware**\n\n`ErrorHandlingMiddleware` sits at the top of the pipeline and handles all unhandled exceptions:\n\n* `OperationCanceledException` — the request is aborted cleanly without an error response.\n* Any exception implementing `IFormattedResponseException` — the middleware uses the exception's `StatusCode` and `ErrorResponse` to return a structured JSON error body.\n* All other exceptions — a `500 Internal Server Error` is returned with no body (to avoid leaking internals).\n\nThe following custom exceptions are provided and all implement `IFormattedResponseException`:\n\n| Exception | HTTP Status |\n|---|---|\n| `BadRequestException` | 400 |\n| `AuthenticationException` | 401 |\n| `AccountLockedException` | 401 |\n| `ResourceNotFoundException` | 404 |\n| `BusinessLogicException` | 409 |\n| `DatabaseException` | 500 |\n| `InternalServerErrorException` | 500 |\n\n### Rate Limiting\n\nRate limiting is applied **only on the authentication endpoint** via `AuthenticationRateLimiterPolicy`, using the built-in ASP.NET Core rate limiter (`Microsoft.AspNetCore.RateLimiting`).\n\nThe policy uses a **fixed window** algorithm partitioned by client IP address. When the limit is exceeded the middleware returns `429 Too Many Requests`.\n\nConfiguration is driven by `CustomRateLimiterOptions`, read from `appsettings.json` under the `CustomRateLimiterOptions` key:\n\n| Option | Description |\n|---|---|\n| `PermitLimit` | Number of requests allowed per window |\n| `QueueLimit` | Number of requests queued when the limit is reached |\n| `Window` | Window duration in seconds |\n\n### Authentication and Authorization\n\n**Authentication**\n\nJWT Bearer authentication is used. The JWT setup lives under `Infrastructure/Authentication/JwtSetup`.\n\n**Authorization**\n\nPolicy-based authorization is implemented with the following components:\n\n* `PermissionRequirement` — wraps a single permission string.\n* `HasPermissionAttribute` — decorates controller actions with the required permission name.\n* `PermissionAuthorizationHandler` — validates that the JWT contains a claim matching the required permission.\n* `PermissionAuthorizationPolicyProvider` — builds policies on demand so they don't all need to be registered at startup.\n\nRequest flow:\n\n1. The controller action declares a required permission via `HasPermissionAttribute`.\n2. `PermissionAuthorizationPolicyProvider` builds the corresponding policy.\n3. `PermissionAuthorizationHandler` checks the JWT claims.\n4. The handler marks the context as succeeded or failed.\n\nPermissions are defined as constants in the Domain layer (`Policies` class) and are seeded into the database via migrations, which keeps code and database in sync.\n\n### Account Lockout\n\nAccount lockout protects against brute-force attacks on the login endpoint. Failed attempts are tracked directly on the `User` entity (`FailedLoginAttempts`, `LockoutEnd`).\n\nOnce the configured number of failed attempts is reached the account is locked until `LockoutEnd` elapses. Subsequent login attempts — even with the correct password — return `401 Unauthorized` with a \"temporarily locked\" message via `AccountLockedException`.\n\nConfiguration is read from `appsettings.json` under the `LockoutOptions` key:\n\n| Option | Default | Description |\n|---|---|---|\n| `MaxFailedAttempts` | `5` | Number of consecutive failures before locking |\n| `LockoutDurationMinutes` | `15` | How long the account stays locked |\n\n### Database and EFCore\n\nThis template uses Postgres alongside EFCore and comes configured with migrations. Migrations are **applied automatically on startup** before the application begins serving requests.\n\n* Entity Framework is configured to use **eager loading** and **no-tracking** by default.\n* Cancellation tokens are passed through every repository call.\n* Passwords are stored using the **BCrypt** algorithm (hash + salt).\n\n**Admin user seeder**\n\nOn every startup, `AdminUserSeeder` runs after migrations. It creates the admin user (with a fixed ID) if it does not exist yet, or updates the stored email and password hash if the credentials configured in `AdminSettings` have changed since the last run. This keeps the admin account in sync with the deployment configuration without requiring a manual migration.\n\nAdmin credentials are read from `AdminSettings` (`AdminSettings__Email` and `AdminSettings__Password` environment variables / Docker secrets).\n\n### Logging\n\nLog configuration is set in `appsettings.{ENVIRONMENT}.json`. See the [Microsoft logging documentation](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-8.0#aspnet-core-and-ef-core-categories) for available categories and levels.\n\n### Network\n\nAccording to [Microsoft](https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-8.0\u0026tabs=visual-studio%2Clinux-ubuntu), APIs should either not listen on HTTP or close the connection with status code 400. Therefore, this application is configured to listen on **HTTPS only**. This is enforced in three places: `launchSettings.json`, `Dockerfile` and Docker Compose files.\n\n### Testing\n\nThere are three test projects:\n\n* **`MyBuyingList.Application.Tests`** — unit tests for services, validators and mappers using xUnit, AutoFixture and NSubstitute.\n* **`MyBuyingList.Infrastructure.Tests`** — unit tests for infrastructure concerns (e.g. JWT token generation).\n* **`MyBuyingList.Web.Tests`** — integration tests for the full HTTP stack. Tests spin up a real Postgres instance via TestContainers (`postgres:14`) and use `WebApplicationFactory` to host the application in-process.\n\n## How to\n\n### Prerequisites\n\n* [.NET 8 SDK](https://dotnet.microsoft.com/download)\n* [Docker](https://www.docker.com/)\n\n### Run locally\n\n1. Clone the repository.\n2. Populate the secret files under `MyBuyingList/local/secrets/` (see the `.csproj` for the expected file names: `JwtSettings__SigningKey`, `ConnectionStrings__DefaultConnection`, `AdminSettings__Email`, `AdminSettings__Password`).\n3. Start Postgres (and pgAdmin) using Docker Compose. There are two ways:\n   - **Via the `.dcproj` file** — open `docker-compose.dcproj` in Visual Studio or Rider, which will manage the compose lifecycle for you.\n   - **Via the CLI:**\n     ```bash\n     docker compose -f docker-compose.development.yml up -d\n     ```\n   \u003e **Before running**, open `docker-compose.development.yml` and review the volume mappings under the `api` service. The file is pre-configured for **macOS** (`~/.aspnet/dev-certs/https`). If you are on **Windows**, comment out the macOS lines and uncomment the Windows lines as shown in the comments inside the file.\n4. Run the application. Choose one of two approaches:\n   - **API inside Docker** (recommended — API and DB share the same network): Docker Compose already handles this; no further steps needed.\n   - **API outside Docker** (`dotnet run`): open `MyBuyingList/appsettings.Development.json`, copy the connection string commented as `// If running locally (not container)`, and paste it as the content of `MyBuyingList/local/secrets/ConnectionStrings__DefaultConnection`. Then run:\n     ```bash\n     dotnet run --project MyBuyingList\n     ```\n   Migrations and the admin user seed are applied automatically on startup.\n5. Open `https://localhost:{port}` to access the Swagger UI.\n\n### Run in production\n\nThe application is designed to run in **Docker Swarm** mode. The production compose setup merges `docker-compose.yml` (base configuration) and `docker-compose.production.yml` (image, secrets, TLS volumes). Secrets are stored as Docker secrets and never baked into the image.\n\n#### 1. Initialize Docker Swarm\n\n```bash\ndocker swarm init\n```\n\n#### 2. Create Docker secrets\n\n```bash\n# Connection string to your database\necho '\u003cyour-connection-string\u003e' \u003e\u003e conn_string.txt\ndocker secret create ConnectionStrings__DefaultConnection conn_string.txt\nrm conn_string.txt\n\n# JWT signing key (use a long, random string)\necho '\u003cyour-jwt-signing-key\u003e' \u003e\u003e jwt.txt\ndocker secret create JwtSettings__SigningKey jwt.txt\nrm jwt.txt\n\n# Admin account credentials\necho '\u003cadmin@yourdomain.com\u003e' \u003e\u003e admin_email.txt\ndocker secret create AdminSettings__Email admin_email.txt\nrm admin_email.txt\n\necho '\u003cyour-admin-password\u003e' \u003e\u003e admin_pwd.txt\ndocker secret create AdminSettings__Password admin_pwd.txt\nrm admin_pwd.txt\n```\n\n#### 3. Configure TLS\n\nThe application expects a TLS certificate at `/etc/letsencrypt/live/\u003cyour-domain\u003e/` on the host (mapped as a volume in `docker-compose.production.yml`). Replace `\u003cyour-domain\u003e` in `docker-compose.production.yml` with your actual domain name.\n\n**Bootstrap with a self-signed certificate** (will be replaced by a real cert in the next step):\n\n```bash\nsudo mkdir -p /etc/letsencrypt/live/\u003cyour-domain\u003e\n\nsudo openssl req -x509 -newkey rsa:4096 \\\n  -keyout /etc/letsencrypt/live/\u003cyour-domain\u003e/privkey.pem \\\n  -out /etc/letsencrypt/live/\u003cyour-domain\u003e/fullchain.pem \\\n  -days 365 -nodes \\\n  -subj \"/CN=\u003cyour-domain\u003e\"\n\nsudo chmod 600 /etc/letsencrypt/live/\u003cyour-domain\u003e/privkey.pem\nsudo chmod 644 /etc/letsencrypt/live/\u003cyour-domain\u003e/fullchain.pem\n```\n\n\u003e **Note:** Health check intentionally skips TLS verification. Reason for that is that we don't want to app to keep restarting if the certificate is not valid. We should rely on something else for alerting on certificate expiration.\n\n**Replace with a Let's Encrypt certificate** once the domain is pointing to your VPS ([Certbot instructions](https://certbot.eff.org/instructions?ws=other\u0026os=snap)):\n\n```bash\nsudo apt update \u0026\u0026 sudo apt install snapd -y\nsudo snap install --classic certbot\nsudo ln -s /snap/bin/certbot /usr/local/bin/certbot\n\nsudo certbot certonly --standalone -d \u003cyour-domain\u003e\n```\n\n#### 4. Deploy the stack\n\n```bash\ncd ~/app\ndocker stack deploy \\\n  -c docker-compose.yml \\\n  -c docker-compose.production.yml \\\n  myapp\n```\n\n### Run tests\n\n```bash\ndotnet test\n```\n\nIntegration tests require Docker to be running (TestContainers will start a Postgres container automatically).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarcellusfarias%2Fdotnet-webapi-template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarcellusfarias%2Fdotnet-webapi-template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarcellusfarias%2Fdotnet-webapi-template/lists"}