diff --git a/CR.Exceptions.AspNet.UnitTests/ExceptionMappingOptionsTests.cs b/CR.Exceptions.AspNet.UnitTests/ExceptionMappingOptionsTests.cs index aab85e9..e526bb5 100644 --- a/CR.Exceptions.AspNet.UnitTests/ExceptionMappingOptionsTests.cs +++ b/CR.Exceptions.AspNet.UnitTests/ExceptionMappingOptionsTests.cs @@ -1,4 +1,5 @@ -using FluentAssertions; +using CR.Exceptions.AspNet.Options; +using FluentAssertions; using Microsoft.AspNetCore.Http; namespace CR.Exceptions.AspNet.UnitTests; diff --git a/CR.Exceptions.AspNet/CR.Exceptions.AspNet.csproj b/CR.Exceptions.AspNet/CR.Exceptions.AspNet.csproj index df8c212..0ed816f 100644 --- a/CR.Exceptions.AspNet/CR.Exceptions.AspNet.csproj +++ b/CR.Exceptions.AspNet/CR.Exceptions.AspNet.csproj @@ -7,7 +7,8 @@ - ASP.NET Core integration for CR.Exceptions. + ASP.NET Core integration for CR.Exceptions. + $(NuGetPackagePrefix).Exceptions.AspNet diff --git a/CR.Exceptions.AspNet/CrExceptionHandler.Logger.cs b/CR.Exceptions.AspNet/CrExceptionHandler.Logger.cs index 4ac5efc..82031b8 100644 --- a/CR.Exceptions.AspNet/CrExceptionHandler.Logger.cs +++ b/CR.Exceptions.AspNet/CrExceptionHandler.Logger.cs @@ -12,21 +12,21 @@ public sealed partial class CrExceptionHandler [LoggerMessage( Level = LogLevel.Warning, Message = "No HTTP status code mapping found for exception type '{ExceptionType}'. Using 500 Internal Server Error.")] - private static partial void LogMissingHttpStatusMapping(ILogger logger, Exception exception, string? exceptionType); + private static partial void LogMissingHttpStatusMapping(ILogger logger, Exception exception, string exceptionType); [LoggerMessage( Level = LogLevel.Debug, Message = "Application exception of type '{ExceptionType}' occurred.")] - private static partial void LogApplicationException(ILogger logger, Exception exception, string? exceptionType); + private static partial void LogApplicationException(ILogger logger, Exception exception, string exceptionType); [LoggerMessage( Level = LogLevel.Error, Message = "An unexpected exception of type '{ExceptionType}' occurred.")] - private static partial void LogUnhandledException(ILogger logger, Exception exception, string? exceptionType); + private static partial void LogUnhandledException(ILogger logger, Exception exception, string exceptionType); [LoggerMessage( Level = LogLevel.Warning, - Message = "ProblemDetails extension key '{Key}' already exists. The value was overwritten.")] + Message = "The ProblemDetails extension '{Key}' was overwritten while building the error response.")] private static partial void LogProblemDetailsExtensionOverwritten(ILogger logger, string key); [LoggerMessage( diff --git a/CR.Exceptions.AspNet/CrExceptionHandler.cs b/CR.Exceptions.AspNet/CrExceptionHandler.cs index b4c48e1..f6f9afb 100644 --- a/CR.Exceptions.AspNet/CrExceptionHandler.cs +++ b/CR.Exceptions.AspNet/CrExceptionHandler.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Diagnostics; +using CR.Exceptions.AspNet.Options; +using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; @@ -10,18 +11,18 @@ namespace CR.Exceptions.AspNet; public sealed partial class CrExceptionHandler : IExceptionHandler { - private static readonly CrError[] FallbackInternalErrors = + private static readonly CrError[] DefaultInternalErrors = [ new(ErrorCodes.InternalError, "An unexpected internal error occurred.") ]; private readonly IProblemDetailsService _problemDetailsService; - private readonly ExceptionMappingOptions _options; + private readonly CrExceptionOptions _options; private readonly ILogger _logger; public CrExceptionHandler( IProblemDetailsService problemDetailsService, - IOptions options, + IOptions options, ILogger logger) { _problemDetailsService = problemDetailsService; @@ -38,7 +39,8 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e } var httpStatusCode = StatusCodes.Status500InternalServerError; - var exceptionTypeFullName = exception.GetType().FullName; + var exceptionType = exception.GetType(); + var exceptionTypeName = exceptionType.FullName ?? exceptionType.Name; CrError[] errors; string detail; @@ -48,25 +50,25 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e detail = crException.Message; errors = crException.Errors; - var statusCode = _options.FindHttpStatusCode(crException); + var statusCode = _options.ExceptionMapping.FindHttpStatusCode(crException); if (statusCode is null) { - LogMissingHttpStatusMapping(_logger, exception, exceptionTypeFullName); + LogMissingHttpStatusMapping(_logger, exception, exceptionTypeName); } else { httpStatusCode = statusCode.Value; } - LogApplicationException(_logger, exception, exceptionTypeFullName); + LogApplicationException(_logger, exception, exceptionTypeName); } else { detail = "An unexpected error occurred."; - errors = FallbackInternalErrors; + errors = DefaultInternalErrors; - LogUnhandledException(_logger, exception, exceptionTypeFullName); + LogUnhandledException(_logger, exception, exceptionTypeName); } var traceId = Activity.Current?.TraceId.ToHexString() ?? httpContext.TraceIdentifier; @@ -77,7 +79,7 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e Exception = exception, ProblemDetails = { - Type = "about:blank", + Type = _options.ProblemDetails.Type, Status = httpStatusCode, Title = string.IsNullOrWhiteSpace(title) ? "An error occurred" : title, Detail = detail, @@ -104,8 +106,8 @@ private void AddProblemDetailsExtension(ProblemDetails problemDetails, string ke { if (!problemDetails.Extensions.TryAdd(key, value)) { - LogProblemDetailsExtensionOverwritten(_logger, key); problemDetails.Extensions[key] = value; + LogProblemDetailsExtensionOverwritten(_logger, key); } } } \ No newline at end of file diff --git a/CR.Exceptions.AspNet/ExceptionMappingOptionsExtensions.cs b/CR.Exceptions.AspNet/ExceptionMappingOptionsExtensions.cs index 904bc5c..6d1d86c 100644 --- a/CR.Exceptions.AspNet/ExceptionMappingOptionsExtensions.cs +++ b/CR.Exceptions.AspNet/ExceptionMappingOptionsExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Http; +using CR.Exceptions.AspNet.Options; +using Microsoft.AspNetCore.Http; namespace CR.Exceptions.AspNet; diff --git a/CR.Exceptions.AspNet/Options/CrExceptionOptions.cs b/CR.Exceptions.AspNet/Options/CrExceptionOptions.cs new file mode 100644 index 0000000..1780a73 --- /dev/null +++ b/CR.Exceptions.AspNet/Options/CrExceptionOptions.cs @@ -0,0 +1,7 @@ +namespace CR.Exceptions.AspNet.Options; + +public sealed class CrExceptionOptions +{ + public ExceptionMappingOptions ExceptionMapping { get; init; } = new(); + public ProblemDetailsOptions ProblemDetails { get; init; } = new(); +} \ No newline at end of file diff --git a/CR.Exceptions.AspNet/ExceptionMappingOptions.cs b/CR.Exceptions.AspNet/Options/ExceptionMappingOptions.cs similarity index 95% rename from CR.Exceptions.AspNet/ExceptionMappingOptions.cs rename to CR.Exceptions.AspNet/Options/ExceptionMappingOptions.cs index 999c598..f04cd60 100644 --- a/CR.Exceptions.AspNet/ExceptionMappingOptions.cs +++ b/CR.Exceptions.AspNet/Options/ExceptionMappingOptions.cs @@ -1,4 +1,4 @@ -namespace CR.Exceptions.AspNet; +namespace CR.Exceptions.AspNet.Options; public sealed class ExceptionMappingOptions { diff --git a/CR.Exceptions.AspNet/Options/ProblemDetailsOptions.cs b/CR.Exceptions.AspNet/Options/ProblemDetailsOptions.cs new file mode 100644 index 0000000..3ee4c6f --- /dev/null +++ b/CR.Exceptions.AspNet/Options/ProblemDetailsOptions.cs @@ -0,0 +1,6 @@ +namespace CR.Exceptions.AspNet.Options; + +public sealed class ProblemDetailsOptions +{ + public string Type { get; set; } = "about:blank"; +} \ No newline at end of file diff --git a/CR.Exceptions.AspNet/ServiceCollectionExtensions.cs b/CR.Exceptions.AspNet/ServiceCollectionExtensions.cs index 2e8bf97..e3719c4 100644 --- a/CR.Exceptions.AspNet/ServiceCollectionExtensions.cs +++ b/CR.Exceptions.AspNet/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using CR.Exceptions.AspNet.Options; +using Microsoft.Extensions.DependencyInjection; namespace CR.Exceptions.AspNet; @@ -10,11 +11,11 @@ public IServiceCollection AddCrExceptionHandler() { return services.AddCrExceptionHandler(options => { - options.AddDefaultMappings(); + options.ExceptionMapping.AddDefaultMappings(); }); } - public IServiceCollection AddCrExceptionHandler(Action setupAction) + public IServiceCollection AddCrExceptionHandler(Action setupAction) { ArgumentNullException.ThrowIfNull(setupAction); diff --git a/CR.Exceptions/CR.Exceptions.csproj b/CR.Exceptions/CR.Exceptions.csproj index 5e36cca..7bf1a01 100644 --- a/CR.Exceptions/CR.Exceptions.csproj +++ b/CR.Exceptions/CR.Exceptions.csproj @@ -7,7 +7,8 @@ - Common exception abstractions and error models for .NET applications. + Common exception abstractions and error models for .NET applications. + $(NuGetPackagePrefix).Exceptions diff --git a/Directory.Build.props b/Directory.Build.props index a39f15e..f931d6f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,6 +12,8 @@ README.md Apache-2.0 + + CrCore diff --git a/README.md b/README.md index 401733d..cd8e9a6 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ Or configure custom exception mappings. ```csharp builder.Services.AddCrExceptionHandler(options => { - options.AddDefaultMappings(); - options.Map(499); + options.ExceptionMapping.AddDefaultMappings(); + options.ExceptionMapping.Map(499); }); ``` @@ -35,21 +35,14 @@ Example: ```csharp public sealed class UserNotFoundException : NotFoundException { - public UserNotFoundException(Guid userId) - : base( - [ - new CrError( - "UserNotFound", - $"User '{userId}' was not found.") - ], + public UserNotFoundException(Guid userId) : base( + [new CrError("UserNotFound", $"User '{userId}' was not found.")], "User was not found.") { } } ``` -Each `CrException` contains one or more `CrError` objects. - ```csharp public sealed record class CrError(string Code, string Message); ```