diff --git a/.github/workflows/BuildAndTest.yml b/.github/workflows/BuildAndTest.yml new file mode 100644 index 0000000..286bc2a --- /dev/null +++ b/.github/workflows/BuildAndTest.yml @@ -0,0 +1,27 @@ +name: Build and test + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Use dotnet 5.0.x + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '5.0.x' + + - name: Restore + run: dotnet restore + - name: Build + run: dotnet build + - name: Test + run: dotnet test \ No newline at end of file diff --git a/Enterwell.AutoMapper.Extensions.Tests/AutoMapperExtensionsTests.cs b/Enterwell.AutoMapper.Extensions.Tests/AutoMapperExtensionsTests.cs index d6329b3..694635b 100644 --- a/Enterwell.AutoMapper.Extensions.Tests/AutoMapperExtensionsTests.cs +++ b/Enterwell.AutoMapper.Extensions.Tests/AutoMapperExtensionsTests.cs @@ -1,15 +1,227 @@ using System; +using AutoMapper; using Xunit; namespace Enterwell.AutoMapper.Extensions.Tests { + /// + /// Tests for AutoMapper extensions. + /// public class AutoMapperExtensionsTests { - + /// + /// Tests MapTo extension. + /// [Fact] - public void Test1() + public void AutoMapperExtensionsTests_MapTo() { - Assert.Equal(1,1); + var mapper = new MapperConfiguration(cfg => + { + cfg.CreateMap() + .ForMember(dst => dst.DestinationPropOne, + opt => opt.MapFrom(src => src.SourcePropOne)); + }).CreateMapper(); + + var source = new MockSimpleEntitySource + { + SourcePropOne = 5, + CommonPropOne = Guid.NewGuid().ToString() + }; + + var destination = source.MapTo(mapper); + + Assert.Equal(source.SourcePropOne, destination.DestinationPropOne); + Assert.Equal(source.CommonPropOne, destination.CommonPropOne); + } + + /// + /// Tests MapProperty extension. + /// + [Fact] + public void AutoMapperExtensionsTests_MapProperty() + { + var mapper = new MapperConfiguration(cfg => + { + cfg.CreateMap() + .MapProperty(dst => dst.DestinationPropOne, src => src.SourcePropOne); + }).CreateMapper(); + + var commonProp = Guid.NewGuid().ToString(); + var srcProp = 5; + var source = new MockSimpleEntitySource { CommonPropOne = commonProp, SourcePropOne = srcProp }; + var dst = mapper.Map(source); + + Assert.Equal(source.CommonPropOne, dst.CommonPropOne); + Assert.Equal(source.SourcePropOne, dst.DestinationPropOne); + } + + /// + /// Tests MapPropertyFunc extension. + /// + [Fact] + public void AutoMapperExtensionsTests_MapPropertyFunc() + { + const int expectedDestProp = 3; + var mapper = new MapperConfiguration(cfg => + { + cfg.CreateMap() + .MapPropertyFunc(dst => dst.DestinationPropOne, src => Math.Min(src.SourcePropOne, expectedDestProp)); + }).CreateMapper(); + + var commonProp = Guid.NewGuid().ToString(); + const int srcProp = 5; + var source = new MockSimpleEntitySource { CommonPropOne = commonProp, SourcePropOne = srcProp }; + var dst = mapper.Map(source); + + Assert.Equal(source.CommonPropOne, dst.CommonPropOne); + Assert.Equal(dst.DestinationPropOne, expectedDestProp); + } + + /// + /// Tests map with additional required property that is valid. + /// + [Fact] + public void AutoMapperExtensionsTests_MapComposeTo_RequiredCompositePropertyValid() + { + var mapper = new MapperConfiguration(cfg => + { + cfg.CreateMap() + .MapProperty(dst => dst.DestinationPropOne, src => src.SourcePropOne) + .MapCompositePropertyRequired(dst => dst.AdditionalRequiredProperty); + }).CreateMapper(); + + var commonProp = Guid.NewGuid().ToString(); + int srcProp = 5; + var additionalProperty = DateTime.Now; + + var source = new MockSimpleEntitySource + { + CommonPropOne = commonProp, + SourcePropOne = srcProp + }; + + var destination = source.MapComposeTo(mapper, additionalProperty); + + Assert.Equal(source.CommonPropOne, destination.CommonPropOne); + Assert.Equal(source.SourcePropOne, destination.DestinationPropOne); + Assert.Equal(destination.AdditionalRequiredProperty, additionalProperty); + } + + /// + /// Tests MapCompose with additional required property that is missing. + /// + [Fact] + public void AutoMapperExtensionsTests_MapComposeTo_RequiredCompositePropertyMissing() + { + var mapper = new MapperConfiguration(cfg => + { + cfg.CreateMap() + .MapProperty(dst => dst.DestinationPropOne, src => src.SourcePropOne) + .MapCompositePropertyRequired(dst => dst.AdditionalRequiredProperty); + }).CreateMapper(); + + var source = new MockSimpleEntitySource(); + + var exception = Record.Exception(() => source.MapComposeTo(mapper)); + + Assert.NotNull(exception); + } + + /// + /// Tests MapCompose with additional required property provided wrong type. + /// + [Fact] + public void AutoMapperExtensionsTests_MapComposeTo_CompositePropertyWrongType() + { + var mapper = new MapperConfiguration(cfg => + { + cfg.CreateMap() + .MapProperty(dst => dst.DestinationPropOne, src => src.SourcePropOne) + .MapCompositePropertyRequired(dst => dst.AdditionalRequiredProperty); + }).CreateMapper(); + + const string additionalPropertyWrongType = "String instead of DateTime"; + + var source = new MockSimpleEntitySource(); + + var exception = Record.Exception(() => source.MapComposeTo(mapper, additionalPropertyWrongType)); + + Assert.NotNull(exception); + } + + /// + /// Tests MapCompose second additional property optional. + /// + [Fact] + public void AutoMapperExtensionsTests_MapComposeTo_SecondPropertyOptional() + { + var mapper = new MapperConfiguration(cfg => + { + cfg.CreateMap() + .MapProperty(dst => dst.DestinationPropOne, src => src.SourcePropOne) + .MapCompositePropertyRequired(dst => dst.AdditionalRequiredProperty) + .MapCompositeProperty(dst => dst.SecondAdditionalOptionalProperty, 1); + }).CreateMapper(); + + var source = new MockSimpleEntitySource(); + var firstAdditionalProperty = DateTime.Now; + var destination = source.MapComposeTo(mapper, firstAdditionalProperty); + Assert.Equal(destination.AdditionalRequiredProperty, firstAdditionalProperty); + } + + /// + /// Tests MapCompose two additional properties required. + /// + [Fact] + public void AutoMapperExtensionsTests_MapComposeTo_TwoAdditionalPropertiesRequired() + { + var mapper = new MapperConfiguration(cfg => + { + cfg.CreateMap() + .MapProperty(dst => dst.DestinationPropOne, src => src.SourcePropOne) + .MapCompositePropertyRequired(dst => dst.AdditionalRequiredProperty) + .MapCompositePropertyRequired(dst => dst.SecondAdditionalOptionalProperty, 1); + }).CreateMapper(); + + var source = new MockSimpleEntitySource(); + var firstAdditionalProperty = DateTime.Now; + var secondAdditionalProperty = Guid.NewGuid().ToString(); + var destination = source.MapComposeTo(mapper, firstAdditionalProperty, secondAdditionalProperty); + Assert.Equal(destination.AdditionalRequiredProperty, firstAdditionalProperty); + Assert.Equal(destination.SecondAdditionalOptionalProperty, secondAdditionalProperty); + } + + /// + /// Tests map with MapCompositeAll. + /// + [Fact] + public void AutoMapperExtensionsTests_MapCompositeAll() + { + var mapper = new MapperConfiguration(cfg => + { + cfg.CreateMap() + .MapProperty(dst => dst.DestinationPropOne, src => src.SourcePropOne) + .MapCompositeAll(typeof(MockComposedEntitySource)); + }).CreateMapper(); + + var source = new MockSimpleEntitySource + { + SourcePropOne = 1, + CommonPropOne = Guid.NewGuid().ToString() + }; + + var composedSource = new MockComposedEntitySource + { + AdditionalRequiredProperty = DateTime.Now, + SecondAdditionalOptionalProperty = Guid.NewGuid().ToString() + }; + + var destination = source.MapComposeTo(mapper, composedSource); + + Assert.Equal(destination.CommonPropOne, source.CommonPropOne); + Assert.Equal(destination.DestinationPropOne, source.SourcePropOne); + Assert.Equal(destination.AdditionalRequiredProperty, composedSource.AdditionalRequiredProperty); + Assert.Equal(destination.SecondAdditionalOptionalProperty, composedSource.SecondAdditionalOptionalProperty); } } } diff --git a/Enterwell.AutoMapper.Extensions.Tests/AutoMapperTestProfile.cs b/Enterwell.AutoMapper.Extensions.Tests/AutoMapperTestProfile.cs deleted file mode 100644 index 4058089..0000000 --- a/Enterwell.AutoMapper.Extensions.Tests/AutoMapperTestProfile.cs +++ /dev/null @@ -1,35 +0,0 @@ -using AutoMapper; - -namespace Enterwell.AutoMapper.Extensions.Tests -{ - /// - /// Automapper tests profile. - /// - /// - public class AutoMapperTestProfile : Profile - { - /// - /// Initializes a new instance of the class. - /// - public AutoMapperTestProfile() - { - - } - } - - /// - /// Mock entity for source. - /// - public class MockEntitySource - { - public string Prop1 { get; set; } - } - - /// - /// Mock entity for destination. - /// - public class MockEntityDestination - { - public string Prop1 { get; set; } - } -} \ No newline at end of file diff --git a/Enterwell.AutoMapper.Extensions.Tests/Enterwell.AutoMapper.Extensions.Tests.csproj b/Enterwell.AutoMapper.Extensions.Tests/Enterwell.AutoMapper.Extensions.Tests.csproj index b44df53..bfa7f21 100644 --- a/Enterwell.AutoMapper.Extensions.Tests/Enterwell.AutoMapper.Extensions.Tests.csproj +++ b/Enterwell.AutoMapper.Extensions.Tests/Enterwell.AutoMapper.Extensions.Tests.csproj @@ -7,7 +7,7 @@ - + @@ -20,4 +20,8 @@ + + + + diff --git a/Enterwell.AutoMapper.Extensions.Tests/MockComposedEntityDestination.cs b/Enterwell.AutoMapper.Extensions.Tests/MockComposedEntityDestination.cs new file mode 100644 index 0000000..6ed4715 --- /dev/null +++ b/Enterwell.AutoMapper.Extensions.Tests/MockComposedEntityDestination.cs @@ -0,0 +1,27 @@ +using System; + +namespace Enterwell.AutoMapper.Extensions.Tests +{ + /// + /// Mock composed entity destination. + /// + /// + public class MockComposedEntityDestination : MockSimpleEntityDestination + { + /// + /// Gets or sets the additional property. + /// + /// + /// The additional property. + /// + public DateTime AdditionalRequiredProperty { get; set; } + + /// + /// Gets or sets the second additional property. + /// + /// + /// The second additional property. + /// + public string SecondAdditionalOptionalProperty { get; set; } + } +} \ No newline at end of file diff --git a/Enterwell.AutoMapper.Extensions.Tests/MockComposedEntitySource.cs b/Enterwell.AutoMapper.Extensions.Tests/MockComposedEntitySource.cs new file mode 100644 index 0000000..f5134c6 --- /dev/null +++ b/Enterwell.AutoMapper.Extensions.Tests/MockComposedEntitySource.cs @@ -0,0 +1,26 @@ +using System; + +namespace Enterwell.AutoMapper.Extensions.Tests +{ + /// + /// Mock source composed entity. + /// + public class MockComposedEntitySource + { + /// + /// Gets or sets the additional property. + /// + /// + /// The additional property. + /// + public DateTime AdditionalRequiredProperty { get; set; } + + /// + /// Gets or sets the second additional property. + /// + /// + /// The second additional property. + /// + public string SecondAdditionalOptionalProperty { get; set; } + } +} \ No newline at end of file diff --git a/Enterwell.AutoMapper.Extensions.Tests/MockEntityDestination.cs b/Enterwell.AutoMapper.Extensions.Tests/MockEntityDestination.cs deleted file mode 100644 index 0b8c66a..0000000 --- a/Enterwell.AutoMapper.Extensions.Tests/MockEntityDestination.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Enterwell.AutoMapper.Extensions.Tests -{ - /// - /// Mock entity for destination. - /// - public class MockEntityDestination - { - /// - /// Gets or sets the prop1. - /// - /// - /// The prop1. - /// - public string Prop1 { get; set; } - } -} \ No newline at end of file diff --git a/Enterwell.AutoMapper.Extensions.Tests/MockEntitySource.cs b/Enterwell.AutoMapper.Extensions.Tests/MockEntitySource.cs deleted file mode 100644 index 677e594..0000000 --- a/Enterwell.AutoMapper.Extensions.Tests/MockEntitySource.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Enterwell.AutoMapper.Extensions.Tests -{ - /// - /// Mock entity for source. - /// - public class MockEntitySource - { - public string Prop1 { get; set; } - } -} \ No newline at end of file diff --git a/Enterwell.AutoMapper.Extensions.Tests/MockSimpleEntityDestination.cs b/Enterwell.AutoMapper.Extensions.Tests/MockSimpleEntityDestination.cs new file mode 100644 index 0000000..ffd9013 --- /dev/null +++ b/Enterwell.AutoMapper.Extensions.Tests/MockSimpleEntityDestination.cs @@ -0,0 +1,24 @@ +namespace Enterwell.AutoMapper.Extensions.Tests +{ + /// + /// Mock entity for destination. + /// + public class MockSimpleEntityDestination + { + /// + /// Gets or sets the prop1. + /// + /// + /// The prop1. + /// + public string CommonPropOne { get; set; } + + /// + /// Gets or sets the destination property one. + /// + /// + /// The destination property one. + /// + public int DestinationPropOne { get; set; } + } +} \ No newline at end of file diff --git a/Enterwell.AutoMapper.Extensions.Tests/MockSimpleEntitySource.cs b/Enterwell.AutoMapper.Extensions.Tests/MockSimpleEntitySource.cs new file mode 100644 index 0000000..7768e8c --- /dev/null +++ b/Enterwell.AutoMapper.Extensions.Tests/MockSimpleEntitySource.cs @@ -0,0 +1,40 @@ +using System; + +namespace Enterwell.AutoMapper.Extensions.Tests +{ + /// + /// Mock simple entity source + /// + public class MockSimpleEntitySource + { + /// + /// Gets or sets the common property one. + /// + /// + /// The common property one. + /// + public string CommonPropOne { get; set; } + + /// + /// Gets or sets the source property one. + /// + /// + /// The source property one. + /// + public int SourcePropOne { get; set; } + } + + /// + /// Mock additional entity source. + /// + public class MockAdditionalEntitySource + { + /// + /// Gets or sets the additional source property. + /// + /// + /// The additional source property. + /// + public DateTime AdditionalSourceProperty { get; set; } + } +} \ No newline at end of file diff --git a/Enterwell.AutoMapper.Extensions/Enterwell.AutoMapper.Extensions.csproj b/Enterwell.AutoMapper.Extensions/Enterwell.AutoMapper.Extensions.csproj index 69d99c7..d3f72cb 100644 --- a/Enterwell.AutoMapper.Extensions/Enterwell.AutoMapper.Extensions.csproj +++ b/Enterwell.AutoMapper.Extensions/Enterwell.AutoMapper.Extensions.csproj @@ -1,11 +1,10 @@ - - + netstandard2.0 + 1.0.0 + true - - - + \ No newline at end of file diff --git a/Enterwell.AutoMapper.Extensions/MapCompositeDefinitionExtensions.cs b/Enterwell.AutoMapper.Extensions/MapCompositeDefinitionExtensions.cs index 659a7b1..ed65b9c 100644 --- a/Enterwell.AutoMapper.Extensions/MapCompositeDefinitionExtensions.cs +++ b/Enterwell.AutoMapper.Extensions/MapCompositeDefinitionExtensions.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Linq.Expressions; using AutoMapper; +using AutoMapper.Internal; namespace Enterwell.AutoMapper.Extensions { @@ -82,5 +83,56 @@ public static IMappingExpression MapCompositePropertyRequired> dest, int additionalPropertyIndex = 0) => map.MapPropertyFunc(dest, (source, ctx) => ctx.GetCompositePropertyRequired(additionalPropertyIndex)); + + /// + /// An IMappingExpression<TSrc,TDest> extension method that map composite object properties to source object. + /// + /// Type of the source. + /// Type of the destination. + /// The map to act on. + /// Type of the composite. + /// (Optional) The index. + public static void MapCompositeAll( + this IMappingExpression map, + Type compositeType, + int index = 0) => + Array.ForEach(compositeType.GetProperties(), + compositeProperty => + map.ForMember(compositeProperty.Name, + opt => opt.MapFrom((src, dest, _, ctx) => + compositeProperty.GetValue(ctx.GetCompositePropertyRequired(compositeType, index))))); + + /// + /// Gets the composite property required. + /// + /// The context. + /// The type. + /// The index. + /// Returns the required composite property at given index. + /// Failed to cast composite property at index {index} to type {typeof(TSource)} + public static object GetCompositePropertyRequired(this ResolutionContext context, Type type, int index) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + if (type == null) throw new ArgumentNullException(nameof(type)); + if (context.Items.Values.Count <= index) + throw new ArgumentOutOfRangeException($"Missing parameter at index {index} of type {type.FullName}"); + + return context.Items.Values.ElementAt(index) != type.GetDefaultValue() + ? context.Items.Values.ElementAt(index) + : throw new InvalidCastException( + $"Failed to cast composite property at index {index} to type {type.FullName}"); + } + + /// + /// Gets the default value of given type. + /// + /// The type. + /// Returns null for classes and default value for value types. + private static object GetDefaultValue(this Type t) + { + if (t.IsValueType && Nullable.GetUnderlyingType(t) == null) + return Activator.CreateInstance(t); + return null; + } } } \ No newline at end of file diff --git a/Enterwell.AutoMapper.Extensions/MapPropertyDefinitionExtensions.cs b/Enterwell.AutoMapper.Extensions/MapPropertyDefinitionExtensions.cs index ad3af48..bb2b1df 100644 --- a/Enterwell.AutoMapper.Extensions/MapPropertyDefinitionExtensions.cs +++ b/Enterwell.AutoMapper.Extensions/MapPropertyDefinitionExtensions.cs @@ -69,7 +69,6 @@ public static IMappingExpression MapPropertyFunc sourceFunc) => map.ForMember(dest, opt => opt.MapFrom((source, unused1, unused2, mapper) => sourceFunc(source, mapper))); - /// /// An IMappingExpression<TSource,TDest> extension method that maps property to source /// member.