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; } }