diff --git a/README.md b/README.md index 20eaebc..cfe1f60 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ For method `CreateMap` this library provide a `ConvertUsingEnumMapping` method. If you want to change some mappings, then you can use `MapValue` method. This is a chainable method. -Default the enum values are mapped by value, but it is possible to map by name calling `MapByName()` or `MapByValue()`. +Default the enum values are mapped by value (`MapByValue()`), but it is possible to map by name calling `MapByName()`. For enums which does not have same values and names, you can use `MapByCustom()`. Then you have to add a `MapValue` for every source enum value. ```csharp using AutoMapper.Extensions.EnumMapping; diff --git a/src/AutoMapper.Extensions.EnumMapping.Tests/ReverseCustomEnumMappingByCustom.cs b/src/AutoMapper.Extensions.EnumMapping.Tests/ReverseCustomEnumMappingByCustom.cs new file mode 100644 index 0000000..77f1d0c --- /dev/null +++ b/src/AutoMapper.Extensions.EnumMapping.Tests/ReverseCustomEnumMappingByCustom.cs @@ -0,0 +1,117 @@ +using System; +using System.Reflection; +using AutoMapper.Extensions.EnumMapping.Tests.Internal; +using Shouldly; +using Xunit; + +namespace AutoMapper.Extensions.EnumMapping.Tests +{ + /// + /// Based on issue #30 + /// + public class ReverseCustomEnumMappingByCustom + { + public class Valid : AutoMapperSpecBase + { + Destination _result; + + // Assume, as is the case for my use case, that both the source + // and destination enumerations are created via code generation + // from sources outside our control, and thus I cannot make updates + // or changes to them to work around the issue. + + // This is idiomatic of how a Protobuf enum is generated, with the Unspecified + // value acting as a stand-in for when the field is not set by the client or server + public enum Source + { + Unspecified = 0, + Bar = 1, + Baz = 2, + } + + // In our case, this is generated from an OpenAPI spec via Kiota + public enum Destination + { + BAR_ALT_NAME, // we can't map by name because the names don't match, even with case insensitivity turned on + BAZ_ALT_NAME, + } + + public class TestEnumProfile : Profile + { + public TestEnumProfile() + { + CreateMap() + .ConvertUsingEnumMapping( + opts => + { + opts + .MapByCustom() + .MapValue(Source.Bar, Destination.BAR_ALT_NAME) + .MapValue(Source.Baz, Destination.BAZ_ALT_NAME) + .MapException(Source.Unspecified, () => new InvalidOperationException($"Unspecified values are not supported")); + }) + .ReverseMap(); + } + } + + protected override MapperConfiguration Configuration { get; } = new MapperConfiguration(cfg => + { + cfg.EnableEnumMappingValidation(); + cfg.AddMaps(typeof(ReverseCustomEnumMappingByCustom).GetTypeInfo().Assembly); + }); + + protected override void Because_of() + { + _result = Mapper.Map(Source.Bar); + } + + [Fact] + public void Should_map_enum_by_value() + { + _result.ShouldBe(Destination.BAR_ALT_NAME); + } + + [Fact] + public void TestBarMapping() + { + // Passes + var res = Mapper.Map(Source.Bar); + res.ShouldBe(Destination.BAR_ALT_NAME); + } + + [Fact] + public void TestBazMapping() + { + // Passes + var res = Mapper.Map(Source.Baz); + res.ShouldBe(Destination.BAZ_ALT_NAME); + } + + [Fact] + public void TestUnspecifiedMapping() + { + // Passes + Assert.Throws(() => + { + Mapper.Map(Source.Unspecified); + }); + } + + [Fact] + public void TestReverseBarMapping() + { + // Passes + var res = Mapper.Map(Destination.BAR_ALT_NAME); + res.ShouldBe(Source.Bar); + } + + [Fact] + public void TestReverseBazMapping() + { + // Failure: Expected: Baz But was: Bar => Fixed + var res = Mapper.Map(Destination.BAZ_ALT_NAME); + res.ShouldBe(Source.Baz); + } + } + } +} diff --git a/src/AutoMapper.Extensions.EnumMapping.Tests/ReverseEnumValueMappingByCustom.cs b/src/AutoMapper.Extensions.EnumMapping.Tests/ReverseEnumValueMappingByCustom.cs new file mode 100644 index 0000000..41d97db --- /dev/null +++ b/src/AutoMapper.Extensions.EnumMapping.Tests/ReverseEnumValueMappingByCustom.cs @@ -0,0 +1,156 @@ +using System; +using AutoMapper.Extensions.EnumMapping.Tests.Internal; +using Shouldly; +using Xunit; + +namespace AutoMapper.Extensions.EnumMapping.Tests +{ + public class ReverseEnumValueMappingByCustom + { + public class Valid : AutoMapperSpecBase + { + Destination _result; + public enum Source { Default, Foo, Bar } + public enum Destination { Default, Bar, Foo } + + protected override MapperConfiguration Configuration { get; } = new MapperConfiguration(cfg => + { + cfg.EnableEnumMappingValidation(); + cfg.CreateMap() + .ConvertUsingEnumMapping(opt => opt + .MapByCustom() + .MapValue(Source.Default, Destination.Default) + .MapValue(Source.Foo, Destination.Foo) + .MapValue(Source.Bar, Destination.Bar) + ) + .ReverseMap(); + }); + + protected override void Because_of() + { + _result = Mapper.Map(Source.Bar); + } + + [Fact] + public void Should_map_enum_by_custom() + { + _result.ShouldBe(Destination.Bar); + ((int)_result).ShouldBe((int)Source.Foo); + + } + } + + public class ValidCustomMapping : AutoMapperSpecBase + { + Destination _result; + public enum Source { Default, Bar } + public enum Destination { Default, Bar } + + protected override MapperConfiguration Configuration { get; } = new MapperConfiguration(cfg => + { + cfg.EnableEnumMappingValidation(); + cfg.CreateMap() + .ConvertUsingEnumMapping(opt => opt + .MapByCustom() + .MapValue(Source.Default, Destination.Default) + .MapValue(Source.Bar, Destination.Bar) + ) + .ReverseMap(); + }); + + protected override void Because_of() + { + _result = Mapper.Map(Source.Bar); + } + + [Fact] + public void Should_map_using_custom_map() + { + _result.ShouldBe(Destination.Bar); + } + } + + public class ValidationErrors : NonValidatingSpecBase + { + public enum Source { Default, Foo, Bar } + public enum Destination { Default, Bar } + + protected override MapperConfiguration Configuration => new MapperConfiguration(cfg => + { + cfg.EnableEnumMappingValidation(); + cfg.CreateMap() + .ConvertUsingEnumMapping(opt => opt + .MapByCustom() + .MapValue(Source.Default, Destination.Default) + .MapValue(Source.Bar, Destination.Bar) + ) + .ReverseMap(); + }); + + [Fact] + public void Should_fail_validation() => + new Action(() => Configuration.AssertConfigurationIsValid()).ShouldThrowException( + ex => ex.Message.ShouldBe( + $@"Missing enum mapping from {typeof(Source).FullName} to {typeof(Destination).FullName} based on Custom{Environment.NewLine}The following source values are not mapped:{Environment.NewLine} - Foo{Environment.NewLine}")); + } + + public class CustomMappingWithValidationErrors : NonValidatingSpecBase + { + public enum Source { Default, Foo, Bar, Error } + public enum Destination { Default, Bar } + + protected override MapperConfiguration Configuration { get; } = new MapperConfiguration(cfg => + { + cfg.EnableEnumMappingValidation(); + cfg.CreateMap() + .ConvertUsingEnumMapping(opt => opt + .MapByCustom() + .MapValue(Source.Default, Destination.Default) + .MapException(Source.Foo, () => new NotSupportedException($"Foo is not valid value")) + .MapValue(Source.Bar, Destination.Bar)) + .ReverseMap(); + }); + + [Fact] + public void Should_fail_validation() => + new Action(() => Configuration.AssertConfigurationIsValid()).ShouldThrowException( + ex => ex.Message.ShouldBe( + $@"Missing enum mapping from {typeof(Source).FullName} to {typeof(Destination).FullName} based on Custom{Environment.NewLine}The following source values are not mapped:{Environment.NewLine} - Error{Environment.NewLine}")); + } + + public class ValidCustomReverseMapping : AutoMapperSpecBase + { + Source _resultDefault; + Source _resultFoo; + Source _resultBar; + public enum Source { Default, Bar } + public enum Destination { Default, Foo, Bar } + + protected override MapperConfiguration Configuration { get; } = new MapperConfiguration(cfg => + { + cfg.EnableEnumMappingValidation(); + cfg.CreateMap() + .ConvertUsingEnumMapping(opt => opt + .MapByCustom() + .MapValue(Source.Default, Destination.Default) + .MapValue(Source.Bar, Destination.Bar)) + .ReverseMap(optr => optr.MapByCustom().MapValue(Destination.Foo, Source.Bar)); + }); + + protected override void Because_of() + { + _resultDefault = Mapper.Map(Destination.Default); + _resultFoo = Mapper.Map(Destination.Foo); + _resultBar = Mapper.Map(Destination.Bar); + } + + [Fact] + public void Should_map_using_reverse_custom_map() + { + _resultDefault.ShouldBe(Source.Default); + _resultFoo.ShouldBe(Source.Bar); + _resultBar.ShouldBe(Source.Bar); + } + } + } +} diff --git a/src/AutoMapper.Extensions.EnumMapping/IEnumConfigurationExpression.cs b/src/AutoMapper.Extensions.EnumMapping/IEnumConfigurationExpression.cs index fa698cd..6878a25 100644 --- a/src/AutoMapper.Extensions.EnumMapping/IEnumConfigurationExpression.cs +++ b/src/AutoMapper.Extensions.EnumMapping/IEnumConfigurationExpression.cs @@ -24,6 +24,12 @@ public interface IEnumConfigurationExpression /// Enum configuration options IEnumConfigurationExpression MapByValue(); + /// + /// (default) Map enum values by custom mapping (no default mapping used) + /// + /// Enum configuration options + IEnumConfigurationExpression MapByCustom(); + /// /// Map enum value from source to destination value /// diff --git a/src/AutoMapper.Extensions.EnumMapping/Internal/EnumMappingFeature.cs b/src/AutoMapper.Extensions.EnumMapping/Internal/EnumMappingFeature.cs index a585503..aba26df 100644 --- a/src/AutoMapper.Extensions.EnumMapping/Internal/EnumMappingFeature.cs +++ b/src/AutoMapper.Extensions.EnumMapping/Internal/EnumMappingFeature.cs @@ -69,7 +69,7 @@ private Dictionary> CreateDefaultEnu } } } - else + else if (EnumMappingType == EnumMappingType.Value) { var sourceEnumValueType = Enum.GetUnderlyingType(sourceType); var destinationEnumValueType = Enum.GetUnderlyingType(destinationType); @@ -89,6 +89,10 @@ private Dictionary> CreateDefaultEnu } } } + else + { + // Custom does not have default mapping + } return enumValueMappings; } @@ -105,6 +109,11 @@ public IEnumConfigurationExpression MapByValue() EnumMappingType = EnumMappingType.Value; return this; } + public IEnumConfigurationExpression MapByCustom() + { + EnumMappingType = EnumMappingType.Custom; + return this; + } public IEnumConfigurationExpression MapValue(TSource source, TDestination destination) { @@ -126,10 +135,14 @@ public IMappingFeature Reverse() { reverseEnumConfigurationExpression.MapByName(IgnoreCase); } - else + else if (EnumMappingType == EnumMappingType.Value) { reverseEnumConfigurationExpression.MapByValue(); } + else + { + reverseEnumConfigurationExpression.MapByCustom(); + } var reverseEnumValueMappingsOverride = new Dictionary(); var sourceEnumValueType = Enum.GetUnderlyingType(typeof(TSource)); @@ -154,31 +167,38 @@ public IMappingFeature Reverse() var sourceValues = destinationsPerSourceMapping.Select(x => x.Key).ToList(); var hasDestinationValueSameValueInSource = destinationValueAsSourceType.HasValue && sourceValues.Contains(destinationValueAsSourceType.Value); - if (hasDestinationValueSameValueInSource) + if (hasDestinationValueSameValueInSource && EnumMappingType != EnumMappingType.Custom) { // if there is a matching source and destination value, then that mapping is preferred and no override is needed continue; } - foreach (var sourceValue in sourceValues) + if (EnumMappingType != EnumMappingType.Custom) { - var hasDestinationSameValueAsSource = HasDestinationSameValueAsSource(sourceValue, destinationEnumValueType, out TDestination? sourceValueAsDestinationType); - - if (!hasDestinationSameValueAsSource) + foreach (var sourceValue in sourceValues) { - continue; - } + var hasDestinationSameValueAsSource = HasDestinationSameValueAsSource(sourceValue, destinationEnumValueType, out TDestination? sourceValueAsDestinationType); - var isSourceValueUsedInDestinationPartOfEnumMapping = sourceValueAsDestinationType.HasValue - && enumValueMappings.Values.Any(x => x.GetDestinationType == GetDestinationType.Value - && Equals(x.GetDestinationFunc.Invoke(), sourceValueAsDestinationType.Value)); - if (!isSourceValueUsedInDestinationPartOfEnumMapping) - { - // if there is a source which is not a destination part of a mapping, then that mapping cannot reversed - continue; - } + if (!hasDestinationSameValueAsSource) + { + continue; + } - reverseEnumValueMappingsOverride.Add(destinationValue, sourceValue); + var isSourceValueUsedInDestinationPartOfEnumMapping = sourceValueAsDestinationType.HasValue + && enumValueMappings.Values.Any(x => x.GetDestinationType == GetDestinationType.Value + && Equals(x.GetDestinationFunc.Invoke(), sourceValueAsDestinationType.Value)); + if (!isSourceValueUsedInDestinationPartOfEnumMapping) + { + // if there is a source which is not a destination part of a mapping, then that mapping cannot reversed + continue; + } + + reverseEnumValueMappingsOverride.Add(destinationValue, sourceValue); + } + } + else if (!reverseEnumValueMappingsOverride.ContainsKey(destinationValue) && sourceValues.Count == 1) + { + reverseEnumValueMappingsOverride.Add(destinationValue, sourceValues.Single()); } } @@ -234,10 +254,15 @@ private bool HasDestinationSameValueAsSource(TSource sourceValue, Type destinati } } } - else + else if(EnumMappingType == EnumMappingType.Value) { destinationValueAsSourceType = (TSource)Convert.ChangeType(destinationValue, sourceEnumValueType); } + else + { + // Custom mapping does not have default + destinationValueAsSourceType = null; + } return destinationValueAsSourceType; } diff --git a/src/AutoMapper.Extensions.EnumMapping/Internal/EnumMappingType.cs b/src/AutoMapper.Extensions.EnumMapping/Internal/EnumMappingType.cs index 72010c6..edaf2d1 100644 --- a/src/AutoMapper.Extensions.EnumMapping/Internal/EnumMappingType.cs +++ b/src/AutoMapper.Extensions.EnumMapping/Internal/EnumMappingType.cs @@ -3,6 +3,7 @@ internal enum EnumMappingType { Value = 0, - Name = 1 + Name = 1, + Custom = 2 } }