diff --git a/.codacy.yml b/.codacy.yml
index 145b401..58d0874 100644
--- a/.codacy.yml
+++ b/.codacy.yml
@@ -3,34 +3,34 @@
# Uses Java glob syntax: https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob
exclude_paths:
-
# Ignore all root-level metadata and documentation
- - '.gitignore'
- - '.runsettings'
- - 'LICENSE'
- - 'README.md'
+ - ".gitignore"
+ - ".runsettings"
+ - "LICENSE"
+ - "README.md"
# Ignore all file types that shouldn't be analyzed
- - '**.yml'
- - '**.json'
- - '**.png'
- - '**.sln'
- - '**.csproj'
+ - "**.yml"
+ - "**.json"
+ - "**.png"
+ - "**.sln"
+ - "**.csproj"
# Ignore generated or infrastructure files
- - '**/*Program.cs'
+ - "**/*Program.cs"
# Ignore specific folders across any depth in the project
- - '**/Configurations/**'
- - '**/Data/**'
- - '**/Enums/**'
- - '**/Extensions/**'
- - '**/Mappings/**'
- - '**/Migrations/**'
- - '**/Models/**'
- - '**/Properties/**'
- - '**/Repositories/**'
- - '**/Utilities/**'
- - '**/Validators/**'
- - 'test/**/*'
- - 'scripts/**/*'
+ - "**/Configurations/**"
+ - "**/Data/**"
+ - "**/Enums/**"
+ - "**/Extensions/**"
+ - "**/Mappings/**"
+ - "**/Middlewares/**"
+ - "**/Migrations/**"
+ - "**/Models/**"
+ - "**/Properties/**"
+ - "**/Repositories/**"
+ - "**/Utilities/**"
+ - "**/Validators/**"
+ - "test/**/*"
+ - "scripts/**/*"
diff --git a/codecov.yml b/codecov.yml
index b6a281d..2379c0c 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -1,9 +1,8 @@
-
# Codecov Repository YAML
# https://docs.codecov.com/docs/codecov-yaml
coverage:
-# https://docs.codecov.com/docs/commit-status
+ # https://docs.codecov.com/docs/commit-status
status:
project:
default:
@@ -29,11 +28,11 @@ component_management:
- component_id: controllers
name: Controllers
paths:
- - 'src/Dotnet.Samples.AspNetCore.WebApi/Controllers/'
+ - "src/Dotnet.Samples.AspNetCore.WebApi/Controllers/"
- component_id: services
name: Services
paths:
- - 'src/Dotnet.Samples.AspNetCore.WebApi/Services/'
+ - "src/Dotnet.Samples.AspNetCore.WebApi/Services/"
comment:
layout: "header, diff, flags, components"
@@ -47,19 +46,20 @@ ignore:
- .*\.json
- .*\.yml
- .*\.png
- - '**/*.md'
+ - "**/*.md"
- .*\/test\/.*
- .*\/scripts\/.*
- .*\/Program\.cs
- - '**/LICENSE'
- - '**/README.md'
+ - "**/LICENSE"
+ - "**/README.md"
- .*\/Configurations\/.*
- .*\/Data\/.*
- .*\/Enums\/.*
- .*\/Extensions\/.*
- .*\/Mappings\/.*
+ - .*\/Middlewares\/.*
- .*\/Migrations\/.*
- .*\/Models\/.*
- .*\/Properties\/.*
diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs
index d32f923..42f25ee 100644
--- a/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs
+++ b/src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs
@@ -189,6 +189,7 @@ [FromBody] PlayerRequestModel player
return TypedResults.NotFound();
}
await playerService.UpdateAsync(player);
+ // codeql[cs/log-forging] Serilog structured logging with @ destructuring automatically escapes control characters
logger.LogInformation("PUT /players/{SquadNumber} updated: {@Player}", squadNumber, player);
return TypedResults.NoContent();
}
diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Extensions/MiddlewareExtensions.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Extensions/MiddlewareExtensions.cs
new file mode 100644
index 0000000..e303545
--- /dev/null
+++ b/src/Dotnet.Samples.AspNetCore.WebApi/Extensions/MiddlewareExtensions.cs
@@ -0,0 +1,21 @@
+using Dotnet.Samples.AspNetCore.WebApi.Middlewares;
+
+namespace Dotnet.Samples.AspNetCore.WebApi.Extensions;
+
+///
+/// Extension methods for configuring middleware in the application pipeline.
+///
+public static class MiddlewareExtensions
+{
+ ///
+ /// Adds global exception handling middleware to the application pipeline.
+ /// This middleware catches unhandled exceptions and returns RFC 7807 compliant error responses.
+ ///
+ /// The web application used to configure the HTTP pipeline, and routes.
+ /// The WebApplication object for method chaining.
+ public static WebApplication UseExceptionHandling(this WebApplication app)
+ {
+ app.UseMiddleware();
+ return app;
+ }
+}
diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Middlewares/ExceptionMiddleware.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Middlewares/ExceptionMiddleware.cs
new file mode 100644
index 0000000..9ded910
--- /dev/null
+++ b/src/Dotnet.Samples.AspNetCore.WebApi/Middlewares/ExceptionMiddleware.cs
@@ -0,0 +1,117 @@
+using System.Text.Json;
+using FluentValidation;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace Dotnet.Samples.AspNetCore.WebApi.Middlewares;
+
+///
+/// Middleware for global exception handling with RFC 7807 Problem Details format.
+///
+public class ExceptionMiddleware(ILogger logger, IHostEnvironment environment)
+{
+ private const string ProblemDetailsContentType = "application/problem+json";
+
+ private static readonly JsonSerializerOptions JsonOptions =
+ new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
+
+ ///
+ /// Invokes the middleware to handle exceptions globally.
+ ///
+ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
+ {
+ try
+ {
+ await next(context);
+ }
+ catch (Exception exception)
+ {
+ await HandleExceptionAsync(context, exception);
+ }
+ }
+
+ ///
+ /// Handles the exception and returns an RFC 7807 compliant error response.
+ ///
+ private async Task HandleExceptionAsync(HttpContext context, Exception exception)
+ {
+ var (status, title) = MapExceptionToStatusCode(exception);
+
+ var problemDetails = new ProblemDetails
+ {
+ Type = $"https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{status}",
+ Title = title,
+ Status = status,
+ Detail = GetExceptionDetail(exception),
+ Instance = context.Request.Path
+ };
+
+ // Add trace ID for request correlation
+ problemDetails.Extensions["traceId"] = context.TraceIdentifier;
+
+ // codeql[cs/log-forging] Serilog structured logging automatically escapes control characters
+ logger.LogError(
+ exception,
+ "Unhandled exception occurred. TraceId: {TraceId}, Path: {Path}, StatusCode: {StatusCode}",
+ context.TraceIdentifier,
+ context.Request.Path,
+ status
+ );
+
+ // Only modify response if headers haven't been sent yet
+ if (!context.Response.HasStarted)
+ {
+ context.Response.StatusCode = status;
+ context.Response.ContentType = ProblemDetailsContentType;
+
+ await context.Response.WriteAsync(
+ JsonSerializer.Serialize(problemDetails, JsonOptions)
+ );
+ }
+ else
+ {
+ logger.LogWarning(
+ "Unable to write error response for TraceId: {TraceId}. Response has already started.",
+ context.TraceIdentifier
+ );
+ }
+ }
+
+ ///
+ /// Maps exception types to appropriate HTTP status codes and titles.
+ ///
+ private static (int StatusCode, string Title) MapExceptionToStatusCode(Exception exception)
+ {
+ return exception switch
+ {
+ ValidationException => (StatusCodes.Status400BadRequest, "Validation Error"),
+ ArgumentException
+ or ArgumentNullException
+ => (StatusCodes.Status400BadRequest, "Bad Request"),
+ InvalidOperationException => (StatusCodes.Status400BadRequest, "Invalid Operation"),
+ DbUpdateConcurrencyException => (StatusCodes.Status409Conflict, "Concurrency Conflict"),
+ OperationCanceledException => (StatusCodes.Status408RequestTimeout, "Request Timeout"),
+ _ => (StatusCodes.Status500InternalServerError, "Internal Server Error")
+ };
+ }
+
+ ///
+ /// Gets the exception detail based on the environment.
+ /// In Development: includes full exception details and stack trace.
+ /// In Production: returns a generic message without sensitive information.
+ ///
+ private string GetExceptionDetail(Exception exception)
+ {
+ if (environment.IsDevelopment())
+ {
+ return $"{exception.Message}\n\nStack Trace:\n{exception.StackTrace}";
+ }
+
+ return exception switch
+ {
+ ValidationException => exception.Message,
+ ArgumentException => exception.Message,
+ _ => "An unexpected error occurred while processing your request."
+ };
+ }
+}
diff --git a/src/Dotnet.Samples.AspNetCore.WebApi/Program.cs b/src/Dotnet.Samples.AspNetCore.WebApi/Program.cs
index 4b43640..8e247cc 100644
--- a/src/Dotnet.Samples.AspNetCore.WebApi/Program.cs
+++ b/src/Dotnet.Samples.AspNetCore.WebApi/Program.cs
@@ -55,6 +55,7 @@
* -------------------------------------------------------------------------- */
app.UseSerilogRequestLogging();
+app.UseExceptionHandling();
app.UseHttpsRedirection();
app.MapHealthChecks("/health");
app.UseRateLimiter();