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..509f32c
--- /dev/null
+++ b/docs/modules/ROOT/partials/changelog-2.0.0.adoc
@@ -0,0 +1,17 @@
+////
+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)
+* Improve documentation
+
+=== New
+* `ApiUsageErrorExceptionHandler`: initial version of exception handler for `ApiUsageError`
+* `ContractViolationExceptionHandler`: initial version of exception handler for `ContractViolation`
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"
}
}
diff --git a/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs b/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs
new file mode 100644
index 0000000..18e1eac
--- /dev/null
+++ b/src/I/Exceptions/ApiUsageErrorExceptionHandler.cs
@@ -0,0 +1,37 @@
+// 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 sealed class ApiUsageErrorExceptionHandler : BaseExceptionHandler
+{
+ ///
+ public ApiUsageErrorExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment)
+ : base(problemDetailsFactory, environment)
+ {
+ }
+
+ ///
+ protected override bool LogException
+ => true;
+
+ ///
+ protected override int? GetStatusCode(ExceptionContext context, ApiUsageError? contextException)
+ => StatusCodes.Status400BadRequest;
+}
diff --git a/src/I/Exceptions/BaseExceptionHandler.cs b/src/I/Exceptions/BaseExceptionHandler.cs
index 536b63b..beea1c5 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)
{
@@ -46,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;
@@ -70,25 +134,81 @@ 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,
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
+ };
}
diff --git a/src/I/Exceptions/ContractViolationExceptionHandler.cs b/src/I/Exceptions/ContractViolationExceptionHandler.cs
new file mode 100644
index 0000000..089640d
--- /dev/null
+++ b/src/I/Exceptions/ContractViolationExceptionHandler.cs
@@ -0,0 +1,41 @@
+// 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 sealed class ContractViolationExceptionHandler
+ : BaseExceptionHandler
+{
+ ///
+ public ContractViolationExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment)
+ : base(problemDetailsFactory, environment)
+ {
+ }
+
+ ///
+ protected override bool LogException
+ => true;
+
+ ///
+ protected override int? GetStatusCode(ExceptionContext context, ContractViolation? contextException)
+ => StatusCodes.Status500InternalServerError;
+}
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;
+}
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/OperationCanceledExceptionHandler.cs b/src/I/Exceptions/OperationCanceledExceptionHandler.cs
deleted file mode 100644
index 150aaa7..0000000
--- a/src/I/Exceptions/OperationCanceledExceptionHandler.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-
-using Microsoft.AspNetCore.Mvc.Filters;
-using Microsoft.AspNetCore.Mvc.Infrastructure;
-using Microsoft.Extensions.Hosting;
-
-namespace PPWCode.AspNetCore.Server.I.Exceptions;
-
-[ExcludeFromCodeCoverage]
-public sealed class OperationCanceledExceptionHandler
- : BaseExceptionHandler
-{
- public OperationCanceledExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment)
- : base(problemDetailsFactory, environment)
- {
- }
-
- ///
- protected override int? GetStatusCode(ExceptionContext context, OperationCanceledException? exception)
- => 499; // client closed
-}
diff --git a/src/I/Exceptions/OperationCancelledExceptionHandler.cs b/src/I/Exceptions/OperationCancelledExceptionHandler.cs
new file mode 100644
index 0000000..81960b9
--- /dev/null
+++ b/src/I/Exceptions/OperationCancelledExceptionHandler.cs
@@ -0,0 +1,34 @@
+// 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;
+using Microsoft.Extensions.Hosting;
+
+namespace PPWCode.AspNetCore.Server.I.Exceptions;
+
+///
+[ExcludeFromCodeCoverage]
+public sealed class OperationCancelledExceptionHandler
+ : BaseExceptionHandler
+{
+ ///
+ public OperationCancelledExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment)
+ : base(problemDetailsFactory, environment)
+ {
+ }
+
+ ///
+ protected override int? GetStatusCode(ExceptionContext context, OperationCanceledException? exception)
+ => 499; // client closed
+}
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/SemanticExceptionHandler.cs b/src/I/Exceptions/SemanticExceptionExceptionHandler.cs
similarity index 79%
rename from src/I/Exceptions/SemanticExceptionHandler.cs
rename to src/I/Exceptions/SemanticExceptionExceptionHandler.cs
index 139063e..77f7536 100644
--- a/src/I/Exceptions/SemanticExceptionHandler.cs
+++ b/src/I/Exceptions/SemanticExceptionExceptionHandler.cs
@@ -20,11 +20,13 @@
namespace PPWCode.AspNetCore.Server.I.Exceptions;
+///
[ExcludeFromCodeCoverage]
-public sealed class SemanticExceptionHandler
- : BaseExceptionHandler
+public class SemanticExceptionExceptionHandler
+ : BaseExceptionHandler
{
- public SemanticExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment)
+ ///
+ public SemanticExceptionExceptionHandler(ProblemDetailsFactory problemDetailsFactory, IHostEnvironment environment)
: base(problemDetailsFactory, environment)
{
}
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; }
}