From dab4cdc8a5f9f435835c4a978209ec4c17ab28ae Mon Sep 17 00:00:00 2001 From: Danny Van den Wouwer Date: Fri, 12 Jun 2026 15:30:37 +0200 Subject: [PATCH 01/11] PPWCODE-15 Update SDK version to 10.0.300 in global.json --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 3c540fe..cf25bed 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "disable", - "version": "10.0.102" + "version": "10.0.300" } } From 397dc0a14b94c18c2b36fcdf324a7c48660d1b1e Mon Sep 17 00:00:00 2001 From: Danny Van den Wouwer Date: Fri, 12 Jun 2026 15:31:14 +0200 Subject: [PATCH 02/11] PPWCODE-15 Fix typo in `SemanticExceptionHandler` class name --- ...ionHandler.cs => SemanticExceptionExceptionHandler.cs} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename src/I/Exceptions/{SemanticExceptionHandler.cs => SemanticExceptionExceptionHandler.cs} (81%) diff --git a/src/I/Exceptions/SemanticExceptionHandler.cs b/src/I/Exceptions/SemanticExceptionExceptionHandler.cs similarity index 81% rename from src/I/Exceptions/SemanticExceptionHandler.cs rename to src/I/Exceptions/SemanticExceptionExceptionHandler.cs index 139063e..09b0b7e 100644 --- a/src/I/Exceptions/SemanticExceptionHandler.cs +++ b/src/I/Exceptions/SemanticExceptionExceptionHandler.cs @@ -21,10 +21,10 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; [ExcludeFromCodeCoverage] -public sealed class SemanticExceptionHandler - : BaseExceptionHandler +public sealed class SemanticExceptionExceptionHandler + : BaseExceptionHandler { - public SemanticExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) + public SemanticExceptionExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) : base(problemDetailsFactory, environment) { } @@ -32,4 +32,4 @@ public SemanticExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHo /// protected override int? GetStatusCode(ExceptionContext context, SemanticException? exception) => StatusCodes.Status400BadRequest; -} +} \ No newline at end of file From 9f4a033c7cc91dd6b195251189afe8a7dd07cff1 Mon Sep 17 00:00:00 2001 From: Danny Van den Wouwer Date: Fri, 12 Jun 2026 15:38:25 +0200 Subject: [PATCH 03/11] PPWCODE-15 Fix typo in `OperationCanceledExceptionHandler` class name --- ...tionHandler.cs => OperationCancelledExceptionHandler.cs} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename src/I/Exceptions/{OperationCanceledExceptionHandler.cs => OperationCancelledExceptionHandler.cs} (64%) diff --git a/src/I/Exceptions/OperationCanceledExceptionHandler.cs b/src/I/Exceptions/OperationCancelledExceptionHandler.cs similarity index 64% rename from src/I/Exceptions/OperationCanceledExceptionHandler.cs rename to src/I/Exceptions/OperationCancelledExceptionHandler.cs index 150aaa7..3a508d7 100644 --- a/src/I/Exceptions/OperationCanceledExceptionHandler.cs +++ b/src/I/Exceptions/OperationCancelledExceptionHandler.cs @@ -7,10 +7,10 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; [ExcludeFromCodeCoverage] -public sealed class OperationCanceledExceptionHandler - : BaseExceptionHandler +public sealed class OperationCancelledExceptionHandler + : BaseExceptionHandler { - public OperationCanceledExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) + public OperationCancelledExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) : base(problemDetailsFactory, environment) { } From a2c897c532bf5669b970be23b63390fb787b9e6b Mon Sep 17 00:00:00 2001 From: Danny Van den Wouwer Date: Fri, 12 Jun 2026 15:31:22 +0200 Subject: [PATCH 04/11] PPWCODE-15 Add `ContractViolationExceptionHandler` implementation --- .../ContractViolationExceptionHandler.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/I/Exceptions/ContractViolationExceptionHandler.cs diff --git a/src/I/Exceptions/ContractViolationExceptionHandler.cs b/src/I/Exceptions/ContractViolationExceptionHandler.cs new file mode 100644 index 0000000..399f6d4 --- /dev/null +++ b/src/I/Exceptions/ContractViolationExceptionHandler.cs @@ -0,0 +1,35 @@ +// Copyright 2026 by PeopleWare n.v.. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Hosting; + +using PPWCode.Vernacular.Contracts.I; + +namespace PPWCode.AspNetCore.Server.I.Exceptions; + +[ExcludeFromCodeCoverage] +public class ContractViolationExceptionHandler + : BaseExceptionHandler +{ + public ContractViolationExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) + : base(problemDetailsFactory, environment) + { + } + + /// + protected override int? GetStatusCode(ExceptionContext context, ContractViolation? contextException) + => StatusCodes.Status500InternalServerError; +} From 0a0e30783e9a5a2a834581d5e4313bf51b472b15 Mon Sep 17 00:00:00 2001 From: Danny Van den Wouwer Date: Fri, 12 Jun 2026 15:31:30 +0200 Subject: [PATCH 05/11] PPWCODE-15 Add `ApiUsageErrorExceptionHandler` implementation --- .../ApiUsageErrorExceptionHandler.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/I/Exceptions/ApiUsageErrorExceptionHandler.cs diff --git a/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs b/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs new file mode 100644 index 0000000..28a7acf --- /dev/null +++ b/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs @@ -0,0 +1,31 @@ +// Copyright 2026 by PeopleWare n.v.. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Hosting; + +using PPWCode.Vernacular.Exceptions.V; + +namespace PPWCode.AspNetCore.Server.I.Exceptions; + +public class ApiUsageErrorExceptionHandler : BaseExceptionHandler +{ + public ApiUsageErrorExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) + : base(problemDetailsFactory, environment) + { + } + + /// + protected override int? GetStatusCode(ExceptionContext context, ApiUsageError? contextException) + => StatusCodes.Status400BadRequest; +} From 23679dfdaeffd99b4c36ffbb0a19c2de569f6537 Mon Sep 17 00:00:00 2001 From: Danny Van den Wouwer Date: Fri, 12 Jun 2026 15:39:02 +0200 Subject: [PATCH 06/11] PPWCODE-15 Add changelog for release 2.0.0 detailing fixes and new exception handlers --- docs/modules/ROOT/pages/changelog.adoc | 5 +++++ docs/modules/ROOT/partials/changelog-2.0.0.adoc | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 docs/modules/ROOT/partials/changelog-2.0.0.adoc diff --git a/docs/modules/ROOT/pages/changelog.adoc b/docs/modules/ROOT/pages/changelog.adoc index 36cf14a..9bfe306 100644 --- a/docs/modules/ROOT/pages/changelog.adoc +++ b/docs/modules/ROOT/pages/changelog.adoc @@ -26,3 +26,8 @@ include::partial$changelog-{release}.adoc[] == `{release}` include::partial$changelog-release-section.adoc[] include::partial$changelog-{release}.adoc[] + +:release: 2.0.0 +== `{release}` +include::partial$changelog-release-section.adoc[] +include::partial$changelog-{release}.adoc[] diff --git a/docs/modules/ROOT/partials/changelog-2.0.0.adoc b/docs/modules/ROOT/partials/changelog-2.0.0.adoc new file mode 100644 index 0000000..164d81a --- /dev/null +++ b/docs/modules/ROOT/partials/changelog-2.0.0.adoc @@ -0,0 +1,16 @@ +//// +The changelog for a specific release. +Note that it is best to update the changelog as new code is developed. + +All attributes used, must be included in this file to keep it self-contained. + +This file is translated into markdown and used for the release notes in the GitHub release! +//// + +=== Fixes +* Renamed `OperationCanceledExceptionHandler` to `OperationCancelledExceptionHandler` because of typo (breaking change) +* Renamed `SemanticExceptionHandler` to `SemanticExceptionExceptionHandler` (breaking change) + +=== New +* `ApiUsageErrorExceptionHandler`: initial version of exception handler for `ApiUsageError` +* `ContractViolationExceptionHandler`: initial version of exception handler for `ContractViolation` From 73457d9e93a94650e374505e9ac2c5869100a292 Mon Sep 17 00:00:00 2001 From: Danny Van den Wouwer Date: Tue, 16 Jun 2026 10:46:06 +0200 Subject: [PATCH 07/11] PPWCODE-15 Add extended summaries, remarks, and copyright headers across exception handlers and related classes --- .../ROOT/partials/changelog-2.0.0.adoc | 1 + .../ApiUsageErrorExceptionHandler.cs | 4 +- src/I/Exceptions/BaseExceptionHandler.cs | 103 +++++++++++++++++- .../ContractViolationExceptionHandler.cs | 4 +- .../ExternalErrorExceptionHandler.cs | 23 +++- src/I/Exceptions/IExceptionHandler.cs | 20 +++- src/I/Exceptions/NotFoundExceptionHandler.cs | 13 +++ .../NotImplementedExceptionHandler.cs | 23 +++- .../OperationCancelledExceptionHandler.cs | 15 ++- src/I/Exceptions/PpwProblemDetailsFactory.cs | 46 +++++++- .../ProgrammingErrorExceptionHandler.cs | 23 +++- .../SemanticExceptionExceptionHandler.cs | 4 +- src/I/ILinksManager.cs | 22 +++- src/I/LinksContext.cs | 30 ++++- src/I/LinksManager.cs | 15 +++ src/I/Transactional/TransactionalAttribute.cs | 23 +++- 16 files changed, 335 insertions(+), 34 deletions(-) diff --git a/docs/modules/ROOT/partials/changelog-2.0.0.adoc b/docs/modules/ROOT/partials/changelog-2.0.0.adoc index 164d81a..509f32c 100644 --- a/docs/modules/ROOT/partials/changelog-2.0.0.adoc +++ b/docs/modules/ROOT/partials/changelog-2.0.0.adoc @@ -10,6 +10,7 @@ This file is translated into markdown and used for the release notes in the GitH === Fixes * Renamed `OperationCanceledExceptionHandler` to `OperationCancelledExceptionHandler` because of typo (breaking change) * Renamed `SemanticExceptionHandler` to `SemanticExceptionExceptionHandler` (breaking change) +* Improve documentation === New * `ApiUsageErrorExceptionHandler`: initial version of exception handler for `ApiUsageError` diff --git a/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs b/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs index 28a7acf..c5781ae 100644 --- a/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs +++ b/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs @@ -18,8 +18,10 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; -public class ApiUsageErrorExceptionHandler : BaseExceptionHandler +/// +public sealed class ApiUsageErrorExceptionHandler : BaseExceptionHandler { + /// public ApiUsageErrorExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) : base(problemDetailsFactory, environment) { diff --git a/src/I/Exceptions/BaseExceptionHandler.cs b/src/I/Exceptions/BaseExceptionHandler.cs index 536b63b..981ffa5 100644 --- a/src/I/Exceptions/BaseExceptionHandler.cs +++ b/src/I/Exceptions/BaseExceptionHandler.cs @@ -1,4 +1,15 @@ -using Microsoft.AspNetCore.Http; +// Copyright 2026 by PeopleWare n.v.. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -8,13 +19,50 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; +/// +/// Base class for implementations that translate a specific +/// into an HTTP response, typically a payload. +/// +/// +/// +/// The handling pipeline in works as follows: +/// +/// decides whether this handler applies to the current exception. +/// +/// When is , the exception is logged through +/// . +/// +/// +/// When returns a status code, a is produced +/// (optionally enriched through ) and used as the response. +/// +/// +/// Otherwise is given the chance to produce a custom +/// . +/// +/// +/// +/// +/// Derived classes customize the behaviour by overriding the relevant members; most +/// handlers only need to override . +/// +/// +/// +/// The concrete handler type, used as the category for the . +/// +/// The exception type this handler is responsible for. public abstract class BaseExceptionHandler : IExceptionHandler where THandler : IExceptionHandler where TException : Exception { - private readonly ProblemDetailsFactory _problemDetailsFactory; private readonly IHostEnvironment _environment; + private readonly ProblemDetailsFactory _problemDetailsFactory; + /// + /// Initializes a new instance of the class. + /// + /// The factory used to create the response. + /// The host environment, used to decide whether development-only details are exposed. protected BaseExceptionHandler( ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) @@ -23,9 +71,20 @@ protected BaseExceptionHandler( _environment = environment; } + /// + /// Indicates whether a handled exception should be logged through . + /// Defaults to . + /// protected virtual bool LogException => false; + /// + /// Indicates whether the current is considered a development + /// environment, in which case additional exception details are added to the . + /// + protected bool IsDevelopment + => PpwProblemDetailsFactory.EnvironmentsConsideredAsDevelopment.Contains(_environment.EnvironmentName); + /// public bool Handle(ExceptionContext context) { @@ -70,21 +129,55 @@ private ILogger CreateLogger(ExceptionContext context) return factory.CreateLogger(); } - protected bool IsDevelopment - => PpwProblemDetailsFactory.EnvironmentsConsideredAsDevelopment.Contains(_environment.EnvironmentName); - + /// + /// Determines whether this handler is able to handle the exception in . + /// By default this is the case when the exception is assignable to . + /// + /// The current exception context. + /// when this handler can handle the exception; otherwise . protected virtual bool CanHandle(ExceptionContext context) => context.Exception is TException; + /// + /// Logs the handled exception. Only invoked when is . + /// + /// The logger for . + /// The current exception context. protected virtual void LogContext(ILogger logger, ExceptionContext context) => logger.LogError(context.Exception, "Handled exception"); + /// + /// Produces a custom for the exception. Only invoked when + /// returned . Returns by default, + /// leaving the exception unhandled. + /// + /// The current exception context. + /// The result to use as the response, or when no result is produced. protected virtual IActionResult? CreateActionResult(ExceptionContext context) => null; + /// + /// Returns the HTTP status code used to build the response, or + /// to fall back to . Returns + /// by default. + /// + /// The current exception context. + /// + /// The exception cast to , or when the cast failed. + /// + /// The HTTP status code, or . protected virtual int? GetStatusCode(ExceptionContext context, TException? contextException) => null; + /// + /// Allows derived classes to add extra information to the before it is returned. + /// Does nothing by default. + /// + /// The current exception context. + /// + /// The exception cast to , or when the cast failed. + /// + /// The problem details to enrich. protected virtual void EnrichProblemDetails( ExceptionContext context, TException? contextException, diff --git a/src/I/Exceptions/ContractViolationExceptionHandler.cs b/src/I/Exceptions/ContractViolationExceptionHandler.cs index 399f6d4..05e88d9 100644 --- a/src/I/Exceptions/ContractViolationExceptionHandler.cs +++ b/src/I/Exceptions/ContractViolationExceptionHandler.cs @@ -20,10 +20,12 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; +/// [ExcludeFromCodeCoverage] -public class ContractViolationExceptionHandler +public sealed class ContractViolationExceptionHandler : BaseExceptionHandler { + /// public ContractViolationExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) : base(problemDetailsFactory, environment) { diff --git a/src/I/Exceptions/ExternalErrorExceptionHandler.cs b/src/I/Exceptions/ExternalErrorExceptionHandler.cs index bc1bb7b..60a866b 100644 --- a/src/I/Exceptions/ExternalErrorExceptionHandler.cs +++ b/src/I/Exceptions/ExternalErrorExceptionHandler.cs @@ -1,4 +1,15 @@ -using System.Diagnostics.CodeAnalysis; +// Copyright 2026 by PeopleWare n.v.. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; @@ -9,20 +20,22 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; +/// [ExcludeFromCodeCoverage] public sealed class ExternalErrorExceptionHandler : BaseExceptionHandler { + /// public ExternalErrorExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) : base(problemDetailsFactory, environment) { } - /// - protected override int? GetStatusCode(ExceptionContext context, ExternalError? exception) - => StatusCodes.Status500InternalServerError; - /// protected override bool LogException => true; + + /// + protected override int? GetStatusCode(ExceptionContext context, ExternalError? exception) + => StatusCodes.Status500InternalServerError; } diff --git a/src/I/Exceptions/IExceptionHandler.cs b/src/I/Exceptions/IExceptionHandler.cs index 0651cca..465a2aa 100644 --- a/src/I/Exceptions/IExceptionHandler.cs +++ b/src/I/Exceptions/IExceptionHandler.cs @@ -1,4 +1,4 @@ -// Copyright 2025 by PeopleWare n.v.. +// Copyright 2026 by PeopleWare n.v.. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -13,7 +13,25 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; +/// +/// Handles an exception captured during request processing and, when applicable, translates it into an +/// HTTP response. +/// +/// +/// Implementations are typically chained: each handler is offered the in turn +/// and signals through the return value of whether it took responsibility for the +/// exception. +/// public interface IExceptionHandler { + /// + /// Attempts to handle the exception in . When the exception is handled, the + /// implementation sets to the response to return. + /// + /// The current exception context. + /// + /// when this handler handled the exception; otherwise , + /// leaving the exception for another handler to process. + /// bool Handle(ExceptionContext context); } diff --git a/src/I/Exceptions/NotFoundExceptionHandler.cs b/src/I/Exceptions/NotFoundExceptionHandler.cs index 7637d4e..9a50430 100644 --- a/src/I/Exceptions/NotFoundExceptionHandler.cs +++ b/src/I/Exceptions/NotFoundExceptionHandler.cs @@ -1,3 +1,14 @@ +// Copyright 2026 by PeopleWare n.v.. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; @@ -10,10 +21,12 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; +/// [ExcludeFromCodeCoverage] public sealed class NotFoundExceptionHandler : BaseExceptionHandler { + /// public NotFoundExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) : base(problemDetailsFactory, environment) { diff --git a/src/I/Exceptions/NotImplementedExceptionHandler.cs b/src/I/Exceptions/NotImplementedExceptionHandler.cs index 0eea746..7c032ec 100644 --- a/src/I/Exceptions/NotImplementedExceptionHandler.cs +++ b/src/I/Exceptions/NotImplementedExceptionHandler.cs @@ -1,4 +1,15 @@ -using System.Diagnostics.CodeAnalysis; +// Copyright 2026 by PeopleWare n.v.. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; @@ -7,20 +18,22 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; +/// [ExcludeFromCodeCoverage] public sealed class NotImplementedExceptionHandler : BaseExceptionHandler { + /// public NotImplementedExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) : base(problemDetailsFactory, environment) { } - /// - protected override int? GetStatusCode(ExceptionContext context, NotImplementedException? exception) - => StatusCodes.Status501NotImplemented; - /// protected override bool LogException => true; + + /// + protected override int? GetStatusCode(ExceptionContext context, NotImplementedException? exception) + => StatusCodes.Status501NotImplemented; } diff --git a/src/I/Exceptions/OperationCancelledExceptionHandler.cs b/src/I/Exceptions/OperationCancelledExceptionHandler.cs index 3a508d7..81960b9 100644 --- a/src/I/Exceptions/OperationCancelledExceptionHandler.cs +++ b/src/I/Exceptions/OperationCancelledExceptionHandler.cs @@ -1,4 +1,15 @@ -using System.Diagnostics.CodeAnalysis; +// Copyright 2026 by PeopleWare n.v.. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -6,10 +17,12 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; +/// [ExcludeFromCodeCoverage] public sealed class OperationCancelledExceptionHandler : BaseExceptionHandler { + /// public OperationCancelledExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) : base(problemDetailsFactory, environment) { diff --git a/src/I/Exceptions/PpwProblemDetailsFactory.cs b/src/I/Exceptions/PpwProblemDetailsFactory.cs index 405064c..2bd7f70 100644 --- a/src/I/Exceptions/PpwProblemDetailsFactory.cs +++ b/src/I/Exceptions/PpwProblemDetailsFactory.cs @@ -1,4 +1,15 @@ -using System.Diagnostics; +// Copyright 2026 by PeopleWare n.v.. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; @@ -10,8 +21,26 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; +/// +/// A that produces and +/// responses with PPWCode-specific defaults. +/// +/// +/// +/// The factory falls back to sensible defaults for the status code, title, type and instance, and applies the +/// configured for the application. +/// +/// +/// When the application runs in a development environment (see ), +/// diagnostic information such as the current activity id and the request trace identifier is added to the +/// . +/// +/// public class PpwProblemDetailsFactory : ProblemDetailsFactory { + /// + /// The set of environment names that are treated as development environments. Matching is case-insensitive. + /// public static readonly ISet EnvironmentsConsideredAsDevelopment = new HashSet(StringComparer.OrdinalIgnoreCase) { @@ -23,6 +52,11 @@ public class PpwProblemDetailsFactory : ProblemDetailsFactory private readonly IHostEnvironment _environment; private readonly ApiBehaviorOptions _options; + /// + /// Initializes a new instance of the class. + /// + /// The configured API behaviour options, used for the client error mapping. + /// The host environment, used to decide whether diagnostic details are exposed. public PpwProblemDetailsFactory( IOptions options, IHostEnvironment environment) @@ -96,6 +130,16 @@ public override ValidationProblemDetails CreateValidationProblemDetails( return problemDetails; } + /// + /// Fills in the title and type of from the configured + /// , falling back to the supplied values, and adds + /// diagnostic information when running in a development environment. + /// + /// The current HTTP context. + /// The problem details to complete. + /// The HTTP status code used to look up the client error mapping. + /// The title to use when neither the problem details nor the mapping provide one. + /// The type to use when neither the problem details nor the mapping provide one. protected virtual void ApplyProblemDetailsDefaults( HttpContext httpContext, ProblemDetails problemDetails, diff --git a/src/I/Exceptions/ProgrammingErrorExceptionHandler.cs b/src/I/Exceptions/ProgrammingErrorExceptionHandler.cs index dc06ac1..3a4bbe0 100644 --- a/src/I/Exceptions/ProgrammingErrorExceptionHandler.cs +++ b/src/I/Exceptions/ProgrammingErrorExceptionHandler.cs @@ -1,4 +1,15 @@ -using System.Diagnostics.CodeAnalysis; +// Copyright 2026 by PeopleWare n.v.. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; @@ -11,22 +22,24 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; +/// [ExcludeFromCodeCoverage] public sealed class ProgrammingErrorExceptionHandler : BaseExceptionHandler { + /// public ProgrammingErrorExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) : base(problemDetailsFactory, environment) { } + /// + protected override bool LogException + => true; + /// protected override int? GetStatusCode(ExceptionContext context, ProgrammingError? exception) => exception is ApiUsageError // ApiUsageError => programming error on the frontend ? StatusCodes.Status400BadRequest : StatusCodes.Status500InternalServerError; - - /// - protected override bool LogException - => true; } diff --git a/src/I/Exceptions/SemanticExceptionExceptionHandler.cs b/src/I/Exceptions/SemanticExceptionExceptionHandler.cs index 09b0b7e..b0fd249 100644 --- a/src/I/Exceptions/SemanticExceptionExceptionHandler.cs +++ b/src/I/Exceptions/SemanticExceptionExceptionHandler.cs @@ -20,10 +20,12 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; +/// [ExcludeFromCodeCoverage] public sealed class SemanticExceptionExceptionHandler : BaseExceptionHandler { + /// public SemanticExceptionExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) : base(problemDetailsFactory, environment) { @@ -32,4 +34,4 @@ public SemanticExceptionExceptionHandler(ProblemDetailsFactory problemDetailsFac /// protected override int? GetStatusCode(ExceptionContext context, SemanticException? exception) => StatusCodes.Status400BadRequest; -} \ No newline at end of file +} diff --git a/src/I/ILinksManager.cs b/src/I/ILinksManager.cs index adbb936..ab49a9b 100644 --- a/src/I/ILinksManager.cs +++ b/src/I/ILinksManager.cs @@ -1,4 +1,4 @@ -// Copyright 2025 by PeopleWare n.v.. +// Copyright 2026 by PeopleWare n.v.. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -10,14 +10,30 @@ // limitations under the License. using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; namespace PPWCode.AspNetCore.Server.I; +/// +/// Generates URLs for named routes, abstracting the underlying . +/// public interface ILinksManager { - /// + /// + /// Generates a URL with a relative path for the specified route. + /// + /// The name of the route used to generate the URL. + /// An object that contains the route values. + /// The generated URL, or when a URL could not be generated. + /// string? RouteUrl(string routeName, object? routeValues); - /// + /// + /// Generates an absolute URL for the specified route. + /// + /// The name of the route used to generate the URL. + /// An object that contains the route values. + /// The generated absolute URL, or when a URL could not be generated. + /// string? Link(string routeName, object? routeValues); } diff --git a/src/I/LinksContext.cs b/src/I/LinksContext.cs index c6e2312..79da554 100644 --- a/src/I/LinksContext.cs +++ b/src/I/LinksContext.cs @@ -1,4 +1,4 @@ -// Copyright 2025 by PeopleWare n.v.. +// Copyright 2026 by PeopleWare n.v.. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -13,20 +13,42 @@ namespace PPWCode.AspNetCore.Server.I; +/// +/// Base class that carries the API version information needed to generate version-aware links. +/// public abstract class LinksContext { + /// + /// The name of the route parameter that holds the API version. + /// public const string VersionRouteParameter = "version"; + + /// + /// The default format used to render the into a route value. + /// public const string DefaultApiVersionFormat = "V"; - protected LinksContext( - ApiVersion apiVersion, - string? apiVersionFormat = null) + /// + /// Initializes a new instance of the class. + /// + /// The API version for which links are generated. + /// + /// The format used to render into a route value, or to use + /// . + /// + protected LinksContext(ApiVersion apiVersion, string? apiVersionFormat = null) { ApiVersion = apiVersion; ApiVersionFormat = apiVersionFormat ?? DefaultApiVersionFormat; } + /// + /// The API version for which links are generated. + /// public ApiVersion ApiVersion { get; } + /// + /// The format used to render the into a route value. + /// public string ApiVersionFormat { get; } } diff --git a/src/I/LinksManager.cs b/src/I/LinksManager.cs index fca6f98..8b00c51 100644 --- a/src/I/LinksManager.cs +++ b/src/I/LinksManager.cs @@ -22,6 +22,13 @@ namespace PPWCode.AspNetCore.Server.I; +/// +/// Default implementation that generates URLs through the current request's +/// . +/// +/// +/// The URL helper is created lazily from the current and cached for subsequent calls. +/// public class LinksManager : ILinksManager { private readonly IHttpContextAccessor _httpContextAccessor; @@ -29,6 +36,10 @@ public class LinksManager : ILinksManager private readonly object _locker = new(); private IUrlHelper? _urlHelper; + /// + /// Initializes a new instance of the class. + /// + /// The accessor used to retrieve the current . public LinksManager(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; @@ -42,6 +53,10 @@ public LinksManager(IHttpContextAccessor httpContextAccessor) public string? Link(string routeName, object? routeValues) => GetUrlHelper().Link(routeName, routeValues); + /// + /// Gets the URL helper for the current request. + /// + /// The URL helper created from the current endpoint's action context. private IUrlHelper GetUrlHelper() { if (_urlHelper is null) diff --git a/src/I/Transactional/TransactionalAttribute.cs b/src/I/Transactional/TransactionalAttribute.cs index 2601502..7cd2b19 100644 --- a/src/I/Transactional/TransactionalAttribute.cs +++ b/src/I/Transactional/TransactionalAttribute.cs @@ -1,4 +1,4 @@ -// Copyright 2025 by PeopleWare n.v.. +// Copyright 2026 by PeopleWare n.v.. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -14,16 +14,37 @@ namespace PPWCode.AspNetCore.Server.I.Transactional; +/// +/// Marks a controller or action as requiring (or explicitly not requiring) a database transaction. +/// +/// +/// When applied to a class, the setting applies to all of its actions; an attribute on an individual action +/// overrides the class-level setting. +/// [ExcludeFromCodeCoverage] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class TransactionalAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// + /// + /// to run the decorated controller or action within a transaction; otherwise + /// . + /// public TransactionalAttribute(bool transactional) { Transactional = transactional; IsolationLevel = IsolationLevel.Unspecified; } + /// + /// Indicates whether the decorated controller or action should run within a transaction. + /// public bool Transactional { get; } + + /// + /// The isolation level to use for the transaction. Defaults to . + /// public IsolationLevel IsolationLevel { get; set; } } From da3303432c0d97e3a5c0304bef324c6d77a6bf62 Mon Sep 17 00:00:00 2001 From: Danny Van den Wouwer Date: Tue, 16 Jun 2026 10:46:15 +0200 Subject: [PATCH 08/11] PPWCODE-15 Add structured exception formatting to enrich `ProblemDetails` in development mode --- src/I/Exceptions/BaseExceptionHandler.cs | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/I/Exceptions/BaseExceptionHandler.cs b/src/I/Exceptions/BaseExceptionHandler.cs index 981ffa5..beea1c5 100644 --- a/src/I/Exceptions/BaseExceptionHandler.cs +++ b/src/I/Exceptions/BaseExceptionHandler.cs @@ -105,6 +105,11 @@ public bool Handle(ExceptionContext context) .CreateProblemDetails( context.HttpContext, statusCode: statusCode.Value); + if (IsDevelopment && contextException is not null) + { + problemDetail.Extensions.Add("Exception", FormatException(contextException)); + } + EnrichProblemDetails(context, contextException, problemDetail); context.Result = new ObjectResult(problemDetail); return true; @@ -184,4 +189,26 @@ protected virtual void EnrichProblemDetails( ProblemDetails problemDetail) { } + + /// + /// Builds a structured, serialization-friendly representation of an exception (including its stack trace and + /// inner exceptions), added to the extensions when + /// is . + /// + /// The exception to format. + /// An object describing the exception, including its inner exception chain. + protected virtual object FormatException(Exception exception) + => new + { + Type = exception.GetType().FullName, + exception.Message, + StackTrace = + exception + .StackTrace? + .Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries), + InnerException = + exception.InnerException is { } inner + ? FormatException(inner) + : null + }; } From a50e0e040b18a4fa841c1284c1ae8cd90f3e6894 Mon Sep 17 00:00:00 2001 From: Danny Van den Wouwer Date: Tue, 16 Jun 2026 10:50:55 +0200 Subject: [PATCH 09/11] PPWCODE-15 Enable exception logging in `ApiUsageErrorExceptionHandler` and `ContractViolationExceptionHandler` --- src/I/Exceptions/ApiUsageErrorExceptionHandler.cs | 4 ++++ src/I/Exceptions/ContractViolationExceptionHandler.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs b/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs index c5781ae..18e1eac 100644 --- a/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs +++ b/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs @@ -27,6 +27,10 @@ public ApiUsageErrorExceptionHandler(ProblemDetailsFactory problemDetailsFactory { } + /// + protected override bool LogException + => true; + /// protected override int? GetStatusCode(ExceptionContext context, ApiUsageError? contextException) => StatusCodes.Status400BadRequest; diff --git a/src/I/Exceptions/ContractViolationExceptionHandler.cs b/src/I/Exceptions/ContractViolationExceptionHandler.cs index 05e88d9..089640d 100644 --- a/src/I/Exceptions/ContractViolationExceptionHandler.cs +++ b/src/I/Exceptions/ContractViolationExceptionHandler.cs @@ -31,6 +31,10 @@ public ContractViolationExceptionHandler(ProblemDetailsFactory problemDetailsFac { } + /// + protected override bool LogException + => true; + /// protected override int? GetStatusCode(ExceptionContext context, ContractViolation? contextException) => StatusCodes.Status500InternalServerError; From bb5e9fca9e5b95b7066c1c27ae91a847ffe5960d Mon Sep 17 00:00:00 2001 From: Danny Van den Wouwer Date: Tue, 16 Jun 2026 11:19:31 +0200 Subject: [PATCH 10/11] PPWCODE-15 Remove sealed modifier from `SemanticExceptionExceptionHandler` --- src/I/Exceptions/SemanticExceptionExceptionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/I/Exceptions/SemanticExceptionExceptionHandler.cs b/src/I/Exceptions/SemanticExceptionExceptionHandler.cs index b0fd249..77f7536 100644 --- a/src/I/Exceptions/SemanticExceptionExceptionHandler.cs +++ b/src/I/Exceptions/SemanticExceptionExceptionHandler.cs @@ -22,7 +22,7 @@ namespace PPWCode.AspNetCore.Server.I.Exceptions; /// [ExcludeFromCodeCoverage] -public sealed class SemanticExceptionExceptionHandler +public class SemanticExceptionExceptionHandler : BaseExceptionHandler { /// From d6260d67ef45aef4b77898dc6706d98b276a5376 Mon Sep 17 00:00:00 2001 From: Danny Van den Wouwer Date: Tue, 16 Jun 2026 11:19:40 +0200 Subject: [PATCH 11/11] PPWCODE-15 Add `ExceptionExceptionHandler` implementation --- src/I/Exceptions/ExceptionExceptionHandler.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/I/Exceptions/ExceptionExceptionHandler.cs diff --git a/src/I/Exceptions/ExceptionExceptionHandler.cs b/src/I/Exceptions/ExceptionExceptionHandler.cs new file mode 100644 index 0000000..0c38233 --- /dev/null +++ b/src/I/Exceptions/ExceptionExceptionHandler.cs @@ -0,0 +1,45 @@ +// Copyright 2026 by PeopleWare n.v.. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Hosting; + +namespace PPWCode.AspNetCore.Server.I.Exceptions; + +/// +/// +/// +/// +/// Use this exception handler to handle all exceptions, such as unhandled exceptions. This should be registered +/// last. +/// +/// +[ExcludeFromCodeCoverage] +public sealed class ExceptionExceptionHandler : BaseExceptionHandler +{ + /// + public ExceptionExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment) + : base(problemDetailsFactory, environment) + { + } + + /// + protected override bool LogException + => true; + + /// + protected override int? GetStatusCode(ExceptionContext context, Exception? contextException) + => StatusCodes.Status500InternalServerError; +}