Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
4c233d7
chore(deps): Bump Swashbuckle.AspNetCore from 8.1.4 to 10.0.1
dependabot[bot] Nov 25, 2025
e2b1139
chore(deps): Bump Microsoft.NET.Test.Sdk from 17.14.1 to 18.0.1
dependabot[bot] Nov 25, 2025
a23e95d
chore(deps): Bump actions/setup-dotnet from 5.0.0 to 5.0.1
dependabot[bot] Nov 25, 2025
878a07f
Merge branch 'master' into dependabot/nuget/src/Dotnet.Samples.AspNet…
nanotaboada Nov 25, 2025
0d067f0
Merge branch 'master' into dependabot/nuget/test/Dotnet.Samples.AspNe…
nanotaboada Nov 25, 2025
22de7c0
docs: enhance copilot-instructions with commands and workflows
nanotaboada Nov 25, 2025
e964ae4
Merge pull request #309 from nanotaboada/feature/custom-instructions-…
nanotaboada Nov 25, 2025
608de90
Merge branch 'master' into dependabot/nuget/test/Dotnet.Samples.AspNe…
nanotaboada Nov 26, 2025
8300420
Merge pull request #306 from nanotaboada/dependabot/nuget/test/Dotnet…
nanotaboada Nov 26, 2025
8d75e2d
Merge branch 'master' into dependabot/github_actions/actions/setup-do…
nanotaboada Nov 26, 2025
e933554
Merge pull request #308 from nanotaboada/dependabot/github_actions/ac…
nanotaboada Nov 26, 2025
b9a568b
chore(deps): Bump Serilog.Settings.Configuration from 9.0.0 to 10.0.0
dependabot[bot] Nov 27, 2025
a046300
Merge pull request #311 from nanotaboada/dependabot/nuget/src/Dotnet.…
nanotaboada Nov 27, 2025
91a6b3a
Merge branch 'dependabot/nuget/src/Dotnet.Samples.AspNetCore.WebApi/S…
nanotaboada Nov 27, 2025
be1d022
fix: update OpenApi namespace for Swashbuckle v10 compatibility
nanotaboada Nov 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 114 additions & 6 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,11 +250,119 @@ return player; // Let caller handle null
- Use route parameters for resource identification
- Apply validation before processing requests

## 🛠️ Essential Commands & Workflows

### Build & Run
```bash
# Build the solution
dotnet build

# Run the API (hot reload enabled)
dotnet watch run --project src/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csproj

# Access Swagger UI (Development only)
# https://localhost:9000/swagger/index.html

# Health check endpoint
# https://localhost:9000/health
```

### Testing
```bash
# Run all tests
dotnet test

# Run tests with coverage
dotnet test --results-directory "coverage" --collect:"XPlat Code Coverage" --settings .runsettings

# Run specific test category
dotnet test --filter "Category=Unit"
```

### Database Migrations
```bash
# Create a new migration
dotnet ef migrations add <MigrationName> --project src/Dotnet.Samples.AspNetCore.WebApi

# Apply migrations
dotnet ef database update --project src/Dotnet.Samples.AspNetCore.WebApi

# Regenerate database with seed data
./scripts/run-migrations-and-copy-database.sh
```

**Important**: The `run-migrations-and-copy-database.sh` script:
- Resets the placeholder database file
- Runs all migrations
- Copies the generated database from `bin/Debug/net8.0/Data/` to `Data/`
- Requires `dotnet ef` CLI tool installed globally

### Docker Operations
```bash
# Build the image
docker compose build

# Start the app (with persistent volume)
docker compose up

# Stop the app (preserve data)
docker compose down

# Reset database (removes volume)
docker compose down -v
```

**Important**: The SQLite database is stored in a Docker volume for persistence. First run copies a pre-seeded database from the image to the volume.

### Rate Limiting
- Configured via `RateLimiter` section in `appsettings.json`
- Default: 60 requests per 60 seconds (fixed window)
- Queue limit: 0 (immediate rejection when limit reached)

## 🚨 Common Issues & Workarounds

### Database Path Issues
- **SQLite database location**: `storage/players-sqlite3.db` relative to binary output
- **Container storage**: `/storage/players-sqlite3.db` (mounted volume)
- **Environment variable**: `STORAGE_PATH` can override the default path in containers

