Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
1 change: 1 addition & 0 deletions .codacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 10 additions & 7 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand All @@ -30,7 +30,7 @@ dotnet watch run --project src/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.A
https://localhost:9000/swagger/index.html
```

![API Documentation](/docs/Swagger.png)
![API Documentation](/assets/images/Swagger.png)

## Credits

Expand Down
Binary file added assets/images/Swagger.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ ignore:
- '**/LICENSE'
- '**/README.md'

- .*\/Configurations\/.*
- .*\/Data\/.*
- .*\/Enums\/.*
- .*\/Mappings\/.*
Expand Down
Binary file removed docs/Swagger.png
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Adds the Bearer security requirement only to operations that have [Authorize] attributes.
/// </summary>
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<AuthorizeAttribute>().Any()
|| context
.MethodInfo.DeclaringType?.GetCustomAttributes(true)
.OfType<AuthorizeAttribute>()
.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<OpenApiSecurityRequirement>();
operation.Security.Add(
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Id = "Bearer",
Type = ReferenceType.SecurityScheme
}
},
Array.Empty<string>()
}
}
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Reflection;
using Microsoft.OpenApi.Models;

namespace Dotnet.Samples.AspNetCore.WebApi.Configurations;

/// <summary>
/// Provides centralized configuration methods for Swagger/OpenAPI docs.
/// Includes XML comments path resolution and security (JWT Bearer) setup.
/// </summary>
public static class SwaggerGenDefaults
{
/// <summary>
/// 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.
/// </summary>
/// <returns>Full file path to the XML documentation file.</returns>
public static string ConfigureXmlCommentsFilePath()
{
return Path.Combine(
AppContext.BaseDirectory,
$"{Assembly.GetExecutingAssembly().GetName().Name}.xml"
);
}

/// <summary>
/// Configures the OpenAPI security definition for JWT Bearer authentication.
/// This will show the padlock icon in Swagger UI and allow users to
/// authenticate.
/// </summary>
/// <returns>An <see cref="OpenApiSecurityScheme"/> describing the Bearer token format.</returns>
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}"
};
}

/// <summary>
/// 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.
/// </summary>
/// <returns>An <see cref="OpenApiSecurityRequirement"/> referencing the Bearer definition.</returns>
public static OpenApiSecurityRequirement ConfigureSecurityRequirement()
{
return new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Id = "Bearer",
Type = ReferenceType.SecurityScheme
}
},
Array.Empty<string>()
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
namespace Dotnet.Samples.AspNetCore.WebApi.Controllers;

[ApiController]
[Route("[controller]")]
[Route("players")]
[Produces("application/json")]
public class PlayerController(
IPlayerService playerService,
Expand All @@ -32,19 +32,19 @@
[ProducesResponseType<PlayerResponseModel>(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<IResult> PostAsync([FromBody] PlayerRequestModel player)
{
var validation = await validator.ValidateAsync(player);

if (!validation.IsValid)
{
var errors = validation
.Errors.Select(error => new { error.PropertyName, error.ErrorMessage })
.ToArray();

logger.LogWarning("POST /players validation failed: {@Errors}", errors);
return TypedResults.BadRequest(errors);
}

Check warning on line 47 in src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 12 lines is too similar to src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs:169

if (await playerService.RetrieveBySquadNumberAsync(player.SquadNumber) != null)
{
Expand Down Expand Up @@ -94,13 +94,12 @@
}

/// <summary>
/// Retrieves a Player by its ID
/// Retrieves a Player by its internal Id (GUID)
/// </summary>
/// <param name="id">The ID of the Player</param>
/// <param name="id">The internal Id (GUID) of the Player</param>
/// <response code="200">OK</response>
/// <response code="404">Not Found</response>
[Authorize(Roles = "Admin")]
[ApiExplorerSettings(IgnoreApi = true)]
[Authorize]
[HttpGet("{id:Guid}", Name = "RetrieveById")]
[ProducesResponseType<PlayerResponseModel>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
Expand All @@ -125,7 +124,7 @@
/// <param name="squadNumber">The Squad Number of the Player</param>
/// <response code="200">OK</response>
/// <response code="404">Not Found</response>
[HttpGet("squadNumber/{squadNumber:int}", Name = "RetrieveBySquadNumber")]
[HttpGet("{squadNumber:int}", Name = "RetrieveBySquadNumber")]
[ProducesResponseType<PlayerResponseModel>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IResult> GetBySquadNumberAsync([FromRoute] int squadNumber)
Expand Down Expand Up @@ -167,19 +166,19 @@
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IResult> PutAsync(
[FromRoute] int squadNumber,
[FromBody] PlayerRequestModel player
)
{
var validation = await validator.ValidateAsync(player);
if (!validation.IsValid)
{
var errors = validation
.Errors.Select(error => new { error.PropertyName, error.ErrorMessage })
.ToArray();

logger.LogWarning(
"PUT /players/{squadNumber} validation failed: {@Errors}",
squadNumber,

Check warning on line 181 in src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 12 lines is too similar to src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs:35
errors
);
return TypedResults.BadRequest(errors);
Expand Down
26 changes: 15 additions & 11 deletions src/Dotnet.Samples.AspNetCore.WebApi/Program.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -23,13 +23,16 @@
* Logging
* -------------------------------------------------------------------------- */
Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(builder.Configuration).CreateLogger();

/* Serilog ------------------------------------------------------------------ */
builder.Host.UseSerilog();

/* -----------------------------------------------------------------------------
* Services
* -------------------------------------------------------------------------- */
builder.Services.AddControllers();

/* Entity Framework Core ---------------------------------------------------- */
builder.Services.AddDbContextPool<PlayerDbContext>(options =>
{
var dataSource = Path.Combine(AppContext.BaseDirectory, "Data", "players-sqlite3.db");
Expand All @@ -44,20 +47,23 @@
builder.Services.AddScoped<IPlayerRepository, PlayerRepository>();
builder.Services.AddScoped<IPlayerService, PlayerService>();
builder.Services.AddMemoryCache();

/* AutoMapper --------------------------------------------------------------- */
builder.Services.AddAutoMapper(typeof(PlayerMappingProfile));

/* FluentValidation --------------------------------------------------------- */
builder.Services.AddScoped<IValidator<PlayerRequestModel>, 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<OpenApiInfo>());
options.IncludeXmlComments(
Path.Combine(
AppContext.BaseDirectory,
$"{Assembly.GetExecutingAssembly().GetName().Name}.xml"
)
);
options.IncludeXmlComments(SwaggerGenDefaults.ConfigureXmlCommentsFilePath());
options.AddSecurityDefinition("Bearer", SwaggerGenDefaults.ConfigureSecurityDefinition());
options.OperationFilter<AuthorizeCheckOperationFilter>();
});
}

Expand All @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="8.2.0" />
<PackageReference Include="Moq" Version="4.20.72" />
</ItemGroup>
Expand Down
26 changes: 13 additions & 13 deletions test/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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, )"
}
}
}
Expand Down
Loading