From 96085e082b0c3ed349cdaca9394f6f3ff6c3e143 Mon Sep 17 00:00:00 2001 From: apptade Date: Sun, 2 Aug 2026 22:39:52 +0300 Subject: [PATCH 01/14] Update base mapping --- CR.Exceptions.AspNet/Mapping/LogLevelMap.cs | 9 ++++--- .../Mapping/LogLevelMapBuilder.cs | 5 ++-- CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs | 9 ++++--- .../Mapping/StatusCodeMapBuilder.cs | 5 ++-- .../Mapping/TypeMapBuilder.cs | 16 ------------- CR.Exceptions/Mapping/ErrorMap.cs | 24 ------------------- CR.Exceptions/Mapping/ErrorMapBuilder.cs | 21 ---------------- CR.Exceptions/Mapping/ErrorRegistration.cs | 19 --------------- CR.Exceptions/Mapping/ExceptionFactory.cs | 13 +++++----- .../Mapping/ExceptionFactoryBuilder.cs | 18 ++++++-------- .../Mapping/ExceptionRegistration.cs | 18 -------------- CR.Exceptions/Mapping/ExceptionTranslator.cs | 20 ++++++++++++++++ .../Mapping/ExceptionTranslatorBuilder.cs | 17 +++++++++++++ CR.Exceptions/Mapping/Map.cs | 5 +++- CR.Exceptions/Mapping/MapBuilder.cs | 15 ++++++++++-- .../Mapping/TypeMap.cs | 14 +++++------ 16 files changed, 88 insertions(+), 140 deletions(-) delete mode 100644 CR.Exceptions.AspNet/Mapping/TypeMapBuilder.cs delete mode 100644 CR.Exceptions/Mapping/ErrorMap.cs delete mode 100644 CR.Exceptions/Mapping/ErrorMapBuilder.cs delete mode 100644 CR.Exceptions/Mapping/ErrorRegistration.cs delete mode 100644 CR.Exceptions/Mapping/ExceptionRegistration.cs create mode 100644 CR.Exceptions/Mapping/ExceptionTranslator.cs create mode 100644 CR.Exceptions/Mapping/ExceptionTranslatorBuilder.cs rename {CR.Exceptions.AspNet => CR.Exceptions}/Mapping/TypeMap.cs (52%) diff --git a/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs b/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs index a04555e..84ab128 100644 --- a/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs +++ b/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs @@ -1,11 +1,10 @@ -using Microsoft.Extensions.Logging; +using CR.Exceptions.Mapping; +using Microsoft.Extensions.Logging; using System.Collections.Frozen; 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) { } } \ No newline at end of file diff --git a/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs b/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs index f2319f4..d379fa4 100644 --- a/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs +++ b/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs @@ -1,8 +1,9 @@ -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 : TypeMapBuilder { public LogLevelMap Build() { diff --git a/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs b/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs index 3fe076a..06dfccb 100644 --- a/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs +++ b/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs @@ -1,10 +1,9 @@ -using System.Collections.Frozen; +using CR.Exceptions.Mapping; +using System.Collections.Frozen; 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) { } } \ No newline at end of file diff --git a/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilder.cs b/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilder.cs index 4ab0eca..a1729af 100644 --- a/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilder.cs +++ b/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilder.cs @@ -1,8 +1,9 @@ -using System.Net; +using CR.Exceptions.Mapping; +using System.Net; namespace CR.Exceptions.AspNet.Mapping; -public sealed class StatusCodeMapBuilder : TypeMapBuilder +public class StatusCodeMapBuilder : TypeMapBuilder { public StatusCodeMap Build() { 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/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..c826062 100644 --- a/CR.Exceptions/Mapping/ExceptionFactory.cs +++ b/CR.Exceptions/Mapping/ExceptionFactory.cs @@ -3,19 +3,18 @@ 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)); + => FactoryToException(GetValue(code)); public bool TryCreate(string code, [MaybeNullWhen(false)] out CrException exception) - => (exception = TryGetValue(code, out var value) ? TransformValueToResult(value) : null) != null; + => (exception = TryGetValue(code, out var value) ? FactoryToException(value) : null) != null; - private static CrException TransformValueToResult(ExceptionRegistration value) + private static CrException FactoryToException(Func factory) { - return value.Factory(value.Definition.Errors) - ?? throw new NullReferenceException("The registered factory return null exception"); + return factory() ?? throw new NullReferenceException($"The registered {nameof(factory)} - return null exception"); } } \ No newline at end of file diff --git a/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs b/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs index 511ff32..06e98dc 100644 --- a/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs +++ b/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs @@ -1,21 +1,17 @@ namespace CR.Exceptions.Mapping; -public sealed class ExceptionFactoryBuilder : MapBuilder +public class ExceptionFactoryBuilder : MapBuilder> { - public ExceptionFactoryBuilder Add(ExceptionRegistration registration) - { - Add(registration.Definition.Code, registration); - return this; - } + public ExceptionFactoryBuilder() : base() { } - public ExceptionFactoryBuilder AddRange(IEnumerable registrations) + public ExceptionFactoryBuilder(int startCapacity) : base(startCapacity) { } + + public ExceptionFactoryBuilder Map(string code, Func factory) { - foreach (var registration in registrations) Add(registration); + AddPair(code, factory); return this; } public ExceptionFactory Build() - { - return new(BuildFrozenDictionary(comparer: StringComparer.Ordinal)); - } + => new(BuildFrozenDictionary(comparer: StringComparer.Ordinal)); } \ 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..530b2b6 --- /dev/null +++ b/CR.Exceptions/Mapping/ExceptionTranslator.cs @@ -0,0 +1,20 @@ +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) + => TranslatorToException(Find(exception.GetType())); + + public bool TryTranslate(CrException exception, [MaybeNullWhen(false)] out CrException value) + => (value = TryFind(exception.GetType(), out var factory) ? TranslatorToException(factory) : null) != null; + + private static CrException TranslatorToException(Func translator) + { + return translator() ?? throw new NullReferenceException($"The registered {nameof(translator)} - return null exception"); + } +} \ 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..f627902 --- /dev/null +++ b/CR.Exceptions/Mapping/ExceptionTranslatorBuilder.cs @@ -0,0 +1,17 @@ +namespace CR.Exceptions.Mapping; + +public class ExceptionTranslatorBuilder : MapBuilder> +{ + public ExceptionTranslatorBuilder() : base() { } + + public ExceptionTranslatorBuilder(int startCapacity) : base(startCapacity) { } + + 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..78901a0 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) + => (key is null) ? new("Key is null and not found in map.") : new($"Key '{key}' 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..ad7b698 100644 --- a/CR.Exceptions/Mapping/MapBuilder.cs +++ b/CR.Exceptions/Mapping/MapBuilder.cs @@ -4,9 +4,20 @@ namespace CR.Exceptions.Mapping; public abstract class MapBuilder where TKey : notnull { - private readonly Dictionary _map = []; + private readonly Dictionary _map; - protected void Add(TKey key, TValue value) + protected MapBuilder() + { + _map = []; + } + + protected MapBuilder(int startCapacity) + { + ArgumentOutOfRangeException.ThrowIfNegative(startCapacity); + _map = new(capacity: startCapacity); + } + + 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 52% rename from CR.Exceptions.AspNet/Mapping/TypeMap.cs rename to CR.Exceptions/Mapping/TypeMap.cs index 1aada42..e552c4b 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 Find(Type? type) + => TryFind(type, out var value) ? value : throw CreateKeyNotFoundException(type!); - for (var type = exception.GetType(); type is not null; type = type.BaseType) + protected bool TryFind(Type? type, [MaybeNullWhen(false)] out TValue value) + { + for (; type is not null; type = type.BaseType) { if (TryGetValue(type, out value)) { From 5d0a9a8319bc427eb8ba75ff360427371651c86f Mon Sep 17 00:00:00 2001 From: apptade Date: Mon, 3 Aug 2026 12:56:14 +0300 Subject: [PATCH 02/14] Update base tests --- .../Component/ErrorMapTests.cs | 36 ------------ .../Component/ExceptionFactoryTests.cs | 54 +++++++++++------- .../Component/ExceptionTranslatorTests.cs | 55 +++++++++++++++++++ CR.Exceptions.Tests/TestException.cs | 10 ---- CR.Exceptions.Tests/TestInternalException.cs | 10 ++++ CR.Exceptions.Tests/TestUnknownException.cs | 10 ++++ .../Unit/ErrorMapBuilderTests.cs | 17 ------ .../Unit/ExceptionFactoryBuilderTests.cs | 9 ++- .../Unit/ExceptionTranslatorBuilderTests.cs | 15 +++++ CR.Exceptions/Mapping/ExceptionTranslator.cs | 4 +- 10 files changed, 130 insertions(+), 90 deletions(-) delete mode 100644 CR.Exceptions.Tests/Component/ErrorMapTests.cs create mode 100644 CR.Exceptions.Tests/Component/ExceptionTranslatorTests.cs delete mode 100644 CR.Exceptions.Tests/TestException.cs create mode 100644 CR.Exceptions.Tests/TestInternalException.cs create mode 100644 CR.Exceptions.Tests/TestUnknownException.cs delete mode 100644 CR.Exceptions.Tests/Unit/ErrorMapBuilderTests.cs create mode 100644 CR.Exceptions.Tests/Unit/ExceptionTranslatorBuilderTests.cs 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..915b083 100644 --- a/CR.Exceptions.Tests/Component/ExceptionFactoryTests.cs +++ b/CR.Exceptions.Tests/Component/ExceptionFactoryTests.cs @@ -4,38 +4,52 @@ 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..fd1454d --- /dev/null +++ b/CR.Exceptions.Tests/Component/ExceptionTranslatorTests.cs @@ -0,0 +1,55 @@ +using CR.Exceptions.Mapping; + +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/TestInternalException.cs b/CR.Exceptions.Tests/TestInternalException.cs new file mode 100644 index 0000000..9426bc1 --- /dev/null +++ b/CR.Exceptions.Tests/TestInternalException.cs @@ -0,0 +1,10 @@ +using System.Collections.Immutable; + +namespace CR.Exceptions.Tests; + +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/TestUnknownException.cs b/CR.Exceptions.Tests/TestUnknownException.cs new file mode 100644 index 0000000..be2384e --- /dev/null +++ b/CR.Exceptions.Tests/TestUnknownException.cs @@ -0,0 +1,10 @@ +using System.Collections.Immutable; + +namespace CR.Exceptions.Tests; + +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/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..5305b88 100644 --- a/CR.Exceptions.Tests/Unit/ExceptionFactoryBuilderTests.cs +++ b/CR.Exceptions.Tests/Unit/ExceptionFactoryBuilderTests.cs @@ -5,14 +5,13 @@ 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..532f61e --- /dev/null +++ b/CR.Exceptions.Tests/Unit/ExceptionTranslatorBuilderTests.cs @@ -0,0 +1,15 @@ +using CR.Exceptions.Mapping; + +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/Mapping/ExceptionTranslator.cs b/CR.Exceptions/Mapping/ExceptionTranslator.cs index 530b2b6..8ea19d3 100644 --- a/CR.Exceptions/Mapping/ExceptionTranslator.cs +++ b/CR.Exceptions/Mapping/ExceptionTranslator.cs @@ -10,8 +10,8 @@ internal ExceptionTranslator(FrozenDictionary> dictionar public CrException Translate(CrException exception) => TranslatorToException(Find(exception.GetType())); - public bool TryTranslate(CrException exception, [MaybeNullWhen(false)] out CrException value) - => (value = TryFind(exception.GetType(), out var factory) ? TranslatorToException(factory) : null) != null; + public bool TryTranslate(CrException exception, [MaybeNullWhen(false)] out CrException translated) + => (translated = TryFind(exception.GetType(), out var factory) ? TranslatorToException(factory) : null) != null; private static CrException TranslatorToException(Func translator) { From 4869ad29f1cb0818ffa4a41c9e39840cbddfd5e8 Mon Sep 17 00:00:00 2001 From: apptade Date: Mon, 3 Aug 2026 15:04:23 +0300 Subject: [PATCH 03/14] Update asp mapping --- CR.Exceptions.AspNet/Mapping/LogLevelMap.cs | 4 ++++ .../Mapping/LogLevelMapBuilder.cs | 22 ++++++++++++------- CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs | 4 ++++ .../Mapping/StatusCodeMapBuilder.cs | 18 ++++++++++----- .../Mapping/ExceptionFactoryBuilder.cs | 4 ---- CR.Exceptions/Mapping/ExceptionTranslator.cs | 4 ++-- .../Mapping/ExceptionTranslatorBuilder.cs | 4 ---- CR.Exceptions/Mapping/MapBuilder.cs | 6 ----- CR.Exceptions/Mapping/TypeMap.cs | 6 ++--- 9 files changed, 39 insertions(+), 33 deletions(-) diff --git a/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs b/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs index 84ab128..62c5bb8 100644 --- a/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs +++ b/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs @@ -1,10 +1,14 @@ using CR.Exceptions.Mapping; using Microsoft.Extensions.Logging; using System.Collections.Frozen; +using System.Diagnostics.CodeAnalysis; namespace CR.Exceptions.AspNet.Mapping; public class LogLevelMap : TypeMap { internal LogLevelMap(FrozenDictionary dictionary) : base(dictionary) { } + + public bool TryFind(CrException exception, [MaybeNullWhen(false)] out LogLevel level) + => TryFindValue(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 d379fa4..d1fcef6 100644 --- a/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs +++ b/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs @@ -3,25 +3,31 @@ namespace CR.Exceptions.AspNet.Mapping; -public 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/StatusCodeMap.cs b/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs index 06dfccb..c519fd8 100644 --- a/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs +++ b/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs @@ -1,9 +1,13 @@ using CR.Exceptions.Mapping; using System.Collections.Frozen; +using System.Diagnostics.CodeAnalysis; namespace CR.Exceptions.AspNet.Mapping; public class StatusCodeMap : TypeMap { internal StatusCodeMap(FrozenDictionary dictionary) : base(dictionary) { } + + public bool TryFind(CrException exception, [MaybeNullWhen(false)] out int code) + => TryFindValue(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 a1729af..8db409e 100644 --- a/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilder.cs +++ b/CR.Exceptions.AspNet/Mapping/StatusCodeMapBuilder.cs @@ -3,19 +3,25 @@ namespace CR.Exceptions.AspNet.Mapping; -public 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/Mapping/ExceptionFactoryBuilder.cs b/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs index 06e98dc..0ec3bff 100644 --- a/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs +++ b/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs @@ -2,10 +2,6 @@ public class ExceptionFactoryBuilder : MapBuilder> { - public ExceptionFactoryBuilder() : base() { } - - public ExceptionFactoryBuilder(int startCapacity) : base(startCapacity) { } - public ExceptionFactoryBuilder Map(string code, Func factory) { AddPair(code, factory); diff --git a/CR.Exceptions/Mapping/ExceptionTranslator.cs b/CR.Exceptions/Mapping/ExceptionTranslator.cs index 8ea19d3..9075020 100644 --- a/CR.Exceptions/Mapping/ExceptionTranslator.cs +++ b/CR.Exceptions/Mapping/ExceptionTranslator.cs @@ -8,10 +8,10 @@ public class ExceptionTranslator : TypeMap> internal ExceptionTranslator(FrozenDictionary> dictionary) : base(dictionary) { } public CrException Translate(CrException exception) - => TranslatorToException(Find(exception.GetType())); + => TranslatorToException(FindValue(exception.GetType())); public bool TryTranslate(CrException exception, [MaybeNullWhen(false)] out CrException translated) - => (translated = TryFind(exception.GetType(), out var factory) ? TranslatorToException(factory) : null) != null; + => (translated = TryFindValue(exception.GetType(), out var factory) ? TranslatorToException(factory) : null) != null; private static CrException TranslatorToException(Func translator) { diff --git a/CR.Exceptions/Mapping/ExceptionTranslatorBuilder.cs b/CR.Exceptions/Mapping/ExceptionTranslatorBuilder.cs index f627902..8e227f3 100644 --- a/CR.Exceptions/Mapping/ExceptionTranslatorBuilder.cs +++ b/CR.Exceptions/Mapping/ExceptionTranslatorBuilder.cs @@ -2,10 +2,6 @@ public class ExceptionTranslatorBuilder : MapBuilder> { - public ExceptionTranslatorBuilder() : base() { } - - public ExceptionTranslatorBuilder(int startCapacity) : base(startCapacity) { } - public ExceptionTranslatorBuilder Map(Func translator) where TException : CrException { AddPair(typeof(TException), translator); diff --git a/CR.Exceptions/Mapping/MapBuilder.cs b/CR.Exceptions/Mapping/MapBuilder.cs index ad7b698..d60e315 100644 --- a/CR.Exceptions/Mapping/MapBuilder.cs +++ b/CR.Exceptions/Mapping/MapBuilder.cs @@ -11,12 +11,6 @@ protected MapBuilder() _map = []; } - protected MapBuilder(int startCapacity) - { - ArgumentOutOfRangeException.ThrowIfNegative(startCapacity); - _map = new(capacity: startCapacity); - } - protected void AddPair(TKey key, TValue value) { ArgumentNullException.ThrowIfNull(key); diff --git a/CR.Exceptions/Mapping/TypeMap.cs b/CR.Exceptions/Mapping/TypeMap.cs index e552c4b..4da53ee 100644 --- a/CR.Exceptions/Mapping/TypeMap.cs +++ b/CR.Exceptions/Mapping/TypeMap.cs @@ -7,10 +7,10 @@ public abstract class TypeMap : Map { protected TypeMap(FrozenDictionary dictionary) : base(dictionary) { } - protected TValue Find(Type? type) - => TryFind(type, out var value) ? value : throw CreateKeyNotFoundException(type!); + protected TValue FindValue(Type? type) + => TryFindValue(type, out var value) ? value : throw CreateKeyNotFoundException(type!); - protected bool TryFind(Type? type, [MaybeNullWhen(false)] out TValue value) + protected bool TryFindValue(Type? type, [MaybeNullWhen(false)] out TValue value) { for (; type is not null; type = type.BaseType) { From 5a13af5711a5c6152fa017f621ea10f87e41928c Mon Sep 17 00:00:00 2001 From: apptade Date: Mon, 3 Aug 2026 15:18:21 +0300 Subject: [PATCH 04/14] Update LogLevelMapBuilderExtensions.cs --- .../Mapping/LogLevelMapBuilderExtensions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilderExtensions.cs b/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilderExtensions.cs index e40716d..2522015 100644 --- a/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilderExtensions.cs +++ b/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilderExtensions.cs @@ -9,6 +9,12 @@ public static class LogLevelMapBuilderExtensions public LogLevelMapBuilder AddDefaultMappings() { builder + .Map(LogLevel.Debug) + .Map(LogLevel.Debug) + .Map(LogLevel.Debug) + .Map(LogLevel.Debug) + .Map(LogLevel.Debug) + .Map(LogLevel.Debug) .Map(LogLevel.Error); return builder; From 540c8361ed6d04d0111898b40c7fc51e9fdb9a20 Mon Sep 17 00:00:00 2001 From: apptade Date: Mon, 3 Aug 2026 15:18:41 +0300 Subject: [PATCH 05/14] Update CrExceptionHandlerLogExtensions.cs --- CR.Exceptions.AspNet/CrExceptionHandlerLogExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 01ba063f91041ff075c89d28304b5cebd3df3256 Mon Sep 17 00:00:00 2001 From: apptade Date: Mon, 3 Aug 2026 15:21:19 +0300 Subject: [PATCH 06/14] Update base mapping --- CR.Exceptions.AspNet/Mapping/LogLevelMap.cs | 2 +- CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs | 2 +- CR.Exceptions/Mapping/ExceptionTranslator.cs | 4 ++-- CR.Exceptions/Mapping/TypeMap.cs | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs b/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs index 62c5bb8..555b9c1 100644 --- a/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs +++ b/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs @@ -10,5 +10,5 @@ public class LogLevelMap : TypeMap internal LogLevelMap(FrozenDictionary dictionary) : base(dictionary) { } public bool TryFind(CrException exception, [MaybeNullWhen(false)] out LogLevel level) - => TryFindValue(exception.GetType(), out level); + => TrySearchValue(exception.GetType(), out level); } \ No newline at end of file diff --git a/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs b/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs index c519fd8..6eedca6 100644 --- a/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs +++ b/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs @@ -9,5 +9,5 @@ public class StatusCodeMap : TypeMap internal StatusCodeMap(FrozenDictionary dictionary) : base(dictionary) { } public bool TryFind(CrException exception, [MaybeNullWhen(false)] out int code) - => TryFindValue(exception.GetType(), out code); + => TrySearchValue(exception.GetType(), out code); } \ No newline at end of file diff --git a/CR.Exceptions/Mapping/ExceptionTranslator.cs b/CR.Exceptions/Mapping/ExceptionTranslator.cs index 9075020..f339968 100644 --- a/CR.Exceptions/Mapping/ExceptionTranslator.cs +++ b/CR.Exceptions/Mapping/ExceptionTranslator.cs @@ -8,10 +8,10 @@ public class ExceptionTranslator : TypeMap> internal ExceptionTranslator(FrozenDictionary> dictionary) : base(dictionary) { } public CrException Translate(CrException exception) - => TranslatorToException(FindValue(exception.GetType())); + => TranslatorToException(SearchValue(exception.GetType())); public bool TryTranslate(CrException exception, [MaybeNullWhen(false)] out CrException translated) - => (translated = TryFindValue(exception.GetType(), out var factory) ? TranslatorToException(factory) : null) != null; + => (translated = TrySearchValue(exception.GetType(), out var factory) ? TranslatorToException(factory) : null) != null; private static CrException TranslatorToException(Func translator) { diff --git a/CR.Exceptions/Mapping/TypeMap.cs b/CR.Exceptions/Mapping/TypeMap.cs index 4da53ee..211d999 100644 --- a/CR.Exceptions/Mapping/TypeMap.cs +++ b/CR.Exceptions/Mapping/TypeMap.cs @@ -7,10 +7,10 @@ public abstract class TypeMap : Map { protected TypeMap(FrozenDictionary dictionary) : base(dictionary) { } - protected TValue FindValue(Type? type) - => TryFindValue(type, out var value) ? value : throw CreateKeyNotFoundException(type!); + protected TValue SearchValue(Type? type) + => TrySearchValue(type, out var value) ? value : throw CreateKeyNotFoundException(type!); - protected bool TryFindValue(Type? type, [MaybeNullWhen(false)] out TValue value) + protected bool TrySearchValue(Type? type, [MaybeNullWhen(false)] out TValue value) { for (; type is not null; type = type.BaseType) { From fdc326910e4fdd7314b91e3d80b8307ca2385594 Mon Sep 17 00:00:00 2001 From: apptade Date: Mon, 3 Aug 2026 15:26:35 +0300 Subject: [PATCH 07/14] Update asp tests --- .../CR.Exceptions.AspNet.Tests.csproj | 1 + .../Component/CrExceptionHandlerTests.cs | 11 ++++--- .../Component/LogLevelMapTests.cs | 31 ++++++++++--------- .../Component/StatusCodeMapTests.cs | 31 ++++++++++--------- .../TestNotFoundException.cs | 8 ----- .../TestUnregisteredException.cs | 8 ----- 6 files changed, 41 insertions(+), 49 deletions(-) delete mode 100644 CR.Exceptions.AspNet.Tests/TestNotFoundException.cs delete mode 100644 CR.Exceptions.AspNet.Tests/TestUnregisteredException.cs diff --git a/CR.Exceptions.AspNet.Tests/CR.Exceptions.AspNet.Tests.csproj b/CR.Exceptions.AspNet.Tests/CR.Exceptions.AspNet.Tests.csproj index 9d00fd5..9f35d36 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..f3b3270 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; +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(), diff --git a/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs b/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs index 262a621..bd46092 100644 --- a/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs +++ b/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs @@ -1,35 +1,38 @@ using CR.Exceptions.AspNet.Mapping; +using CR.Exceptions.Tests; 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_TrueAndLogLevel_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 _); - Assert.False(map.TryFind(new TestUnregisteredException(), out var _)); + Assert.False(result); } - 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..9fe5f37 100644 --- a/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs +++ b/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs @@ -1,35 +1,38 @@ using CR.Exceptions.AspNet.Mapping; +using CR.Exceptions.Tests; 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_TrueAndStatusCode_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 _); - Assert.False(map.TryFind(new TestUnregisteredException(), out var _)); + Assert.False(result); } - 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 From f84c7969bbdb81a2609bd100aca4badb4056e4d3 Mon Sep 17 00:00:00 2001 From: apptade Date: Mon, 3 Aug 2026 15:35:34 +0300 Subject: [PATCH 08/14] Update tests --- .../CR.Exceptions.AspNet.Tests.csproj | 2 +- .../Component/CrExceptionHandlerTests.cs | 2 +- .../Component/LogLevelMapTests.cs | 2 +- .../Component/StatusCodeMapTests.cs | 2 +- .../CR.Exceptions.Tests.Shared.csproj | 14 ++++++++++++++ .../TestInternalException.cs | 2 +- .../TestUnknownException.cs | 2 +- CR.Exceptions.Tests/CR.Exceptions.Tests.csproj | 2 +- .../Component/ExceptionFactoryTests.cs | 1 + .../Component/ExceptionTranslatorTests.cs | 1 + .../Unit/ExceptionFactoryBuilderTests.cs | 1 + .../Unit/ExceptionTranslatorBuilderTests.cs | 1 + CR.Exceptions.slnx | 1 + 13 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 CR.Exceptions.Tests.Shared/CR.Exceptions.Tests.Shared.csproj rename {CR.Exceptions.Tests => CR.Exceptions.Tests.Shared}/TestInternalException.cs (87%) rename {CR.Exceptions.Tests => CR.Exceptions.Tests.Shared}/TestUnknownException.cs (88%) diff --git a/CR.Exceptions.AspNet.Tests/CR.Exceptions.AspNet.Tests.csproj b/CR.Exceptions.AspNet.Tests/CR.Exceptions.AspNet.Tests.csproj index 9f35d36..0d197aa 100644 --- a/CR.Exceptions.AspNet.Tests/CR.Exceptions.AspNet.Tests.csproj +++ b/CR.Exceptions.AspNet.Tests/CR.Exceptions.AspNet.Tests.csproj @@ -22,7 +22,7 @@ - + diff --git a/CR.Exceptions.AspNet.Tests/Component/CrExceptionHandlerTests.cs b/CR.Exceptions.AspNet.Tests/Component/CrExceptionHandlerTests.cs index f3b3270..bed49c0 100644 --- a/CR.Exceptions.AspNet.Tests/Component/CrExceptionHandlerTests.cs +++ b/CR.Exceptions.AspNet.Tests/Component/CrExceptionHandlerTests.cs @@ -1,4 +1,4 @@ -using CR.Exceptions.Tests; +using CR.Exceptions.Tests.Shared; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; diff --git a/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs b/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs index bd46092..fdf6452 100644 --- a/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs +++ b/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs @@ -1,5 +1,5 @@ using CR.Exceptions.AspNet.Mapping; -using CR.Exceptions.Tests; +using CR.Exceptions.Tests.Shared; using Microsoft.Extensions.Logging; namespace CR.Exceptions.AspNet.Tests.Component; diff --git a/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs b/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs index 9fe5f37..cee081f 100644 --- a/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs +++ b/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs @@ -1,5 +1,5 @@ using CR.Exceptions.AspNet.Mapping; -using CR.Exceptions.Tests; +using CR.Exceptions.Tests.Shared; using Microsoft.AspNetCore.Http; namespace CR.Exceptions.AspNet.Tests.Component; 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/TestInternalException.cs b/CR.Exceptions.Tests.Shared/TestInternalException.cs similarity index 87% rename from CR.Exceptions.Tests/TestInternalException.cs rename to CR.Exceptions.Tests.Shared/TestInternalException.cs index 9426bc1..3836a57 100644 --- a/CR.Exceptions.Tests/TestInternalException.cs +++ b/CR.Exceptions.Tests.Shared/TestInternalException.cs @@ -1,6 +1,6 @@ using System.Collections.Immutable; -namespace CR.Exceptions.Tests; +namespace CR.Exceptions.Tests.Shared; public sealed class TestInternalException : InternalException { diff --git a/CR.Exceptions.Tests/TestUnknownException.cs b/CR.Exceptions.Tests.Shared/TestUnknownException.cs similarity index 88% rename from CR.Exceptions.Tests/TestUnknownException.cs rename to CR.Exceptions.Tests.Shared/TestUnknownException.cs index be2384e..e7d5a29 100644 --- a/CR.Exceptions.Tests/TestUnknownException.cs +++ b/CR.Exceptions.Tests.Shared/TestUnknownException.cs @@ -1,6 +1,6 @@ using System.Collections.Immutable; -namespace CR.Exceptions.Tests; +namespace CR.Exceptions.Tests.Shared; public sealed class TestUnknownException : CrException { 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/ExceptionFactoryTests.cs b/CR.Exceptions.Tests/Component/ExceptionFactoryTests.cs index 915b083..0790f52 100644 --- a/CR.Exceptions.Tests/Component/ExceptionFactoryTests.cs +++ b/CR.Exceptions.Tests/Component/ExceptionFactoryTests.cs @@ -1,4 +1,5 @@ using CR.Exceptions.Mapping; +using CR.Exceptions.Tests.Shared; namespace CR.Exceptions.Tests.Component; diff --git a/CR.Exceptions.Tests/Component/ExceptionTranslatorTests.cs b/CR.Exceptions.Tests/Component/ExceptionTranslatorTests.cs index fd1454d..b2a3ffc 100644 --- a/CR.Exceptions.Tests/Component/ExceptionTranslatorTests.cs +++ b/CR.Exceptions.Tests/Component/ExceptionTranslatorTests.cs @@ -1,4 +1,5 @@ using CR.Exceptions.Mapping; +using CR.Exceptions.Tests.Shared; namespace CR.Exceptions.Tests.Component; diff --git a/CR.Exceptions.Tests/Unit/ExceptionFactoryBuilderTests.cs b/CR.Exceptions.Tests/Unit/ExceptionFactoryBuilderTests.cs index 5305b88..fe13be2 100644 --- a/CR.Exceptions.Tests/Unit/ExceptionFactoryBuilderTests.cs +++ b/CR.Exceptions.Tests/Unit/ExceptionFactoryBuilderTests.cs @@ -1,4 +1,5 @@ using CR.Exceptions.Mapping; +using CR.Exceptions.Tests.Shared; namespace CR.Exceptions.Tests.Unit; diff --git a/CR.Exceptions.Tests/Unit/ExceptionTranslatorBuilderTests.cs b/CR.Exceptions.Tests/Unit/ExceptionTranslatorBuilderTests.cs index 532f61e..0bfa369 100644 --- a/CR.Exceptions.Tests/Unit/ExceptionTranslatorBuilderTests.cs +++ b/CR.Exceptions.Tests/Unit/ExceptionTranslatorBuilderTests.cs @@ -1,4 +1,5 @@ using CR.Exceptions.Mapping; +using CR.Exceptions.Tests.Shared; namespace CR.Exceptions.Tests.Unit; 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 @@ + From 83fe2a3523fbac11e7a834463ba4dc3e043f4af4 Mon Sep 17 00:00:00 2001 From: apptade Date: Mon, 3 Aug 2026 15:41:51 +0300 Subject: [PATCH 09/14] Update asp extensions --- CR.Exceptions.AspNet.Tests/Component/CrExceptionHandlerTests.cs | 2 +- CR.Exceptions.AspNet/ServiceCollectionExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CR.Exceptions.AspNet.Tests/Component/CrExceptionHandlerTests.cs b/CR.Exceptions.AspNet.Tests/Component/CrExceptionHandlerTests.cs index bed49c0..716a7b0 100644 --- a/CR.Exceptions.AspNet.Tests/Component/CrExceptionHandlerTests.cs +++ b/CR.Exceptions.AspNet.Tests/Component/CrExceptionHandlerTests.cs @@ -99,7 +99,7 @@ private static ServiceProvider CreateServiceProvider() { return new ServiceCollection() .AddLogging() - .AddCrExceptions() + .AddCrExceptionsCore() .BuildServiceProvider(); } 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() From f7d61b126525e0dcc91479e06160c86ae9a7fc30 Mon Sep 17 00:00:00 2001 From: apptade Date: Mon, 3 Aug 2026 15:50:26 +0300 Subject: [PATCH 10/14] Update base extensions --- CR.Exceptions/Extensions/FuncExtensions.cs | 16 ++++++++++++++++ .../Extensions/ImmutableArrayExtensions.cs | 2 +- CR.Exceptions/Mapping/ExceptionFactory.cs | 12 ++++-------- CR.Exceptions/Mapping/ExceptionTranslator.cs | 12 ++++-------- 4 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 CR.Exceptions/Extensions/FuncExtensions.cs diff --git a/CR.Exceptions/Extensions/FuncExtensions.cs b/CR.Exceptions/Extensions/FuncExtensions.cs new file mode 100644 index 0000000..ba9d014 --- /dev/null +++ b/CR.Exceptions/Extensions/FuncExtensions.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +namespace CR.Exceptions.Extensions; + +internal static class FuncExtensions +{ + extension(Func func) where TResult : allows ref struct + { + public TResult ToResult([CallerArgumentExpression(nameof(func))] string? paramName = null) + { + ArgumentNullException.ThrowIfNull(func, paramName); + + return func() ?? throw new ArgumentNullException(paramName, "Func 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/ExceptionFactory.cs b/CR.Exceptions/Mapping/ExceptionFactory.cs index c826062..579016c 100644 --- a/CR.Exceptions/Mapping/ExceptionFactory.cs +++ b/CR.Exceptions/Mapping/ExceptionFactory.cs @@ -1,4 +1,5 @@ -using System.Collections.Frozen; +using CR.Exceptions.Extensions; +using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; namespace CR.Exceptions.Mapping; @@ -8,13 +9,8 @@ public class ExceptionFactory : Map> internal ExceptionFactory(FrozenDictionary> dictionary) : base(dictionary) { } public CrException Create(string code) - => FactoryToException(GetValue(code)); + => GetValue(code).ToResult(); public bool TryCreate(string code, [MaybeNullWhen(false)] out CrException exception) - => (exception = TryGetValue(code, out var value) ? FactoryToException(value) : null) != null; - - private static CrException FactoryToException(Func factory) - { - return factory() ?? throw new NullReferenceException($"The registered {nameof(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/ExceptionTranslator.cs b/CR.Exceptions/Mapping/ExceptionTranslator.cs index f339968..3a3ddac 100644 --- a/CR.Exceptions/Mapping/ExceptionTranslator.cs +++ b/CR.Exceptions/Mapping/ExceptionTranslator.cs @@ -1,4 +1,5 @@ -using System.Collections.Frozen; +using CR.Exceptions.Extensions; +using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; namespace CR.Exceptions.Mapping; @@ -8,13 +9,8 @@ public class ExceptionTranslator : TypeMap> internal ExceptionTranslator(FrozenDictionary> dictionary) : base(dictionary) { } public CrException Translate(CrException exception) - => TranslatorToException(SearchValue(exception.GetType())); + => SearchValue(exception.GetType()).ToResult(); public bool TryTranslate(CrException exception, [MaybeNullWhen(false)] out CrException translated) - => (translated = TrySearchValue(exception.GetType(), out var factory) ? TranslatorToException(factory) : null) != null; - - private static CrException TranslatorToException(Func translator) - { - return translator() ?? throw new NullReferenceException($"The registered {nameof(translator)} - return null exception"); - } + => (translated = TrySearchValue(exception.GetType(), out var translator) ? translator.ToResult() : null) != null; } \ No newline at end of file From 02ddc1026506fcd348d74a37030d88231db29762 Mon Sep 17 00:00:00 2001 From: apptade Date: Mon, 3 Aug 2026 19:28:01 +0300 Subject: [PATCH 11/14] Update base --- CR.Exceptions.AspNet/Mapping/LogLevelMap.cs | 2 +- CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs | 2 +- CR.Exceptions/Extensions/FuncExtensions.cs | 5 ++--- CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs | 5 +++++ CR.Exceptions/Mapping/ExceptionTranslator.cs | 4 ++-- CR.Exceptions/Mapping/Map.cs | 2 +- CR.Exceptions/Mapping/MapBuilder.cs | 7 +------ CR.Exceptions/Mapping/TypeMap.cs | 6 +++--- CR.Exceptions/ValidationException.cs | 2 +- 9 files changed, 17 insertions(+), 18 deletions(-) diff --git a/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs b/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs index 555b9c1..1ef880e 100644 --- a/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs +++ b/CR.Exceptions.AspNet/Mapping/LogLevelMap.cs @@ -10,5 +10,5 @@ public class LogLevelMap : TypeMap internal LogLevelMap(FrozenDictionary dictionary) : base(dictionary) { } public bool TryFind(CrException exception, [MaybeNullWhen(false)] out LogLevel level) - => TrySearchValue(exception.GetType(), out level); + => TryGetByHierarchy(exception.GetType(), out level); } \ No newline at end of file diff --git a/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs b/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs index 6eedca6..50c85bc 100644 --- a/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs +++ b/CR.Exceptions.AspNet/Mapping/StatusCodeMap.cs @@ -9,5 +9,5 @@ public class StatusCodeMap : TypeMap internal StatusCodeMap(FrozenDictionary dictionary) : base(dictionary) { } public bool TryFind(CrException exception, [MaybeNullWhen(false)] out int code) - => TrySearchValue(exception.GetType(), out code); + => TryGetByHierarchy(exception.GetType(), out code); } \ No newline at end of file diff --git a/CR.Exceptions/Extensions/FuncExtensions.cs b/CR.Exceptions/Extensions/FuncExtensions.cs index ba9d014..12c1be1 100644 --- a/CR.Exceptions/Extensions/FuncExtensions.cs +++ b/CR.Exceptions/Extensions/FuncExtensions.cs @@ -4,13 +4,12 @@ namespace CR.Exceptions.Extensions; internal static class FuncExtensions { - extension(Func func) where TResult : allows ref struct + extension(Func func) { public TResult ToResult([CallerArgumentExpression(nameof(func))] string? paramName = null) { ArgumentNullException.ThrowIfNull(func, paramName); - - return func() ?? throw new ArgumentNullException(paramName, "Func return null"); + return func() ?? throw new NullReferenceException("delegate return null"); } } } \ No newline at end of file diff --git a/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs b/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs index 0ec3bff..a85e9e2 100644 --- a/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs +++ b/CR.Exceptions/Mapping/ExceptionFactoryBuilder.cs @@ -4,10 +4,15 @@ public class ExceptionFactoryBuilder : MapBuilder> { public ExceptionFactoryBuilder Map(string code, Func factory) { + ThrowIfInvalidCode(code); AddPair(code, factory); + return this; } public ExceptionFactory Build() => 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/ExceptionTranslator.cs b/CR.Exceptions/Mapping/ExceptionTranslator.cs index 3a3ddac..62d399d 100644 --- a/CR.Exceptions/Mapping/ExceptionTranslator.cs +++ b/CR.Exceptions/Mapping/ExceptionTranslator.cs @@ -9,8 +9,8 @@ public class ExceptionTranslator : TypeMap> internal ExceptionTranslator(FrozenDictionary> dictionary) : base(dictionary) { } public CrException Translate(CrException exception) - => SearchValue(exception.GetType()).ToResult(); + => GetByHierarchy(exception.GetType()).ToResult(); public bool TryTranslate(CrException exception, [MaybeNullWhen(false)] out CrException translated) - => (translated = TrySearchValue(exception.GetType(), out var translator) ? translator.ToResult() : null) != null; + => (translated = TryGetByHierarchy(exception.GetType(), out var translator) ? translator.ToResult() : null) != null; } \ No newline at end of file diff --git a/CR.Exceptions/Mapping/Map.cs b/CR.Exceptions/Mapping/Map.cs index 78901a0..769c35a 100644 --- a/CR.Exceptions/Mapping/Map.cs +++ b/CR.Exceptions/Mapping/Map.cs @@ -21,5 +21,5 @@ protected bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => _dictionary.TryGetValue(key, out value); protected KeyNotFoundException CreateKeyNotFoundException(TKey key) - => (key is null) ? new("Key is null and not found in map.") : new($"Key '{key}' in map is not found."); + => 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 d60e315..b1f9558 100644 --- a/CR.Exceptions/Mapping/MapBuilder.cs +++ b/CR.Exceptions/Mapping/MapBuilder.cs @@ -4,12 +4,7 @@ namespace CR.Exceptions.Mapping; public abstract class MapBuilder where TKey : notnull { - private readonly Dictionary _map; - - protected MapBuilder() - { - _map = []; - } + private readonly Dictionary _map = []; protected void AddPair(TKey key, TValue value) { diff --git a/CR.Exceptions/Mapping/TypeMap.cs b/CR.Exceptions/Mapping/TypeMap.cs index 211d999..b671b46 100644 --- a/CR.Exceptions/Mapping/TypeMap.cs +++ b/CR.Exceptions/Mapping/TypeMap.cs @@ -7,10 +7,10 @@ public abstract class TypeMap : Map { protected TypeMap(FrozenDictionary dictionary) : base(dictionary) { } - protected TValue SearchValue(Type? type) - => TrySearchValue(type, out var value) ? value : throw CreateKeyNotFoundException(type!); + protected TValue GetByHierarchy(Type? type) + => TryGetByHierarchy(type, out var value) ? value : throw CreateKeyNotFoundException(type); - protected bool TrySearchValue(Type? type, [MaybeNullWhen(false)] out TValue value) + protected bool TryGetByHierarchy(Type? type, [MaybeNullWhen(false)] out TValue value) { for (; type is not null; type = type.BaseType) { 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) { } From b2f047e5d1f6624e59e4932520cb87c36ef2cd2d Mon Sep 17 00:00:00 2001 From: apptade Date: Mon, 3 Aug 2026 20:05:43 +0300 Subject: [PATCH 12/14] Update CR.Exceptions.csproj --- CR.Exceptions/CR.Exceptions.csproj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 From ae8864f9afaac09ee801dbf41c8e5a5e1ac8b6ec Mon Sep 17 00:00:00 2001 From: apptade Date: Mon, 3 Aug 2026 20:37:56 +0300 Subject: [PATCH 13/14] Update asp base --- CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs | 5 +++-- CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs | 5 +++-- CR.Exceptions.AspNet/CR.Exceptions.AspNet.csproj | 1 + CR.Exceptions.AspNet/CrExceptionHandler.cs | 4 ++-- CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs | 2 +- CR.Exceptions.AspNet/Mapping/LogLevelMapBuilderExtensions.cs | 4 +--- .../Mapping/StatusCodeMapBuilderExtensions.cs | 4 +--- 7 files changed, 12 insertions(+), 13 deletions(-) diff --git a/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs b/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs index fdf6452..80d9d0e 100644 --- a/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs +++ b/CR.Exceptions.AspNet.Tests/Component/LogLevelMapTests.cs @@ -11,7 +11,7 @@ public sealed class LogLevelMapTests private static readonly TestUnknownException NonExistentException = new(); [Fact] - public void TryFind_ShouldReturn_TrueAndLogLevel_WhenExceptionExists() + public void TryFind_ShouldReturn_TrueAndLevel_WhenExceptionExists() { var map = GetDefaultMap(); var result = map.TryFind(ExistentException, out var actualLevel); @@ -24,9 +24,10 @@ public void TryFind_ShouldReturn_TrueAndLogLevel_WhenExceptionExists() public void TryFind_ShouldReturn_FalseAndDefault_WhenExceptionDoesNotExist() { var map = GetDefaultMap(); - var result = map.TryFind(NonExistentException, out var _); + var result = map.TryFind(NonExistentException, out var level); Assert.False(result); + Assert.Equal(default, level); } private static LogLevelMap GetDefaultMap() diff --git a/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs b/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs index cee081f..f2b0e92 100644 --- a/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs +++ b/CR.Exceptions.AspNet.Tests/Component/StatusCodeMapTests.cs @@ -11,7 +11,7 @@ public sealed class StatusCodeMapTests private static readonly TestUnknownException NonExistentException = new(); [Fact] - public void TryFind_ShouldReturn_TrueAndStatusCode_WhenExceptionExists() + public void TryFind_ShouldReturn_TrueAndCode_WhenExceptionExists() { var map = GetDefaultMap(); var result = map.TryFind(ExistentException, out var actualCode); @@ -24,9 +24,10 @@ public void TryFind_ShouldReturn_TrueAndStatusCode_WhenExceptionExists() public void TryFind_ShouldReturn_FalseAndDefault_WhenExceptionDoesNotExist() { var map = GetDefaultMap(); - var result = map.TryFind(NonExistentException, out var _); + var result = map.TryFind(NonExistentException, out var code); Assert.False(result); + Assert.Equal(default, code); } private static StatusCodeMap GetDefaultMap() 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/Mapping/LogLevelMapBuilder.cs b/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs index d1fcef6..5660cb5 100644 --- a/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs +++ b/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilder.cs @@ -15,7 +15,7 @@ public LogLevelMapBuilder Map(LogLevel level) where TException : CrE public LogLevelMap Build() => new(BuildFrozenDictionary()); - + private static void ThrowIfInvalidLevel(LogLevel level) { if (!Enum.IsDefined(level)) diff --git a/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilderExtensions.cs b/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilderExtensions.cs index 2522015..77a22ae 100644 --- a/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilderExtensions.cs +++ b/CR.Exceptions.AspNet/Mapping/LogLevelMapBuilderExtensions.cs @@ -8,7 +8,7 @@ public static class LogLevelMapBuilderExtensions { public LogLevelMapBuilder AddDefaultMappings() { - builder + return builder .Map(LogLevel.Debug) .Map(LogLevel.Debug) .Map(LogLevel.Debug) @@ -16,8 +16,6 @@ public LogLevelMapBuilder AddDefaultMappings() .Map(LogLevel.Debug) .Map(LogLevel.Debug) .Map(LogLevel.Error); - - return builder; } } } \ 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 From e8edeba116f32943d709b026326ffe5d05d0cba9 Mon Sep 17 00:00:00 2001 From: apptade Date: Mon, 3 Aug 2026 20:58:02 +0300 Subject: [PATCH 14/14] Update READMEs --- CR.Exceptions.AspNet/README.md | 29 ++++++---- CR.Exceptions/README.md | 101 ++++++++++++++++++--------------- README.md | 4 +- 3 files changed, 73 insertions(+), 61 deletions(-) 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/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/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