### Validation Patterns
- **FluentValidation** runs in the validator class for input format/structure
- **Business rule validation** (e.g., unique squad number check) happens in the service layer
- This separation is intentional to keep validators focused on data structure, not business logic

### Locking & Caching
- **DbContextPool** is used for performance - don't manually dispose DbContext
- **IMemoryCache** is cleared on data modifications using `Remove(CacheKey_RetrieveAsync)`
- Cache keys use `nameof()` for type safety

### Test Configuration
- Test coverage excludes test projects via `.runsettings` configuration
- Coverage reports merge multiple Cobertura files into one
- `FluentAssertions` and `Moq` are standard testing libraries

## 📝 Commit Message Conventions

Follow **Conventional Commits** (<https://www.conventionalcommits.org/>):
- `feat:` - New features
- `fix:` - Bug fixes
- `chore:` - Maintenance tasks
- `docs:` - Documentation changes
- `test:` - Test additions or modifications
- `refactor:` - Code restructuring without behavior change

**Constraints**:
- Header max length: 80 characters
- Body max line length: 80 characters
- Enforced via `commitlint.config.mjs` in CI/CD

## 🚀 Future Evolution Considerations

- **Database Migration**: SQLite → PostgreSQL transition path
- **Authentication**: JWT Bearer token implementation ready
- **API Versioning**: URL-based versioning strategy
- **OpenAPI**: Comprehensive Swagger documentation
- **Monitoring**: Health checks and metrics endpoints
- **Containerization**: Docker multi-stage builds optimized
See open issues on GitHub for planned enhancements:
- **Clean Architecture Refactoring** ([#266](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/issues/266)) - Migrate to Clean Architecture-inspired structure
- **PostgreSQL Support** ([#249](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/issues/249)) - Add PostgreSQL to Docker Compose setup
- **.NET Aspire Integration** ([#256](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/issues/256)) - Evaluate Aspire for dev-time orchestration and observability
- **JWT Authentication** ([#105](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/issues/105)) - Implement Client Credentials Flow for protected routes
- **Global Exception Handling** ([#184](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/issues/184)) - Add middleware with RFC 7807 Problem Details
- **Optimistic Concurrency** ([#65](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/issues/65)) - Handle conflicts with application-managed tokens
- **Database Normalization** ([#125](https://github.com/nanotaboada/Dotnet.Samples.AspNetCore.WebApi/issues/125)) - Extract Position, Team, League into separate tables
2 changes: 1 addition & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
uses: actions/[email protected]

- name: Set up .NET ${{ env.DOTNET_VERSION }}
uses: actions/[email protected].0
uses: actions/[email protected].1
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
# The action searches for packages.lock.json in the repository root,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Dotnet.Samples.AspNetCore.WebApi.Configurations
Expand All @@ -25,23 +25,17 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
return;

// Add security requirement (shows the lock icon)
operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(
new OpenApiSecurityRequirement
{
// In Microsoft.OpenApi 2.x, use OpenApiSecuritySchemeReference instead of OpenApiSecurityScheme with nested Reference
if (operation is OpenApiOperation openApiOperation)
{
openApiOperation.Security ??= new List<OpenApiSecurityRequirement>();
openApiOperation.Security.Add(
new OpenApiSecurityRequirement
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Id = "Bearer",
Type = ReferenceType.SecurityScheme
}
},
Array.Empty<string>()
{ new OpenApiSecuritySchemeReference("Bearer", null), new List<string>() }
}
}
);
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
<PackageReference Include="FluentValidation" Version="12.1.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using Dotnet.Samples.AspNetCore.WebApi.Validators;
using FluentValidation;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi;
using Serilog;

namespace Dotnet.Samples.AspNetCore.WebApi.Extensions;
Expand Down
14 changes: 2 additions & 12 deletions src/Dotnet.Samples.AspNetCore.WebApi/Utilities/SwaggerUtilities.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.Reflection;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi;

namespace Dotnet.Samples.AspNetCore.WebApi.Utilities;

Expand Down Expand Up @@ -58,17 +58,7 @@ public static OpenApiSecurityRequirement ConfigureSecurityRequirement()
{
return new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Id = "Bearer",
Type = ReferenceType.SecurityScheme
}
},
Array.Empty<string>()
}
{ new OpenApiSecuritySchemeReference("Bearer", null), new List<string>() }
};
}
}
72 changes: 38 additions & 34 deletions src/Dotnet.Samples.AspNetCore.WebApi/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,13 @@
},
"Serilog.Settings.Configuration": {
"type": "Direct",
"requested": "[9.0.0, )",
"resolved": "9.0.0",
"contentHash": "4/Et4Cqwa+F88l5SeFeNZ4c4Z6dEAIKbu3MaQb2Zz9F/g27T5a3wvfMcmCOaAiACjfUb4A6wrlTVfyYUZk3RRQ==",
"requested": "[10.0.0, )",
"resolved": "10.0.0",
"contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Binder": "9.0.0",
"Microsoft.Extensions.DependencyModel": "9.0.0",
"Serilog": "4.2.0"
"Microsoft.Extensions.Configuration.Binder": "10.0.0",
"Microsoft.Extensions.DependencyModel": "10.0.0",
"Serilog": "4.3.0"
}
},
"Serilog.Sinks.Console": {
Expand All @@ -160,14 +160,14 @@
},
"Swashbuckle.AspNetCore": {
"type": "Direct",
"requested": "[8.1.4, )",
"resolved": "8.1.4",
"contentHash": "qYk8VHyvs6wML+KXtjyCgS9Aj18mcm0ZtnJeNCTlj/DYQ7A3pfLIztQgLuZS/LEMYsrTo1lSKR3IIZ5/HzVCWA==",
"requested": "[10.0.1, )",
"resolved": "10.0.1",
"contentHash": "177+JNAV5TNvy8gLCdrcWBY9n2jdkxiHQDY4vhaExeqUpKrOqDatCcm/kW3kze60GqfnZ2NobD/IKiAPOL+CEw==",
"dependencies": {
"Microsoft.Extensions.ApiDescription.Server": "6.0.5",
"Swashbuckle.AspNetCore.Swagger": "8.1.4",
"Swashbuckle.AspNetCore.SwaggerGen": "8.1.4",
"Swashbuckle.AspNetCore.SwaggerUI": "8.1.4"
"Microsoft.Extensions.ApiDescription.Server": "8.0.0",
"Swashbuckle.AspNetCore.Swagger": "10.0.1",
"Swashbuckle.AspNetCore.SwaggerGen": "10.0.1",
"Swashbuckle.AspNetCore.SwaggerUI": "10.0.1"
}
},
"Humanizer": {
Expand Down Expand Up @@ -850,8 +850,8 @@
},
"Microsoft.Extensions.ApiDescription.Server": {
"type": "Transitive",
"resolved": "6.0.5",
"contentHash": "Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw=="
"resolved": "8.0.0",
"contentHash": "jDM3a95WerM8g6IcMiBXq1qRS9dqmEUpgnCk2DeMWpPkYtp1ia+CkXabOnK93JmhVlUmv8l9WMPsCSUm+WqkIA=="
},
"Microsoft.Extensions.Caching.Abstractions": {
"type": "Transitive",
Expand Down Expand Up @@ -892,10 +892,11 @@
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "9.0.0",
"contentHash": "RiScL99DcyngY9zJA2ROrri7Br8tn5N4hP4YNvGdTN/bvg1A3dwvDOxHnNZ3Im7x2SJ5i4LkX1uPiR/MfSFBLQ==",
"resolved": "10.0.0",
"contentHash": "tMF9wNh+hlyYDWB8mrFCQHQmWHlRosol1b/N2Jrefy1bFLnuTlgSYmPyHNmz8xVQgs7DpXytBRWxGhG+mSTp0g==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.0"
"Microsoft.Extensions.Configuration": "10.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.0"
}
},
"Microsoft.Extensions.Configuration.FileExtensions": {
Expand Down Expand Up @@ -925,11 +926,11 @@
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "9.0.11",
"contentHash": "DaBLlKcD5AYFLEeX7M07Q0vWOEBd86KYXOb+5ZRdQ1jYtN39cJd6fftxdNbRazEYQc9QqsAZiqKb9ub0gA+q+Q==",
"resolved": "10.0.0",
"contentHash": "RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==",
"dependencies": {
"System.Text.Encodings.Web": "9.0.11",
"System.Text.Json": "9.0.11"
"System.Text.Encodings.Web": "10.0.0",
"System.Text.Json": "10.0.0"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
Expand Down Expand Up @@ -1017,8 +1018,11 @@
},
"Microsoft.OpenApi": {
"type": "Transitive",
"resolved": "1.6.23",
"contentHash": "tZ1I0KXnn98CWuV8cpI247A17jaY+ILS9vvF7yhI0uPPEqF4P1d7BWL5Uwtel10w9NucllHB3nTkfYTAcHAh8g=="
"resolved": "2.3.0",
"contentHash": "5RZpjyt0JMmoc/aEgY9c1vE5pusdDGvkPl9qKIy9KFbRiIXD+w7gBJxX+unSjzzOcfgRoYxnO4okZyqDAL2WEw==",
"dependencies": {
"System.Text.Json": "8.0.5"
}
},
"Microsoft.VisualStudio.Web.CodeGeneration": {
"type": "Transitive",
Expand Down Expand Up @@ -1262,8 +1266,8 @@
},
"Serilog": {
"type": "Transitive",
"resolved": "4.2.0",
"contentHash": "gmoWVOvKgbME8TYR+gwMf7osROiWAURterc6Rt2dQyX7wtjZYpqFiA/pY6ztjGQKKV62GGCyOcmtP1UKMHgSmA=="
"resolved": "4.3.0",
"contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ=="
},
"Serilog.Extensions.Hosting": {
"type": "Transitive",
Expand Down Expand Up @@ -1334,24 +1338,24 @@
},
"Swashbuckle.AspNetCore.Swagger": {
"type": "Transitive",
"resolved": "8.1.4",
"contentHash": "w83aYEBJYNa6ZYomziwZWwXhqQPLKhZH0n8MzqqNhF1ElCGBKm71kd7W6pgIr/yu0i6ymQzrZUFSZLdvH1kY5w==",
"resolved": "10.0.1",
"contentHash": "HJYFSP18YF1Z6LCwunL+v8wuZUzzvcjarB8AJna/NVVIpq11FH9BW/D/6abwigu7SsKRbisStmk8xu2mTsxxHg==",
"dependencies": {
"Microsoft.OpenApi": "1.6.23"
"Microsoft.OpenApi": "2.3.0"
}
},
"Swashbuckle.AspNetCore.SwaggerGen": {
"type": "Transitive",
"resolved": "8.1.4",
"contentHash": "aBwO2MF1HHAaWgdBwX8tlSqxycOKTKmCT6pEpb0oSY1pn7mUdmzJvHZA0HxWx9nfmKP0eOGQcLC9ZnN/MuehRQ==",
"resolved": "10.0.1",
"contentHash": "vMMBDiTC53KclPs1aiedRZnXkoI2ZgF5/JFr3Dqr8KT7wvIbA/MwD+ormQ4qf25gN5xCrJbmz/9/Z3RrpSofMA==",
"dependencies": {
"Swashbuckle.AspNetCore.Swagger": "8.1.4"
"Swashbuckle.AspNetCore.Swagger": "10.0.1"
}
},
"Swashbuckle.AspNetCore.SwaggerUI": {
"type": "Transitive",
"resolved": "8.1.4",
"contentHash": "mTn6OwB43ETrN6IgAZd7ojWGhTwBZ98LT3QwbAn6Gg3wJStQV4znU0mWiHaKFlD/+Qhj1uhAUOa52rmd6xmbzg=="
"resolved": "10.0.1",
"contentHash": "a2eLI/fCxJ3WH+H1hr7Q2T82ZBk20FfqYBEZ9hOr3f+426ZUfGU2LxYWzOJrf5/4y6EKShmWpjJG01h3Rc+l6Q=="
},
"System.CodeDom": {
"type": "Transitive",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</PropertyGroup>

<ItemGroup Label="Test dependencies">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" PrivateAssets="all" />
<PackageReference Include="Moq" Version="4.20.72" PrivateAssets="all" />
<PackageReference Include="FluentAssertions" Version="8.8.0" PrivateAssets="all" />
<PackageReference Include="xunit" Version="2.9.3" PrivateAssets="all" />
Expand Down
Loading