diff --git a/CR.Exceptions.AspNet.Tests/CR.Exceptions.AspNet.Tests.csproj b/CR.Exceptions.AspNet.Tests/CR.Exceptions.AspNet.Tests.csproj
index 9d00fd5..0d197aa 100644
--- a/CR.Exceptions.AspNet.Tests/CR.Exceptions.AspNet.Tests.csproj
+++ b/CR.Exceptions.AspNet.Tests/CR.Exceptions.AspNet.Tests.csproj
@@ -22,6 +22,7 @@
+
diff --git a/CR.Exceptions.AspNet.Tests/Component/CrExceptionHandlerTests.cs b/CR.Exceptions.AspNet.Tests/Component/CrExceptionHandlerTests.cs
index 1b0be3f..716a7b0 100644
--- a/CR.Exceptions.AspNet.Tests/Component/CrExceptionHandlerTests.cs
+++ b/CR.Exceptions.AspNet.Tests/Component/CrExceptionHandlerTests.cs
@@ -1,4 +1,5 @@
-using Microsoft.AspNetCore.Diagnostics;
+using CR.Exceptions.Tests.Shared;
+using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
@@ -19,11 +20,11 @@ public CrExceptionHandlerTests(ITestOutputHelper output)
}
[Fact]
- public Task Should_Return_404_For_NotFoundException()
+ public Task Should_Return_500_For_InternalException()
{
return AssertHandlerResult(
- new TestNotFoundException(),
- StatusCodes.Status404NotFound,
+ new TestInternalException(),
+ StatusCodes.Status500InternalServerError,
canCreateActivity: true);
}
@@ -37,7 +38,7 @@ public Task Should_Return_500_For_UnhandledException()
}
[Fact]
- public Task Should_Return_500_For_UnhandledException_When_Activity_Is_Missing()
+ public Task Should_Return_500_For_UnhandledException_When_ActivityIsMissing()
{
return AssertHandlerResult(
new InvalidOperationException(),
@@ -98,7 +99,7 @@ private static ServiceProvider CreateServiceProvider()
{
return new ServiceCollection()
.AddLogging()
- .AddCrExceptions()
+ .AddCrExceptionsCore()
.BuildServiceProvider();
}
diff --git a/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs b/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs
index 262a621..80d9d0e 100644
--- a/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs
+++ b/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs
@@ -1,35 +1,39 @@
using CR.Exceptions.AspNet.Mapping;
+using CR.Exceptions.Tests.Shared;
using Microsoft.Extensions.Logging;
namespace CR.Exceptions.AspNet.Tests.Component;
public sealed class LogLevelMapTests
{
+ private const LogLevel ExpectedLogLevel = LogLevel.Warning;
+ private static readonly TestInternalException ExistentException = new();
+ private static readonly TestUnknownException NonExistentException = new();
+
[Fact]
- public void TryFind_ShouldReturn_Level_For_NotFoundException()
+ public void TryFind_ShouldReturn_TrueAndLevel_WhenExceptionExists()
{
- var level = LogLevel.Warning;
- var map = CreateMap(builder => builder.Map(level));
-
- var result = map.TryFind(new TestNotFoundException(), out var actualLevel);
+ var map = GetDefaultMap();
+ var result = map.TryFind(ExistentException, out var actualLevel);
Assert.True(result);
- Assert.Equal(level, actualLevel);
+ Assert.Equal(ExpectedLogLevel, actualLevel);
}
[Fact]
- public void TryFind_ShouldReturn_False_For_UnregisteredException()
+ public void TryFind_ShouldReturn_FalseAndDefault_WhenExceptionDoesNotExist()
{
- var map = CreateMap();
+ var map = GetDefaultMap();
+ var result = map.TryFind(NonExistentException, out var level);
- Assert.False(map.TryFind(new TestUnregisteredException(), out var _));
+ Assert.False(result);
+ Assert.Equal(default, level);
}
- private static LogLevelMap CreateMap(Action? configurator = null)
+ private static LogLevelMap GetDefaultMap()
{
- var builder = new LogLevelMapBuilder();
- configurator?.Invoke(builder);
-
- return builder.Build();
+ return new LogLevelMapBuilder()
+ .Map(ExpectedLogLevel)
+ .Build();
}
}
\ No newline at end of file
diff --git a/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs b/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs
index 3a426d4..f2b0e92 100644
--- a/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs
+++ b/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs
@@ -1,35 +1,39 @@
using CR.Exceptions.AspNet.Mapping;
+using CR.Exceptions.Tests.Shared;
using Microsoft.AspNetCore.Http;
namespace CR.Exceptions.AspNet.Tests.Component;
public sealed class StatusCodeMapTests
{
+ private const int ExpectedStatusCode = StatusCodes.Status500InternalServerError;
+ private static readonly TestInternalException ExistentException = new();
+ private static readonly TestUnknownException NonExistentException = new();
+
[Fact]
- public void TryFind_ShouldReturn_404_For_NotFoundException()
+ public void TryFind_ShouldReturn_TrueAndCode_WhenExceptionExists()
{
- var code = StatusCodes.Status404NotFound;
- var map = CreateMap(builder => builder.Map(code));
-
- var result = map.TryFind(new TestNotFoundException(), out var actualCode);
+ var map = GetDefaultMap();
+ var result = map.TryFind(ExistentException, out var actualCode);
Assert.True(result);
- Assert.Equal(code, actualCode);
+ Assert.Equal(ExpectedStatusCode, actualCode);
}
[Fact]
- public void TryFind_ShouldReturn_False_For_UnregisteredException()
+ public void TryFind_ShouldReturn_FalseAndDefault_WhenExceptionDoesNotExist()
{
- var map = CreateMap();
+ var map = GetDefaultMap();
+ var result = map.TryFind(NonExistentException, out var code);
- Assert.False(map.TryFind(new TestUnregisteredException(), out var _));
+ Assert.False(result);
+ Assert.Equal(default, code);
}
- private static StatusCodeMap CreateMap(Action? configurator = null)
+ private static StatusCodeMap GetDefaultMap()
{
- var builder = new StatusCodeMapBuilder();
- configurator?.Invoke(builder);
-
- return builder.Build();
+ return new StatusCodeMapBuilder()
+ .Map(ExpectedStatusCode)
+ .Build();
}
}
\ No newline at end of file
diff --git a/CR.Exceptions.AspNet.Tests/TestNotFoundException.cs b/CR.Exceptions.AspNet.Tests/TestNotFoundException.cs
deleted file mode 100644
index caee3f5..0000000
--- a/CR.Exceptions.AspNet.Tests/TestNotFoundException.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace CR.Exceptions.AspNet.Tests;
-
-internal sealed class TestNotFoundException : NotFoundException
-{
- public TestNotFoundException() : base([new("TestNotFound", "Test entity not found error message")])
- {
- }
-}
\ No newline at end of file
diff --git a/CR.Exceptions.AspNet.Tests/TestUnregisteredException.cs b/CR.Exceptions.AspNet.Tests/TestUnregisteredException.cs
deleted file mode 100644
index 4d9a96b..0000000
--- a/CR.Exceptions.AspNet.Tests/TestUnregisteredException.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace CR.Exceptions.AspNet.Tests;
-
-internal sealed class TestUnregisteredException : CrException
-{
- public TestUnregisteredException() : base([new("TestUnregistered", "Test error message")], "Unregistered detail")
- {
- }
-}
\ No newline at end of file
diff --git a/CR.Exceptions.AspNet/CR.Exceptions.AspNet.csproj b/CR.Exceptions.AspNet/CR.Exceptions.AspNet.csproj
index fb57f64..a442889 100644
--- a/CR.Exceptions.AspNet/CR.Exceptions.AspNet.csproj
+++ b/CR.Exceptions.AspNet/CR.Exceptions.AspNet.csproj
@@ -4,6 +4,7 @@
net10.0
enable
enable
+ true
diff --git a/CR.Exceptions.AspNet/CrExceptionHandler.cs b/CR.Exceptions.AspNet/CrExceptionHandler.cs
index 2e21e36..ca3d52e 100644
--- a/CR.Exceptions.AspNet/CrExceptionHandler.cs
+++ b/CR.Exceptions.AspNet/CrExceptionHandler.cs
@@ -9,8 +9,8 @@ namespace CR.Exceptions.AspNet;
public sealed class CrExceptionHandler : IExceptionHandler
{
- private static readonly ImmutableArray DefaultInternalErrors =
- [new("InternalError", "An unexpected internal error occurred.")];
+ private static readonly ImmutableArray DefaultInternalErrors
+ = [new("InternalError", "An unexpected internal error occurred.")];
private readonly IProblemDetailsService _problemDetailsService;
private readonly ILogger _logger;
diff --git a/CR.Exceptions.AspNet/CrExceptionHandlerLogExtensions.cs b/CR.Exceptions.AspNet/CrExceptionHandlerLogExtensions.cs
index a82a7d5..0187174 100644
--- a/CR.Exceptions.AspNet/CrExceptionHandlerLogExtensions.cs
+++ b/CR.Exceptions.AspNet/CrExceptionHandlerLogExtensions.cs
@@ -54,7 +54,7 @@ private static class LogIds
[LoggerMessage(
EventId = LogIds.MissingLogLevelMapping,
- Level = LogLevel.Debug,
+ Level = LogLevel.Warning,
Message = "No log level mapping found for exception type '{ExceptionType}'. Using fallback log level '{FallbackLogLevel}'.")]
public static partial void LogMissingLogLevelMapping(this ILogger logger, string? exceptionType, LogLevel fallbackLogLevel);
}
\ No newline at end of file
diff --git a/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs b/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs
index a04555e..1ef880e 100644
--- a/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs
+++ b/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs
@@ -1,11 +1,14 @@
-using Microsoft.Extensions.Logging;
+using CR.Exceptions.Mapping;
+using Microsoft.Extensions.Logging;
using System.Collections.Frozen;
+using System.Diagnostics.CodeAnalysis;
namespace CR.Exceptions.AspNet.Mapping;
-public sealed class LogLevelMap : TypeMap
+public class LogLevelMap : TypeMap
{
- internal LogLevelMap(FrozenDictionary dictionary) : base(dictionary)
- {
- }
+ internal LogLevelMap(FrozenDictionary dictionary) : base(dictionary) { }
+
+ public bool TryFind(CrException exception, [MaybeNullWhen(false)] out LogLevel level)
+ => TryGetByHierarchy(exception.GetType(), out level);
}
\ No newline at end of file
diff --git a/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs b/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs
index f2319f4..5660cb5 100644
--- a/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs
+++ b/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs
@@ -1,26 +1,33 @@
-using Microsoft.Extensions.Logging;
+using CR.Exceptions.Mapping;
+using Microsoft.Extensions.Logging;
namespace CR.Exceptions.AspNet.Mapping;
-public sealed class LogLevelMapBuilder : TypeMapBuilder
+public class LogLevelMapBuilder : MapBuilder
{
- public LogLevelMap Build()
+ public LogLevelMapBuilder Map(LogLevel level) where TException : CrException
{
- return new(BuildFrozenDictionary());
+ ThrowIfInvalidLevel(level);
+ AddPair(typeof(TException), level);
+
+ return this;
}
- protected override void ThrowIfInvalidValue(LogLevel value)
+ public LogLevelMap Build()
+ => new(BuildFrozenDictionary());
+
+ private static void ThrowIfInvalidLevel(LogLevel level)
{
- if (!Enum.IsDefined(value))
+ if (!Enum.IsDefined(level))
{
throw new ArgumentOutOfRangeException(
- nameof(value), value, $"The value '{value}' is not a valid {nameof(LogLevel)}.");
+ nameof(level), level, $"The value '{level}' is not a valid {nameof(LogLevel)}.");
}
- if (value == LogLevel.None)
+ if (level is LogLevel.None)
{
throw new ArgumentException(
- $"{nameof(LogLevel)}.{nameof(LogLevel.None)} cannot be used for exception mapping.", nameof(value));
+ $"{nameof(LogLevel)}.{nameof(LogLevel.None)} cannot be used for exception mapping.", nameof(level));
}
}
}
\ No newline at end of file
diff --git a/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilderExtensions.cs b/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilderExtensions.cs
index e40716d..77a22ae 100644
--- a/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilderExtensions.cs
+++ b/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilderExtensions.cs
@@ -8,10 +8,14 @@ public static class LogLevelMapBuilderExtensions
{
public LogLevelMapBuilder AddDefaultMappings()
{
- builder
+ return builder
+ .Map(LogLevel.Debug)
+ .Map(LogLevel.Debug)
+ .Map(LogLevel.Debug)
+ .Map(LogLevel.Debug)
+ .Map(LogLevel.Debug)
+ .Map(LogLevel.Debug)
.Map(LogLevel.Error);
-
- return builder;
}
}
}
\ No newline at end of file
diff --git a/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs b/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs
index 3fe076a..50c85bc 100644
--- a/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs
+++ b/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs
@@ -1,10 +1,13 @@
-using System.Collections.Frozen;
+using CR.Exceptions.Mapping;
+using System.Collections.Frozen;
+using System.Diagnostics.CodeAnalysis;
namespace CR.Exceptions.AspNet.Mapping;
-public sealed class StatusCodeMap : TypeMap
+public class StatusCodeMap : TypeMap
{
- internal StatusCodeMap(FrozenDictionary dictionary) : base(dictionary)
- {
- }
+ internal StatusCodeMap(FrozenDictionary dictionary) : base(dictionary) { }
+
+ public bool TryFind(CrException exception, [MaybeNullWhen(false)] out int code)
+ => TryGetByHierarchy(exception.GetType(), out code);
}
\ No newline at end of file
diff --git a/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilder.cs b/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilder.cs
index 4ab0eca..8db409e 100644
--- a/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilder.cs
+++ b/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilder.cs
@@ -1,20 +1,27 @@
-using System.Net;
+using CR.Exceptions.Mapping;
+using System.Net;
namespace CR.Exceptions.AspNet.Mapping;
-public sealed class StatusCodeMapBuilder : TypeMapBuilder
+public class StatusCodeMapBuilder : MapBuilder
{
- public StatusCodeMap Build()
+ public StatusCodeMapBuilder Map(int code) where TException : CrException
{
- return new(BuildFrozenDictionary());
+ ThrowIfInvalidCode(code);
+ AddPair(typeof(TException), code);
+
+ return this;
}
- protected override void ThrowIfInvalidValue(int value)
+ public StatusCodeMap Build()
+ => new(BuildFrozenDictionary());
+
+ private static void ThrowIfInvalidCode(int code)
{
- if (!Enum.IsDefined(typeof(HttpStatusCode), value))
+ if (!Enum.IsDefined(typeof(HttpStatusCode), code))
{
throw new ArgumentOutOfRangeException(
- nameof(value), $"'{value}' is not a standard HTTP status code.");
+ nameof(code), $"'{code}' is not a standard HTTP status code.");
}
}
}
\ No newline at end of file
diff --git a/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilderExtensions.cs b/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilderExtensions.cs
index ecd4606..597a311 100644
--- a/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilderExtensions.cs
+++ b/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilderExtensions.cs
@@ -8,7 +8,7 @@ public static class StatusCodeMapBuilderExtensions
{
public StatusCodeMapBuilder AddDefaultMappings()
{
- builder
+ return builder
.Map(StatusCodes.Status400BadRequest)
.Map(StatusCodes.Status401Unauthorized)
.Map(StatusCodes.Status403Forbidden)
@@ -16,8 +16,6 @@ public StatusCodeMapBuilder AddDefaultMappings()
.Map(StatusCodes.Status409Conflict)
.Map(StatusCodes.Status422UnprocessableEntity)
.Map(StatusCodes.Status500InternalServerError);
-
- return builder;
}
}
}
\ No newline at end of file
diff --git a/CR.Exceptions.AspNet/Mapping/TypeMapBuilder.cs b/CR.Exceptions.AspNet/Mapping/TypeMapBuilder.cs
deleted file mode 100644
index 6f866d6..0000000
--- a/CR.Exceptions.AspNet/Mapping/TypeMapBuilder.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using CR.Exceptions.Mapping;
-
-namespace CR.Exceptions.AspNet.Mapping;
-
-public abstract class TypeMapBuilder : MapBuilder
-{
- public TypeMapBuilder Map(TValue value) where TException : CrException
- {
- ThrowIfInvalidValue(value);
- Add(typeof(TException), value);
-
- return this;
- }
-
- protected abstract void ThrowIfInvalidValue(TValue value);
-}
\ No newline at end of file
diff --git a/CR.Exceptions.AspNet/README.md b/CR.Exceptions.AspNet/README.md
index 54153bc..c644228 100644
--- a/CR.Exceptions.AspNet/README.md
+++ b/CR.Exceptions.AspNet/README.md
@@ -23,7 +23,7 @@ dotnet add package CrCore.Exceptions.AspNet
Register the default exception handling during application startup.
```csharp
-builder.Services.AddCrExceptions();
+builder.Services.AddCrExceptionsCore();
app.UseExceptionHandler();
```
@@ -59,8 +59,13 @@ builder.Services.AddCrStatusCodeMapping(builder =>
| Exception | Log Level |
|-----------|------------:|
+| `ValidationException` | Debug |
+| `UnauthorizedException` | Debug |
+| `ForbiddenException` | Debug |
+| `NotFoundException` | Debug |
+| `ConflictException` | Debug |
+| `UnprocessableException` | Debug |
| `InternalException` | Error |
-| `OtherUnregistered` | Debug |
# Custom Log Level Mapping
@@ -83,18 +88,18 @@ Example:
```json
{
- "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
- "title": "Not Found",
- "status": 404,
- "detail": "The requested resource was not found.",
- "instance": "/api/users/1",
+ "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
+ "title": "An error occurred while processing your request.",
+ "status": 500,
+ "detail": "An unexpected internal error occurred.",
+ "instance": "/api/test",
"errors": [
{
- "code": "Identity.UserNotFound",
- "message": "User was not found."
+ "code": "TestInternalCode",
+ "message": "TestInternalMessage"
}
],
- "traceId": "..."
+ "traceId": "1ca274bed877413cefd8094fc63bd559"
}
```
@@ -114,13 +119,13 @@ Example:
"title": "An error occurred while processing your request.",
"status": 500,
"detail": "An unexpected error occurred.",
- "instance": "/api/users/1",
+ "instance": "/api/test",
"errors": [
{
"code": "InternalError",
"message": "An unexpected internal error occurred."
}
],
- "traceId": "..."
+ "traceId": "b217277ea131750f161bc6e8d8b33302"
}
```
\ No newline at end of file
diff --git a/CR.Exceptions.AspNet/ServiceCollectionExtensions.cs b/CR.Exceptions.AspNet/ServiceCollectionExtensions.cs
index 532296f..c5a2965 100644
--- a/CR.Exceptions.AspNet/ServiceCollectionExtensions.cs
+++ b/CR.Exceptions.AspNet/ServiceCollectionExtensions.cs
@@ -7,7 +7,7 @@ public static class ServiceCollectionExtensions
{
extension(IServiceCollection services)
{
- public IServiceCollection AddCrExceptions()
+ public IServiceCollection AddCrExceptionsCore()
{
return services
.AddCrExceptionHandler()
diff --git a/CR.Exceptions.Tests.Shared/CR.Exceptions.Tests.Shared.csproj b/CR.Exceptions.Tests.Shared/CR.Exceptions.Tests.Shared.csproj
new file mode 100644
index 0000000..5c75d70
--- /dev/null
+++ b/CR.Exceptions.Tests.Shared/CR.Exceptions.Tests.Shared.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CR.Exceptions.Tests.Shared/TestInternalException.cs b/CR.Exceptions.Tests.Shared/TestInternalException.cs
new file mode 100644
index 0000000..3836a57
--- /dev/null
+++ b/CR.Exceptions.Tests.Shared/TestInternalException.cs
@@ -0,0 +1,10 @@
+using System.Collections.Immutable;
+
+namespace CR.Exceptions.Tests.Shared;
+
+public sealed class TestInternalException : InternalException
+{
+ private static readonly ImmutableArray _errors = [new("TestInternalCode", "TestInternalMessage")];
+
+ public TestInternalException() : base(_errors) { }
+}
\ No newline at end of file
diff --git a/CR.Exceptions.Tests.Shared/TestUnknownException.cs b/CR.Exceptions.Tests.Shared/TestUnknownException.cs
new file mode 100644
index 0000000..e7d5a29
--- /dev/null
+++ b/CR.Exceptions.Tests.Shared/TestUnknownException.cs
@@ -0,0 +1,10 @@
+using System.Collections.Immutable;
+
+namespace CR.Exceptions.Tests.Shared;
+
+public sealed class TestUnknownException : CrException
+{
+ private static readonly ImmutableArray _errors = [new("TestUnknownCode", "TestUnknownMessage")];
+
+ public TestUnknownException() : base(_errors, "Test unknown exception message") { }
+}
\ No newline at end of file
diff --git a/CR.Exceptions.Tests/CR.Exceptions.Tests.csproj b/CR.Exceptions.Tests/CR.Exceptions.Tests.csproj
index 566be10..4f5554c 100644
--- a/CR.Exceptions.Tests/CR.Exceptions.Tests.csproj
+++ b/CR.Exceptions.Tests/CR.Exceptions.Tests.csproj
@@ -21,7 +21,7 @@
-
+
diff --git a/CR.Exceptions.Tests/Component/ErrorMapTests.cs b/CR.Exceptions.Tests/Component/ErrorMapTests.cs
deleted file mode 100644
index ebe48be..0000000
--- a/CR.Exceptions.Tests/Component/ErrorMapTests.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using CR.Exceptions.Mapping;
-
-namespace CR.Exceptions.Tests.Component;
-
-public sealed class ErrorMapTests
-{
- [Fact]
- public void TryGet_ShouldReturn_Errors_WhenCodeExists()
- {
- const string errorCode = "InvalidGrant";
- const string registrationCode = "invalid_grant";
-
- var map = new ErrorMapBuilder()
- .Add(new(registrationCode, [new(errorCode, "Invalid username or password.")]))
- .Build();
-
- var result = map.TryGet(registrationCode, out var errors);
-
- Assert.True(result);
- var singleError = Assert.Single(errors);
- Assert.Equal(errorCode, singleError.Code);
- }
-
- [Fact]
- public void TryGet_ShouldReturn_False_WhenCodeNotExist()
- {
- var map = new ErrorMapBuilder()
- .Add(new("?", [new("?", "?")]))
- .Build();
-
- var result = map.TryGet("non_existent_code", out var errors);
-
- Assert.False(result);
- Assert.True(errors.IsDefaultOrEmpty);
- }
-}
\ No newline at end of file
diff --git a/CR.Exceptions.Tests/Component/ExceptionFactoryTests.cs b/CR.Exceptions.Tests/Component/ExceptionFactoryTests.cs
index 4a697c8..0790f52 100644
--- a/CR.Exceptions.Tests/Component/ExceptionFactoryTests.cs
+++ b/CR.Exceptions.Tests/Component/ExceptionFactoryTests.cs
@@ -1,41 +1,56 @@
using CR.Exceptions.Mapping;
+using CR.Exceptions.Tests.Shared;
namespace CR.Exceptions.Tests.Component;
public sealed class ExceptionFactoryTests
{
+ private const string ExistentCode = "Test";
+ private const string NonExistentCode = "non_existent_code";
+
[Fact]
- public void TryCreate_ShouldReturn_Exception_WhenCodeExists()
+ public void TryCreate_ShouldReturn_TrueAndException_WhenCodeExists()
{
- const string errorCode = "TestError";
- const string registrationCode = "test_error";
+ var factory = GetDefaultFactory(ExistentCode);
+ var result = factory.TryCreate(ExistentCode, out var exception);
- var errorRegistration = new ErrorRegistration(registrationCode, [new(errorCode, "Something went wrong.")]);
+ Assert.True(result);
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ }
- var factory = new ExceptionFactoryBuilder()
- .Add(new(errorRegistration, errors => new TestException(errors)))
- .Build();
+ [Fact]
+ public void TryCreate_ShouldReturn_FalseAndNull_WhenCodeDoesNotExist()
+ {
+ var factory = GetDefaultFactory("?");
+ var result = factory.TryCreate(NonExistentCode, out var exception);
- var result = factory.TryCreate(registrationCode, out var exception);
+ Assert.False(result);
+ Assert.Null(exception);
+ }
- Assert.True(result);
- Assert.NotNull(exception);
+ [Fact]
+ public void Create_ShouldReturn_Exception_WhenCodeExists()
+ {
+ var factory = GetDefaultFactory(ExistentCode);
+ var exception = factory.Create(ExistentCode);
- var typedException = Assert.IsType(exception);
- var singleError = Assert.Single(typedException.Errors);
- Assert.Equal(errorCode, singleError.Code);
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
}
[Fact]
- public void TryCreate_ShouldReturn_False_WhenCodeNotExist()
+ public void Create_ShouldThrow_WhenCodeDoesNotExist()
{
- var factory = new ExceptionFactoryBuilder()
- .Add(new(new("?", [new("?", "?")]), errors => new TestException(errors)))
- .Build();
+ var factory = GetDefaultFactory("?");
- var result = factory.TryCreate("non_existent_code", out var exception);
+ Assert.Throws(() => factory.Create(NonExistentCode));
+ }
- Assert.False(result);
- Assert.Null(exception);
+ private static ExceptionFactory GetDefaultFactory(string code)
+ {
+ return new ExceptionFactoryBuilder()
+ .Map(code, () => new TestInternalException())
+ .Build();
}
}
\ No newline at end of file
diff --git a/CR.Exceptions.Tests/Component/ExceptionTranslatorTests.cs b/CR.Exceptions.Tests/Component/ExceptionTranslatorTests.cs
new file mode 100644
index 0000000..b2a3ffc
--- /dev/null
+++ b/CR.Exceptions.Tests/Component/ExceptionTranslatorTests.cs
@@ -0,0 +1,56 @@
+using CR.Exceptions.Mapping;
+using CR.Exceptions.Tests.Shared;
+
+namespace CR.Exceptions.Tests.Component;
+
+public sealed class ExceptionTranslatorTests
+{
+ private static readonly TestInternalException ExistentException = new();
+ private static readonly TestUnknownException NonExistentException = new();
+
+ [Fact]
+ public void TryTranslate_ShouldReturn_TrueAndException_WhenExceptionExists()
+ {
+ var translator = GetDefaultTranslator();
+ var result = translator.TryTranslate(ExistentException, out var exception);
+
+ Assert.True(result);
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ }
+
+ [Fact]
+ public void TryTranslate_ShouldReturn_FalseAndNull_WhenExceptionDoesNotExist()
+ {
+ var translator = GetDefaultTranslator();
+ var result = translator.TryTranslate(NonExistentException, out var exception);
+
+ Assert.False(result);
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void Translate_ShouldReturn_Exception_WhenExceptionExists()
+ {
+ var translator = GetDefaultTranslator();
+ var exception = translator.Translate(ExistentException);
+
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ }
+
+ [Fact]
+ public void Translate_ShouldThrow_WhenExceptionDoesNotExist()
+ {
+ var translator = GetDefaultTranslator();
+
+ Assert.Throws(() => translator.Translate(NonExistentException));
+ }
+
+ private static ExceptionTranslator GetDefaultTranslator()
+ {
+ return new ExceptionTranslatorBuilder()
+ .Map(() => new TestUnknownException())
+ .Build();
+ }
+}
\ No newline at end of file
diff --git a/CR.Exceptions.Tests/TestException.cs b/CR.Exceptions.Tests/TestException.cs
deleted file mode 100644
index 0dbb7ce..0000000
--- a/CR.Exceptions.Tests/TestException.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System.Collections.Immutable;
-
-namespace CR.Exceptions.Tests;
-
-internal sealed class TestException : CrException
-{
- public TestException(ImmutableArray errors) : base(errors, "Test exception message")
- {
- }
-}
\ No newline at end of file
diff --git a/CR.Exceptions.Tests/Unit/ErrorMapBuilderTests.cs b/CR.Exceptions.Tests/Unit/ErrorMapBuilderTests.cs
deleted file mode 100644
index 5f0f647..0000000
--- a/CR.Exceptions.Tests/Unit/ErrorMapBuilderTests.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using CR.Exceptions.Mapping;
-
-namespace CR.Exceptions.Tests.Unit;
-
-public sealed class ErrorMapBuilderTests
-{
- [Fact]
- public void Add_ShouldThrow_WhenDuplicateRegistered()
- {
- var errorRegistration = new ErrorRegistration("duplicate", [new("code", "message")]);
-
- var builder = new ErrorMapBuilder()
- .Add(errorRegistration);
-
- Assert.ThrowsAny(() => builder.Add(errorRegistration));
- }
-}
\ No newline at end of file
diff --git a/CR.Exceptions.Tests/Unit/ExceptionFactoryBuilderTests.cs b/CR.Exceptions.Tests/Unit/ExceptionFactoryBuilderTests.cs
index fbc4b1d..fe13be2 100644
--- a/CR.Exceptions.Tests/Unit/ExceptionFactoryBuilderTests.cs
+++ b/CR.Exceptions.Tests/Unit/ExceptionFactoryBuilderTests.cs
@@ -1,18 +1,18 @@
using CR.Exceptions.Mapping;
+using CR.Exceptions.Tests.Shared;
namespace CR.Exceptions.Tests.Unit;
public sealed class ExceptionFactoryBuilderTests
{
[Fact]
- public void Add_ShouldThrow_WhenDuplicateRegistered()
+ public void Map_ShouldThrow_WhenDuplicateRegistered()
{
- var errorRegistration = new ErrorRegistration("duplicate", [new("code", "message")]);
- var exceptionRegistration = new ExceptionRegistration(errorRegistration, errors => new TestException(errors));
+ const string code = "duplicate";
var builder = new ExceptionFactoryBuilder()
- .Add(exceptionRegistration);
+ .Map(code, () => new TestUnknownException());
- Assert.ThrowsAny(() => builder.Add(exceptionRegistration));
+ Assert.ThrowsAny(() => builder.Map(code, () => new TestUnknownException()));
}
}
\ No newline at end of file
diff --git a/CR.Exceptions.Tests/Unit/ExceptionTranslatorBuilderTests.cs b/CR.Exceptions.Tests/Unit/ExceptionTranslatorBuilderTests.cs
new file mode 100644
index 0000000..0bfa369
--- /dev/null
+++ b/CR.Exceptions.Tests/Unit/ExceptionTranslatorBuilderTests.cs
@@ -0,0 +1,16 @@
+using CR.Exceptions.Mapping;
+using CR.Exceptions.Tests.Shared;
+
+namespace CR.Exceptions.Tests.Unit;
+
+public sealed class ExceptionTranslatorBuilderTests
+{
+ [Fact]
+ public void Map_ShouldThrow_WhenDuplicateRegistered()
+ {
+ var builder = new ExceptionTranslatorBuilder()
+ .Map(() => new TestUnknownException());
+
+ Assert.ThrowsAny(() => builder.Map(() => new TestUnknownException()));
+ }
+}
\ No newline at end of file
diff --git a/CR.Exceptions.slnx b/CR.Exceptions.slnx
index 34ade86..9bffe89 100644
--- a/CR.Exceptions.slnx
+++ b/CR.Exceptions.slnx
@@ -4,6 +4,7 @@
+
diff --git a/CR.Exceptions/CR.Exceptions.csproj b/CR.Exceptions/CR.Exceptions.csproj
index b40b4fb..5729cf5 100644
--- a/CR.Exceptions/CR.Exceptions.csproj
+++ b/CR.Exceptions/CR.Exceptions.csproj
@@ -4,13 +4,14 @@
net10.0
enable
enable
+ true
- Core .NET exception framework providing base error models, an exception factory, and error mapping.
+ Core library for advanced .NET error handling. Provides a unified exception set, an exception factory, and an exception translator.
$(NuGetPackagePrefix).Exceptions
- exceptions;exception-handling;exception-factory;error-handling;error-factory;error-mapping;
+ exception;exceptions;error-handling;exception-handling;exception-factory;exception-translator;exception-mapping;domain-exceptions;architecture;helpers
README.md
diff --git a/CR.Exceptions/Extensions/FuncExtensions.cs b/CR.Exceptions/Extensions/FuncExtensions.cs
new file mode 100644
index 0000000..12c1be1
--- /dev/null
+++ b/CR.Exceptions/Extensions/FuncExtensions.cs
@@ -0,0 +1,15 @@
+using System.Runtime.CompilerServices;
+
+namespace CR.Exceptions.Extensions;
+
+internal static class FuncExtensions
+{
+ extension(Func func)
+ {
+ public TResult ToResult([CallerArgumentExpression(nameof(func))] string? paramName = null)
+ {
+ ArgumentNullException.ThrowIfNull(func, paramName);
+ return func() ?? throw new NullReferenceException("delegate return null");
+ }
+ }
+}
\ No newline at end of file
diff --git a/CR.Exceptions/Extensions/ImmutableArrayExtensions.cs b/CR.Exceptions/Extensions/ImmutableArrayExtensions.cs
index 3fa7157..5b1a635 100644
--- a/CR.Exceptions/Extensions/ImmutableArrayExtensions.cs
+++ b/CR.Exceptions/Extensions/ImmutableArrayExtensions.cs
@@ -3,7 +3,7 @@
namespace CR.Exceptions.Extensions;
-public static class ImmutableArrayExtensions
+internal static class ImmutableArrayExtensions
{
extension(ImmutableArray source)
{
diff --git a/CR.Exceptions/Mapping/ErrorMap.cs b/CR.Exceptions/Mapping/ErrorMap.cs
deleted file mode 100644
index 35c65fb..0000000
--- a/CR.Exceptions/Mapping/ErrorMap.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Collections.Frozen;
-using System.Collections.Immutable;
-
-namespace CR.Exceptions.Mapping;
-
-public sealed class ErrorMap : Map
-{
- internal ErrorMap(FrozenDictionary dictionary) : base(dictionary) { }
-
- public ImmutableArray Get(string code)
- => GetValue(code).Errors;
-
- public bool TryGet(string code, out ImmutableArray errors)
- {
- if (TryGetValue(code, out var value))
- {
- errors = value.Errors;
- return true;
- }
-
- errors = [];
- return false;
- }
-}
\ No newline at end of file
diff --git a/CR.Exceptions/Mapping/ErrorMapBuilder.cs b/CR.Exceptions/Mapping/ErrorMapBuilder.cs
deleted file mode 100644
index 4f9b19f..0000000
--- a/CR.Exceptions/Mapping/ErrorMapBuilder.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace CR.Exceptions.Mapping;
-
-public sealed class ErrorMapBuilder : MapBuilder
-{
- public ErrorMapBuilder Add(ErrorRegistration registration)
- {
- Add(registration.Code, registration);
- return this;
- }
-
- public ErrorMapBuilder AddRange(IEnumerable registrations)
- {
- foreach (var registration in registrations) Add(registration);
- return this;
- }
-
- public ErrorMap Build()
- {
- return new(BuildFrozenDictionary(comparer: StringComparer.Ordinal));
- }
-}
\ No newline at end of file
diff --git a/CR.Exceptions/Mapping/ErrorRegistration.cs b/CR.Exceptions/Mapping/ErrorRegistration.cs
deleted file mode 100644
index bbbacf2..0000000
--- a/CR.Exceptions/Mapping/ErrorRegistration.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using CR.Exceptions.Extensions;
-using System.Collections.Immutable;
-
-namespace CR.Exceptions.Mapping;
-
-public record class ErrorRegistration
-{
- public string Code { get; init; }
- public ImmutableArray Errors { get; init; }
-
- public ErrorRegistration(string code, ImmutableArray errors)
- {
- ArgumentException.ThrowIfNullOrEmpty(code);
- errors.ThrowIfEmptyOrContainsNull();
-
- Code = code;
- Errors = errors;
- }
-}
\ No newline at end of file
diff --git a/CR.Exceptions/Mapping/ExceptionFactory.cs b/CR.Exceptions/Mapping/ExceptionFactory.cs
index c0e2c54..579016c 100644
--- a/CR.Exceptions/Mapping/ExceptionFactory.cs
+++ b/CR.Exceptions/Mapping/ExceptionFactory.cs
@@ -1,21 +1,16 @@
-using System.Collections.Frozen;
+using CR.Exceptions.Extensions;
+using System.Collections.Frozen;
using System.Diagnostics.CodeAnalysis;
namespace CR.Exceptions.Mapping;
-public sealed class ExceptionFactory : Map
+public class ExceptionFactory : Map>
{
- internal ExceptionFactory(FrozenDictionary dictionary) : base(dictionary) { }
+ internal ExceptionFactory(FrozenDictionary> dictionary) : base(dictionary) { }
public CrException Create(string code)
- => TransformValueToResult(GetValue(code));
+ => GetValue(code).ToResult();
public bool TryCreate(string code, [MaybeNullWhen(false)] out CrException exception)
- => (exception = TryGetValue(code, out var value) ? TransformValueToResult(value) : null) != null;
-
- private static CrException TransformValueToResult(ExceptionRegistration value)
- {
- return value.Factory(value.Definition.Errors)
- ?? throw new NullReferenceException("The registered factory return null exception");
- }
+ => (exception = TryGetValue(code, out var factory) ? factory.ToResult() : null) != null;
}
\ No newline at end of file
diff --git a/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs b/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs
index 511ff32..a85e9e2 100644
--- a/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs
+++ b/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs
@@ -1,21 +1,18 @@
namespace CR.Exceptions.Mapping;
-public sealed class ExceptionFactoryBuilder : MapBuilder
+public class ExceptionFactoryBuilder : MapBuilder>
{
- public ExceptionFactoryBuilder Add(ExceptionRegistration registration)
+ public ExceptionFactoryBuilder Map(string code, Func factory)
{
- Add(registration.Definition.Code, registration);
- return this;
- }
+ ThrowIfInvalidCode(code);
+ AddPair(code, factory);
- public ExceptionFactoryBuilder AddRange(IEnumerable registrations)
- {
- foreach (var registration in registrations) Add(registration);
return this;
}
public ExceptionFactory Build()
- {
- return new(BuildFrozenDictionary(comparer: StringComparer.Ordinal));
- }
+ => new(BuildFrozenDictionary(comparer: StringComparer.Ordinal));
+
+ private static void ThrowIfInvalidCode(string code)
+ => ArgumentException.ThrowIfNullOrEmpty(code);
}
\ No newline at end of file
diff --git a/CR.Exceptions/Mapping/ExceptionRegistration.cs b/CR.Exceptions/Mapping/ExceptionRegistration.cs
deleted file mode 100644
index eed2adf..0000000
--- a/CR.Exceptions/Mapping/ExceptionRegistration.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Collections.Immutable;
-
-namespace CR.Exceptions.Mapping;
-
-public record class ExceptionRegistration
-{
- public ErrorRegistration Definition { get; init; }
- public Func, CrException> Factory { get; init; }
-
- public ExceptionRegistration(ErrorRegistration definition, Func, CrException> factory)
- {
- ArgumentNullException.ThrowIfNull(definition);
- ArgumentNullException.ThrowIfNull(factory);
-
- Definition = definition;
- Factory = factory;
- }
-}
\ No newline at end of file
diff --git a/CR.Exceptions/Mapping/ExceptionTranslator.cs b/CR.Exceptions/Mapping/ExceptionTranslator.cs
new file mode 100644
index 0000000..62d399d
--- /dev/null
+++ b/CR.Exceptions/Mapping/ExceptionTranslator.cs
@@ -0,0 +1,16 @@
+using CR.Exceptions.Extensions;
+using System.Collections.Frozen;
+using System.Diagnostics.CodeAnalysis;
+
+namespace CR.Exceptions.Mapping;
+
+public class ExceptionTranslator : TypeMap>
+{
+ internal ExceptionTranslator(FrozenDictionary> dictionary) : base(dictionary) { }
+
+ public CrException Translate(CrException exception)
+ => GetByHierarchy(exception.GetType()).ToResult();
+
+ public bool TryTranslate(CrException exception, [MaybeNullWhen(false)] out CrException translated)
+ => (translated = TryGetByHierarchy(exception.GetType(), out var translator) ? translator.ToResult() : null) != null;
+}
\ No newline at end of file
diff --git a/CR.Exceptions/Mapping/ExceptionTranslatorBuilder.cs b/CR.Exceptions/Mapping/ExceptionTranslatorBuilder.cs
new file mode 100644
index 0000000..8e227f3
--- /dev/null
+++ b/CR.Exceptions/Mapping/ExceptionTranslatorBuilder.cs
@@ -0,0 +1,13 @@
+namespace CR.Exceptions.Mapping;
+
+public class ExceptionTranslatorBuilder : MapBuilder>
+{
+ public ExceptionTranslatorBuilder Map(Func translator) where TException : CrException
+ {
+ AddPair(typeof(TException), translator);
+ return this;
+ }
+
+ public ExceptionTranslator Build()
+ => new(BuildFrozenDictionary());
+}
\ No newline at end of file
diff --git a/CR.Exceptions/Mapping/Map.cs b/CR.Exceptions/Mapping/Map.cs
index b05d5c3..769c35a 100644
--- a/CR.Exceptions/Mapping/Map.cs
+++ b/CR.Exceptions/Mapping/Map.cs
@@ -15,8 +15,11 @@ protected Map(FrozenDictionary dictionary)
}
protected TValue GetValue(TKey key)
- => TryGetValue(key, out var value) ? value : throw new KeyNotFoundException($"Key '{key}' in map is not found.");
+ => TryGetValue(key, out var value) ? value : throw CreateKeyNotFoundException(key);
protected bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
=> _dictionary.TryGetValue(key, out value);
+
+ protected KeyNotFoundException CreateKeyNotFoundException(TKey key)
+ => new($"Key '{key?.ToString() ?? "null"}' in map is not found.");
}
\ No newline at end of file
diff --git a/CR.Exceptions/Mapping/MapBuilder.cs b/CR.Exceptions/Mapping/MapBuilder.cs
index 89bcbb1..b1f9558 100644
--- a/CR.Exceptions/Mapping/MapBuilder.cs
+++ b/CR.Exceptions/Mapping/MapBuilder.cs
@@ -6,7 +6,7 @@ public abstract class MapBuilder where TKey : notnull
{
private readonly Dictionary _map = [];
- protected void Add(TKey key, TValue value)
+ protected void AddPair(TKey key, TValue value)
{
ArgumentNullException.ThrowIfNull(key);
ArgumentNullException.ThrowIfNull(value);
diff --git a/CR.Exceptions.AspNet/Mapping/TypeMap.cs b/CR.Exceptions/Mapping/TypeMap.cs
similarity index 50%
rename from CR.Exceptions.AspNet/Mapping/TypeMap.cs
rename to CR.Exceptions/Mapping/TypeMap.cs
index 1aada42..b671b46 100644
--- a/CR.Exceptions.AspNet/Mapping/TypeMap.cs
+++ b/CR.Exceptions/Mapping/TypeMap.cs
@@ -1,18 +1,18 @@
-using CR.Exceptions.Mapping;
-using System.Collections.Frozen;
+using System.Collections.Frozen;
using System.Diagnostics.CodeAnalysis;
-namespace CR.Exceptions.AspNet.Mapping;
+namespace CR.Exceptions.Mapping;
public abstract class TypeMap : Map
{
protected TypeMap(FrozenDictionary dictionary) : base(dictionary) { }
- public bool TryFind(CrException exception, [MaybeNullWhen(false)] out TValue value)
- {
- ArgumentNullException.ThrowIfNull(exception);
+ protected TValue GetByHierarchy(Type? type)
+ => TryGetByHierarchy(type, out var value) ? value : throw CreateKeyNotFoundException(type);
- for (var type = exception.GetType(); type is not null; type = type.BaseType)
+ protected bool TryGetByHierarchy(Type? type, [MaybeNullWhen(false)] out TValue value)
+ {
+ for (; type is not null; type = type.BaseType)
{
if (TryGetValue(type, out value))
{
diff --git a/CR.Exceptions/README.md b/CR.Exceptions/README.md
index 410f78c..815bfa0 100644
--- a/CR.Exceptions/README.md
+++ b/CR.Exceptions/README.md
@@ -1,17 +1,17 @@
# Intro
-A lightweight library for defining application errors, creating typed exceptions, and mapping external error codes into domain-specific exceptions.
+A lightweight library for defining application errors, creating typed exceptions, and translating external exceptions into domain-specific exceptions.
-This package contains only the core exception model and does not depend on ASP.NET Core.
+This package contains only the core exception model and has no ASP.NET Core dependencies.
## Features
-- Typed application exceptions
-- Standard exception categories
-- Structured application errors (`CrError`)
-- External error mapping (`ErrorMap`)
-- Exception factory (`ExceptionFactory`)
-- No ASP.NET Core dependencies
+* Typed application exceptions
+* Standard exception categories
+* Structured application errors (`CrError`)
+* Error code → exception mapping (`ExceptionFactory`)
+* Exception → exception translation (`ExceptionTranslator`)
+* No ASP.NET Core dependencies
---
@@ -28,13 +28,15 @@ dotnet add package CrCore.Exceptions
Every application error is represented by `CrError`.
```csharp
-var error = new CrError("IdentityUserNotFound", "User was not found.");
+var error = new CrError(
+ "IdentityUserNotFound",
+ "User was not found.");
```
Each error contains:
-- `Code` — stable identifier for clients.
-- `Message` — human-readable description.
+* `Code` — stable identifier intended for clients.
+* `Message` — human-readable error description.
---
@@ -44,24 +46,28 @@ Applications should inherit from one of the predefined exception categories.
Available categories:
-| Exception | Purpose |
-|-----------|---------|
-| `ValidationException` | Validation failures |
-| `UnauthorizedException` | Authentication required |
-| `ForbiddenException` | Access denied |
-| `NotFoundException` | Resource not found |
-| `ConflictException` | Resource conflict |
+| Exception | Purpose |
+| ------------------------ | ----------------------- |
+| `ValidationException` | Validation failures |
+| `UnauthorizedException` | Authentication required |
+| `ForbiddenException` | Access denied |
+| `NotFoundException` | Resource not found |
+| `ConflictException` | Resource conflict |
| `UnprocessableException` | Business rule violation |
-| `InternalException` | Internal server error |
+| `InternalException` | Internal server error |
Example:
```csharp
public sealed class UserNotFoundException : NotFoundException
{
- public UserNotFoundException(Guid userId) : base(
- [new CrError("IdentityUserNotFound", $"User '{userId}' was not found.")],
- "User was not found.")
+ public UserNotFoundException()
+ : base(
+ [
+ new CrError(
+ "IdentityUserNotFound",
+ "User was not found.")
+ ])
{
}
}
@@ -70,64 +76,65 @@ public sealed class UserNotFoundException : NotFoundException
Usage:
```csharp
-throw new UserNotFoundException(userId);
+throw new UserNotFoundException();
```
---
-# ErrorMap
+# ExceptionFactory
-External systems usually expose their own error codes.
+External APIs often return string error codes.
For example:
```text
-user_not_found
+invalid_grant
```
-Those codes can be mapped into application errors.
+`ExceptionFactory` maps those codes to typed exceptions.
+
+Registration:
```csharp
-ErrorMap errorMap = builder
- .Add(new ErrorRegistration(
- "user_not_found",
- [new CrError("IdentityUserNotFound", "User was not found.")]))
+ExceptionFactory factory = new ExceptionFactoryBuilder()
+ .Map(
+ "invalid_grant",
+ static () => new InvalidCredentialsException())
.Build();
```
-Resolving an external error:
+Usage:
```csharp
-if (errorMap.TryGet("user_not_found", out var errors))
-{
- throw new UserNotFoundException(errors);
-}
+throw factory.Create("invalid_grant");
```
-This keeps external service contracts isolated from the application domain.
+This keeps external service contracts isolated from your application.
---
-# ExceptionFactory
+# ExceptionTranslator
+
+Infrastructure exceptions are often not suitable for the application layer.
-`ExceptionFactory` creates typed exceptions from registered external error codes.
+`ExceptionTranslator` converts one exception type into another.
Registration:
```csharp
-ExceptionFactory factory = builder
- .Add(new ExceptionRegistration(
- new ErrorRegistration(
- "invalid_grant",
- [new CrError("IdentityInvalidCredentials", "Invalid username or password.")]),
- errors => new InvalidCredentialsException(errors)))
+ExceptionTranslator translator = new ExceptionTranslatorBuilder()
+ .Map(
+ static () => new UserNotFoundException())
.Build();
```
Usage:
```csharp
-throw factory.Create("invalid_grant");
+catch (KeycloakUserNotFoundException ex)
+{
+ throw translator.Translate(ex);
+}
```
-The factory only creates exceptions for registered error codes.
\ No newline at end of file
+This allows infrastructure-specific exceptions to remain inside the infrastructure layer while exposing domain-specific exceptions to the rest of the application.
\ No newline at end of file
diff --git a/CR.Exceptions/ValidationException.cs b/CR.Exceptions/ValidationException.cs
index b071177..689336a 100644
--- a/CR.Exceptions/ValidationException.cs
+++ b/CR.Exceptions/ValidationException.cs
@@ -5,7 +5,7 @@ namespace CR.Exceptions;
public abstract class ValidationException : CrException
{
protected ValidationException(ImmutableArray errors, Exception? innerException = null)
- : base(errors, "One or more validation errors occurred.", innerException)
+ : base(errors, "The provided data is invalid. Check the specific errors list.", innerException)
{
}
diff --git a/README.md b/README.md
index acb840f..79efd4f 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ A lightweight framework for defining application errors, creating typed exceptio
### CR.Exceptions
-Core library for defining application errors, exception categories, error mapping, and exception creation.
+Core library for defining application errors, exception categories and their creation.
Documentation:
[CR.Exceptions README](./CR.Exceptions/README.md)
@@ -15,7 +15,7 @@ Documentation:
### CR.Exceptions.AspNet
-ASP.NET Core integration for handling application exceptions and converting them into RFC 7807 ProblemDetails responses.
+ASP.NET Core integration for handling exceptions and converting them into RFC 7807 ProblemDetails responses.
Documentation:
[CR.Exceptions.AspNet README](./CR.Exceptions.AspNet/README.md)
\ No newline at end of file