Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using FluentAssertions;
using CR.Exceptions.AspNet.Options;
using FluentAssertions;
using Microsoft.AspNetCore.Http;

namespace CR.Exceptions.AspNet.UnitTests;
Expand Down
3 changes: 2 additions & 1 deletion CR.Exceptions.AspNet/CR.Exceptions.AspNet.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
</PropertyGroup>

<PropertyGroup>
<Description>ASP.NET Core integration for CR.Exceptions.</Description>
<Description>ASP.NET Core integration for CR.Exceptions.</Description>
<PackageId>$(NuGetPackagePrefix).Exceptions.AspNet</PackageId>
</PropertyGroup>

<ItemGroup>
Expand Down
8 changes: 4 additions & 4 deletions CR.Exceptions.AspNet/CrExceptionHandler.Logger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
26 changes: 14 additions & 12 deletions CR.Exceptions.AspNet/CrExceptionHandler.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<CrExceptionHandler> _logger;

public CrExceptionHandler(
IProblemDetailsService problemDetailsService,
IOptions<ExceptionMappingOptions> options,
IOptions<CrExceptionOptions> options,
ILogger<CrExceptionHandler> logger)
{
_problemDetailsService = problemDetailsService;
Expand All @@ -38,7 +39,8 @@ public async ValueTask<bool> 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;
Expand All @@ -48,25 +50,25 @@ public async ValueTask<bool> 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;
Expand All @@ -77,7 +79,7 @@ public async ValueTask<bool> 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,
Expand All @@ -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);
}
}
}
3 changes: 2 additions & 1 deletion CR.Exceptions.AspNet/ExceptionMappingOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
using CR.Exceptions.AspNet.Options;
using Microsoft.AspNetCore.Http;

namespace CR.Exceptions.AspNet;

Expand Down
7 changes: 7 additions & 0 deletions CR.Exceptions.AspNet/Options/CrExceptionOptions.cs
Original file line number Diff line number Diff line change
@@ -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();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace CR.Exceptions.AspNet;
namespace CR.Exceptions.AspNet.Options;

public sealed class ExceptionMappingOptions
{
Expand Down
6 changes: 6 additions & 0 deletions CR.Exceptions.AspNet/Options/ProblemDetailsOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace CR.Exceptions.AspNet.Options;

public sealed class ProblemDetailsOptions
{
public string Type { get; set; } = "about:blank";
}
7 changes: 4 additions & 3 deletions CR.Exceptions.AspNet/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using CR.Exceptions.AspNet.Options;
using Microsoft.Extensions.DependencyInjection;

namespace CR.Exceptions.AspNet;

Expand All @@ -10,11 +11,11 @@ public IServiceCollection AddCrExceptionHandler()
{
return services.AddCrExceptionHandler(options =>
{
options.AddDefaultMappings();
options.ExceptionMapping.AddDefaultMappings();
});
}

public IServiceCollection AddCrExceptionHandler(Action<ExceptionMappingOptions> setupAction)
public IServiceCollection AddCrExceptionHandler(Action<CrExceptionOptions> setupAction)
{
ArgumentNullException.ThrowIfNull(setupAction);

Expand Down
3 changes: 2 additions & 1 deletion CR.Exceptions/CR.Exceptions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
</PropertyGroup>

<PropertyGroup>
<Description>Common exception abstractions and error models for .NET applications.</Description>
<Description>Common exception abstractions and error models for .NET applications.</Description>
<PackageId>$(NuGetPackagePrefix).Exceptions</PackageId>
</PropertyGroup>

</Project>
2 changes: 2 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>

<NuGetPackagePrefix>CrCore</NuGetPackagePrefix>
</PropertyGroup>

<ItemGroup>
Expand Down
15 changes: 4 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ Or configure custom exception mappings.
```csharp
builder.Services.AddCrExceptionHandler(options =>
{
options.AddDefaultMappings();
options.Map<MyCustomException>(499);
options.ExceptionMapping.AddDefaultMappings();
options.ExceptionMapping.Map<MyCustomException>(499);
});
```

Expand All @@ -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);
```
Expand Down
Loading