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