diff --git a/.codacy.yml b/.codacy.yml
index 38b1635..be01c73 100644
--- a/.codacy.yml
+++ b/.codacy.yml
@@ -21,6 +21,7 @@ exclude_paths:
- '**/*Program.cs' # Main entry point, often not useful for static analysis
# Ignore specific folders across any depth in the project
+ - '**/Configurations/**' # Swagger options, etc
- '**/Data/**' # Repositories, DbContext, database file, etc.
- '**/Enums/**' # Enumeration types
- '**/Mappings/**' # AutoMapper profiles
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 51d6333..c470083 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,19 +3,22 @@
version: 2
updates:
- package-ecosystem: "nuget"
- directory: "/"
+ directory: "/src/Dotnet.Samples.AspNetCore.WebApi"
schedule:
interval: "daily"
groups:
efcore:
patterns:
- - "Microsoft.EntityFrameworkCore*"
- xunit:
- patterns:
- - "xunit*"
- coverlet:
+ - "Microsoft.EntityFrameworkCore*"
+ serilog:
patterns:
- - "coverlet*"
+ - "Serilog*"
+
+ - package-ecosystem: "nuget"
+ directory: "/test/Dotnet.Samples.AspNetCore.WebApi.Tests"
+ schedule:
+ interval: "daily"
+
- package-ecosystem: "github-actions"
directory: "/"
schedule:
diff --git a/README.md b/README.md
index eabc231..58b7d5f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# 🧪 Web API made with .NET 8 (LTS) and ASP.NET Core 8.0
+# 🧪 Web API made with .NET 8 (LTS) and ASP.NET Core
## Status
@@ -16,7 +16,7 @@
## About
-Proof of Concept for a Web API made with [ASP.NET Core 8.0](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0) targeting [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8) inspired by the official guide: [Create a web API with ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-web-api?view=aspnetcore-8.0&tabs=visual-studio-code).
+Proof of Concept for a Web API made with [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8) (LTS) and [ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0).
## Start
@@ -30,7 +30,7 @@ dotnet watch run --project src/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.A
https://localhost:9000/swagger/index.html
```
-
+
## Credits
diff --git a/assets/images/Swagger.png b/assets/images/Swagger.png
new file mode 100644
index 0000000..2b47e02
Binary files /dev/null and b/assets/images/Swagger.png differ
diff --git a/codecov.yml b/codecov.yml
index f289b69..d7ac00c 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -55,6 +55,7 @@ ignore:
- '**/LICENSE'
- '**/README.md'
+ - .*\/Configurations\/.*
- .*\/Data\/.*
- .*\/Enums\/.*
- .*\/Mappings\/.*
diff --git a/docs/Swagger.png b/docs/Swagger.png
deleted file mode 100644
index 1cf5c5d..0000000
Binary files a/docs/Swagger.png and /dev/null differ
diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs
new file mode 100644
index 0000000..a78b116
--- /dev/null
+++ b/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs
@@ -0,0 +1,47 @@
+using System.Linq;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace Dotnet.Samples.AspNetCore.WebApi.Configurations
+{
+ ///
+ /// Adds the Bearer security requirement only to operations that have [Authorize] attributes.
+ ///
+ public class AuthorizeCheckOperationFilter : IOperationFilter
+ {
+ public void Apply(OpenApiOperation operation, OperationFilterContext context)
+ {
+ // Check if [Authorize] is applied at the method or class level
+ var hasAuthorize =
+ context.MethodInfo.GetCustomAttributes(true).OfType().Any()
+ || context
+ .MethodInfo.DeclaringType?.GetCustomAttributes(true)
+ .OfType()
+ .Any() == true;
+
+ // If there's no [Authorize] attribute, skip adding the security requirement
+ if (!hasAuthorize)
+ return;
+
+ // Add security requirement (shows the lock icon)
+ operation.Security ??= new List();
+ operation.Security.Add(
+ new OpenApiSecurityRequirement
+ {
+ {
+ new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
+ {
+ Id = "Bearer",
+ Type = ReferenceType.SecurityScheme
+ }
+ },
+ Array.Empty()
+ }
+ }
+ );
+ }
+ }
+}
diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/SwaggerDocOptions.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/SwaggerDocOptions.cs
new file mode 100644
index 0000000..f099a25
--- /dev/null
+++ b/src/Dotnet.Samples.AspNetCore.WebApi/Configurations/SwaggerDocOptions.cs
@@ -0,0 +1,68 @@
+using System.Reflection;
+using Microsoft.OpenApi.Models;
+
+namespace Dotnet.Samples.AspNetCore.WebApi.Configurations;
+
+///
+/// Provides centralized configuration methods for Swagger/OpenAPI docs.
+/// Includes XML comments path resolution and security (JWT Bearer) setup.
+///
+public static class SwaggerGenDefaults
+{
+ ///
+ /// Resolves the path to the XML comments file generated from code
+ /// documentation.
+ /// This is used to enrich the Swagger UI with method summaries and remarks.
+ ///
+ /// Full file path to the XML documentation file.
+ public static string ConfigureXmlCommentsFilePath()
+ {
+ return Path.Combine(
+ AppContext.BaseDirectory,
+ $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"
+ );
+ }
+
+ ///
+ /// Configures the OpenAPI security definition for JWT Bearer authentication.
+ /// This will show the padlock icon in Swagger UI and allow users to
+ /// authenticate.
+ ///
+ /// An describing the Bearer token format.
+ public static OpenApiSecurityScheme ConfigureSecurityDefinition()
+ {
+ return new OpenApiSecurityScheme
+ {
+ Name = "Authorization",
+ Type = SecuritySchemeType.Http,
+ Scheme = "bearer",
+ BearerFormat = "JWT",
+ In = ParameterLocation.Header,
+ Description = "Enter your JWT token below. Example: Bearer {token}"
+ };
+ }
+
+ ///
+ /// Adds a global security requirement to the Swagger spec that applies
+ /// the Bearer definition.
+ /// Routes decorated with [Authorize] will be marked as protected in the UI.
+ ///
+ /// An referencing the Bearer definition.
+ public static OpenApiSecurityRequirement ConfigureSecurityRequirement()
+ {
+ return new OpenApiSecurityRequirement
+ {
+ {
+ new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
+ {
+ Id = "Bearer",
+ Type = ReferenceType.SecurityScheme
+ }
+ },
+ Array.Empty()
+ }
+ };
+ }
+}
diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs
index 06bf789..d32f923 100644
--- a/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs
+++ b/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs
@@ -8,7 +8,7 @@
namespace Dotnet.Samples.AspNetCore.WebApi.Controllers;
[ApiController]
-[Route("[controller]")]
+[Route("players")]
[Produces("application/json")]
public class PlayerController(
IPlayerService playerService,
@@ -94,13 +94,12 @@ public async Task GetAsync()
}
///
- /// Retrieves a Player by its ID
+ /// Retrieves a Player by its internal Id (GUID)
///
- /// The ID of the Player
+ /// The internal Id (GUID) of the Player
/// OK
/// Not Found
- [Authorize(Roles = "Admin")]
- [ApiExplorerSettings(IgnoreApi = true)]
+ [Authorize]
[HttpGet("{id:Guid}", Name = "RetrieveById")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
@@ -125,7 +124,7 @@ public async Task GetByIdAsync([FromRoute] Guid id)
/// The Squad Number of the Player
/// OK
/// Not Found
- [HttpGet("squadNumber/{squadNumber:int}", Name = "RetrieveBySquadNumber")]
+ [HttpGet("{squadNumber:int}", Name = "RetrieveBySquadNumber")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task GetBySquadNumberAsync([FromRoute] int squadNumber)
@@ -134,7 +133,7 @@ public async Task GetBySquadNumberAsync([FromRoute] int squadNumber)
if (player != null)
{
logger.LogInformation(
- "GET /players/squad/{SquadNumber} retrieved: {@Player}",
+ "GET /players/{SquadNumber} retrieved: {@Player}",
squadNumber,
player
);
@@ -142,7 +141,7 @@ public async Task GetBySquadNumberAsync([FromRoute] int squadNumber)
}
else
{
- logger.LogWarning("GET /players/squad/{SquadNumber} not found", squadNumber);
+ logger.LogWarning("GET /players/{SquadNumber} not found", squadNumber);
return TypedResults.NotFound();
}
}
diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Program.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Program.cs
index fb2e912..df8376a 100644
--- a/src/Dotnet.Samples.AspNetCore.WebApi/Program.cs
+++ b/src/Dotnet.Samples.AspNetCore.WebApi/Program.cs
@@ -1,4 +1,4 @@
-using System.Reflection;
+using Dotnet.Samples.AspNetCore.WebApi.Configurations;
using Dotnet.Samples.AspNetCore.WebApi.Data;
using Dotnet.Samples.AspNetCore.WebApi.Mappings;
using Dotnet.Samples.AspNetCore.WebApi.Models;
@@ -23,6 +23,8 @@
* Logging
* -------------------------------------------------------------------------- */
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(builder.Configuration).CreateLogger();
+
+/* Serilog ------------------------------------------------------------------ */
builder.Host.UseSerilog();
/* -----------------------------------------------------------------------------
@@ -30,6 +32,7 @@
* -------------------------------------------------------------------------- */
builder.Services.AddControllers();
+/* Entity Framework Core ---------------------------------------------------- */
builder.Services.AddDbContextPool(options =>
{
var dataSource = Path.Combine(AppContext.BaseDirectory, "Data", "players-sqlite3.db");
@@ -44,20 +47,23 @@
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddMemoryCache();
+
+/* AutoMapper --------------------------------------------------------------- */
builder.Services.AddAutoMapper(typeof(PlayerMappingProfile));
+
+/* FluentValidation --------------------------------------------------------- */
builder.Services.AddScoped, PlayerRequestModelValidator>();
if (builder.Environment.IsDevelopment())
{
+ /* Swagger UI ----------------------------------------------------------- */
+ // https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", builder.Configuration.GetSection("SwaggerDoc").Get());
- options.IncludeXmlComments(
- Path.Combine(
- AppContext.BaseDirectory,
- $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"
- )
- );
+ options.IncludeXmlComments(SwaggerGenDefaults.ConfigureXmlCommentsFilePath());
+ options.AddSecurityDefinition("Bearer", SwaggerGenDefaults.ConfigureSecurityDefinition());
+ options.OperationFilter();
});
}
@@ -67,16 +73,14 @@
* Middlewares
* https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware
* -------------------------------------------------------------------------- */
+app.UseSerilogRequestLogging();
+
if (app.Environment.IsDevelopment())
{
- // https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle
app.UseSwagger();
app.UseSwaggerUI();
}
-// https://github.com/serilog/serilog-aspnetcore
-app.UseSerilogRequestLogging();
-
// https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl
app.UseHttpsRedirection();
diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Dotnet.Samples.AspNetCore.WebApi.Tests.csproj b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Dotnet.Samples.AspNetCore.WebApi.Tests.csproj
index e5ba8cc..c3d41c2 100644
--- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Dotnet.Samples.AspNetCore.WebApi.Tests.csproj
+++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/Dotnet.Samples.AspNetCore.WebApi.Tests.csproj
@@ -9,16 +9,16 @@
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
runtime; build; native; contentfiles; analyzers; buildtransitive
all
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
diff --git a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json
index 0c5e6d6..349f283 100644
--- a/test/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json
+++ b/test/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json
@@ -1511,35 +1511,35 @@
},
"Swashbuckle.AspNetCore": {
"type": "Transitive",
- "resolved": "8.1.0",
- "contentHash": "8NJwXskWNBhX4IntitP1VP68hzhQ9Sex4mR9ZbqCuuUsaWlfrMA+yDIYDFsbHlB2NQ6Gt27AT15BWo9jxHj1tg==",
+ "resolved": "8.1.1",
+ "contentHash": "HJHexmU0PiYevgTLvKjYkxEtclF2w4O7iTd3Ef3p6KeT0kcYLpkFVgCw6glpGS57h8769anv8G+NFi9Kge+/yw==",
"dependencies": {
"Microsoft.Extensions.ApiDescription.Server": "6.0.5",
- "Swashbuckle.AspNetCore.Swagger": "8.1.0",
- "Swashbuckle.AspNetCore.SwaggerGen": "8.1.0",
- "Swashbuckle.AspNetCore.SwaggerUI": "8.1.0"
+ "Swashbuckle.AspNetCore.Swagger": "8.1.1",
+ "Swashbuckle.AspNetCore.SwaggerGen": "8.1.1",
+ "Swashbuckle.AspNetCore.SwaggerUI": "8.1.1"
}
},
"Swashbuckle.AspNetCore.Swagger": {
"type": "Transitive",
- "resolved": "8.1.0",
- "contentHash": "TRpBDdWh/6IzXHOLKVEuZSuhcdc8OIwhr8dyLnWvEh6FO4f0KiATAzr8yzHspcCFzCc/CONpvUCq4DzazpP6Og==",
+ "resolved": "8.1.1",
+ "contentHash": "h+8D5jQtnl6X4f2hJQwf0Khj0SnCQANzirCELjXJ6quJ4C1aNNCvJrAsQ+4fOKAMqJkvW48cKj79ftG+YoGcRg==",
"dependencies": {
"Microsoft.OpenApi": "1.6.23"
}
},
"Swashbuckle.AspNetCore.SwaggerGen": {
"type": "Transitive",
- "resolved": "8.1.0",
- "contentHash": "sXAUNodmnzrJb1qmgdLY6HgTDsgagXCn6p+qygW2wP0BjcvdZZA6GdDgtuc8iQON9ekrp7m5G0Zc09lvuNhc/A==",
+ "resolved": "8.1.1",
+ "contentHash": "2EuPzXSNleOOzYvziERWRLnk1Oz9i0Z1PimaUFy1SasBqeV/rG+eMfwFAMtTaf4W6gvVOzRcUCNRHvpBIIzr+A==",
"dependencies": {
- "Swashbuckle.AspNetCore.Swagger": "8.1.0"
+ "Swashbuckle.AspNetCore.Swagger": "8.1.1"
}
},
"Swashbuckle.AspNetCore.SwaggerUI": {
"type": "Transitive",
- "resolved": "8.1.0",
- "contentHash": "PYyTbYrEz1q8N8ZpCFzY5hOkvP5gv9oIml06wrrrjGRFvVXyCF/b58FRI/9mnRKRBAmWKZR5pqhmJBzvsVveSQ=="
+ "resolved": "8.1.1",
+ "contentHash": "GDLX/MpK4oa2nYC1N/zN2UidQTtVKLPF6gkdEmGb0RITEwpJG9Gu8olKqPYnKqVeFn44JZoCS0M2LGRKXP8B/A=="
},
"System.ClientModel": {
"type": "Transitive",
@@ -1837,7 +1837,7 @@
"Serilog.Settings.Configuration": "[9.0.0, )",
"Serilog.Sinks.Console": "[6.0.0, )",
"Serilog.Sinks.File": "[6.0.0, )",
- "Swashbuckle.AspNetCore": "[8.1.0, )"
+ "Swashbuckle.AspNetCore": "[8.1.1, )"
}
}
}