diff --git a/AgileMapper.PerformanceTester.Net45/AgileMapper.PerformanceTester.Net45.csproj b/AgileMapper.PerformanceTester.Net45/AgileMapper.PerformanceTester.Net45.csproj index cf7abc181..6d774bf3e 100644 --- a/AgileMapper.PerformanceTester.Net45/AgileMapper.PerformanceTester.Net45.csproj +++ b/AgileMapper.PerformanceTester.Net45/AgileMapper.PerformanceTester.Net45.csproj @@ -37,8 +37,8 @@ ..\packages\AgileObjects.NetStandardPolyfills.1.4.0\lib\net40\AgileObjects.NetStandardPolyfills.dll - - ..\packages\AgileObjects.ReadableExpressions.2.1.0\lib\net40\AgileObjects.ReadableExpressions.dll + + ..\packages\AgileObjects.ReadableExpressions.2.1.1\lib\net40\AgileObjects.ReadableExpressions.dll ..\packages\AutoMapper.7.0.1\lib\net45\AutoMapper.dll @@ -46,8 +46,8 @@ ..\packages\Expressmapper.1.9.1\lib\net45\ExpressMapper.dll - - ..\packages\Mapster.3.2.0\lib\net45\Mapster.dll + + ..\packages\Mapster.3.3.1\lib\net45\Mapster.dll ..\packages\ValueInjecter.3.1.3\lib\net45\Omu.ValueInjecter.dll diff --git a/AgileMapper.PerformanceTester.Net45/packages.config b/AgileMapper.PerformanceTester.Net45/packages.config index 92d57fe9a..fcd3dee54 100644 --- a/AgileMapper.PerformanceTester.Net45/packages.config +++ b/AgileMapper.PerformanceTester.Net45/packages.config @@ -1,10 +1,10 @@  - + - + \ No newline at end of file diff --git a/AgileMapper.PerformanceTester.NetCore21/AgileMapper.PerformanceTester.NetCore21.csproj b/AgileMapper.PerformanceTester.NetCore21/AgileMapper.PerformanceTester.NetCore21.csproj index 420da1489..62060b4bf 100644 --- a/AgileMapper.PerformanceTester.NetCore21/AgileMapper.PerformanceTester.NetCore21.csproj +++ b/AgileMapper.PerformanceTester.NetCore21/AgileMapper.PerformanceTester.NetCore21.csproj @@ -10,7 +10,7 @@ - + diff --git a/AgileMapper.PerformanceTesting/AgileMapper.PerformanceTesting.csproj b/AgileMapper.PerformanceTesting/AgileMapper.PerformanceTesting.csproj index c06fe05e4..6a4ff3140 100644 --- a/AgileMapper.PerformanceTesting/AgileMapper.PerformanceTesting.csproj +++ b/AgileMapper.PerformanceTesting/AgileMapper.PerformanceTesting.csproj @@ -9,7 +9,7 @@ - + diff --git a/AgileMapper.UnitTests.Net35/AgileMapper.UnitTests.Net35.csproj b/AgileMapper.UnitTests.Net35/AgileMapper.UnitTests.Net35.csproj index 0e0c4f28a..7f64f66af 100644 --- a/AgileMapper.UnitTests.Net35/AgileMapper.UnitTests.Net35.csproj +++ b/AgileMapper.UnitTests.Net35/AgileMapper.UnitTests.Net35.csproj @@ -37,8 +37,8 @@ ..\packages\AgileObjects.NetStandardPolyfills.1.4.0\lib\net35\AgileObjects.NetStandardPolyfills.dll - - ..\packages\AgileObjects.ReadableExpressions.2.1.0\lib\net35\AgileObjects.ReadableExpressions.dll + + ..\packages\AgileObjects.ReadableExpressions.2.1.1\lib\net35\AgileObjects.ReadableExpressions.dll ..\packages\DynamicLanguageRuntime.1.1.2\lib\Net35\Microsoft.Dynamic.dll diff --git a/AgileMapper.UnitTests.Net35/packages.config b/AgileMapper.UnitTests.Net35/packages.config index e4f4503b6..737e3b5da 100644 --- a/AgileMapper.UnitTests.Net35/packages.config +++ b/AgileMapper.UnitTests.Net35/packages.config @@ -1,7 +1,7 @@  - + diff --git a/AgileMapper.UnitTests.NetCore2.1/AgileMapper.UnitTests.NetCore2.1.csproj b/AgileMapper.UnitTests.NetCore2.1/AgileMapper.UnitTests.NetCore2.1.csproj index 73ed7fdf7..d6d85e595 100644 --- a/AgileMapper.UnitTests.NetCore2.1/AgileMapper.UnitTests.NetCore2.1.csproj +++ b/AgileMapper.UnitTests.NetCore2.1/AgileMapper.UnitTests.NetCore2.1.csproj @@ -31,7 +31,7 @@ - + diff --git a/AgileMapper.UnitTests.NetCore2.2/AgileMapper.UnitTests.NetCore2.2.csproj b/AgileMapper.UnitTests.NetCore2.2/AgileMapper.UnitTests.NetCore2.2.csproj new file mode 100644 index 000000000..170bb8528 --- /dev/null +++ b/AgileMapper.UnitTests.NetCore2.2/AgileMapper.UnitTests.NetCore2.2.csproj @@ -0,0 +1,63 @@ + + + + + netcoreapp2.2 + true + AgileObjects.AgileMapper.UnitTests.NetCore2_1 + AgileObjects.AgileMapper.UnitTests.NetCore2_1 + true + 2.2.2 + false + false + false + false + false + AgileObjects.AgileMapper.UnitTests.NetCore2 + + + + TRACE;DEBUG;NETCOREAPP2_0;NET_STANDARD + + + + TRACE;RELEASE;NETCOREAPP2_0;NET_STANDARD + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + %(RecursiveDir)%(Filename)%(Extension) + + + + + + + + + + + + diff --git a/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj b/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj index 1fc3c9e92..ab86a47df 100644 --- a/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj +++ b/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj @@ -42,8 +42,8 @@ ..\packages\AgileObjects.NetStandardPolyfills.1.4.0\lib\net40\AgileObjects.NetStandardPolyfills.dll - - ..\packages\AgileObjects.ReadableExpressions.2.1.0\lib\net40\AgileObjects.ReadableExpressions.dll + + ..\packages\AgileObjects.ReadableExpressions.2.1.1\lib\net40\AgileObjects.ReadableExpressions.dll @@ -89,7 +89,9 @@ - + + Designer + diff --git a/AgileMapper.UnitTests.NonParallel/app.config b/AgileMapper.UnitTests.NonParallel/app.config index b1d125491..dc349b495 100644 --- a/AgileMapper.UnitTests.NonParallel/app.config +++ b/AgileMapper.UnitTests.NonParallel/app.config @@ -6,6 +6,10 @@ + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.NonParallel/packages.config b/AgileMapper.UnitTests.NonParallel/packages.config index 574dc374e..4f52878b7 100644 --- a/AgileMapper.UnitTests.NonParallel/packages.config +++ b/AgileMapper.UnitTests.NonParallel/packages.config @@ -1,7 +1,7 @@  - + diff --git a/AgileMapper.UnitTests.Orms.Ef5.LocalDb/App.config b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/App.config index c279b5844..814d8bd60 100644 --- a/AgileMapper.UnitTests.Orms.Ef5.LocalDb/App.config +++ b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/App.config @@ -21,6 +21,10 @@ + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/AgileMapper.UnitTests.Orms.Ef5.csproj b/AgileMapper.UnitTests.Orms.Ef5/AgileMapper.UnitTests.Orms.Ef5.csproj index ae63bd726..3b9f091b7 100644 --- a/AgileMapper.UnitTests.Orms.Ef5/AgileMapper.UnitTests.Orms.Ef5.csproj +++ b/AgileMapper.UnitTests.Orms.Ef5/AgileMapper.UnitTests.Orms.Ef5.csproj @@ -46,14 +46,14 @@ ..\AgileMapper.snk - - ..\packages\Effort.2.0.1\lib\net45\Effort.dll + + ..\packages\Effort.2.0.5\lib\net45\Effort.dll ..\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll - ..\packages\NMemory.3.0.0\lib\net45\NMemory.dll + ..\packages\NMemory.3.0.2\lib\net45\NMemory.dll diff --git a/AgileMapper.UnitTests.Orms.Ef5/App.config b/AgileMapper.UnitTests.Orms.Ef5/App.config index 72aa76b53..e5921ea3e 100644 --- a/AgileMapper.UnitTests.Orms.Ef5/App.config +++ b/AgileMapper.UnitTests.Orms.Ef5/App.config @@ -24,6 +24,10 @@ + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/packages.config b/AgileMapper.UnitTests.Orms.Ef5/packages.config index 72190b619..73b2406a2 100644 --- a/AgileMapper.UnitTests.Orms.Ef5/packages.config +++ b/AgileMapper.UnitTests.Orms.Ef5/packages.config @@ -1,8 +1,8 @@  - + - + diff --git a/AgileMapper.UnitTests.Orms.Ef6.LocalDb/App.config b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/App.config index 560d9159f..031cf79f5 100644 --- a/AgileMapper.UnitTests.Orms.Ef6.LocalDb/App.config +++ b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/App.config @@ -20,6 +20,10 @@ + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/AgileMapper.UnitTests.Orms.Ef6.csproj b/AgileMapper.UnitTests.Orms.Ef6/AgileMapper.UnitTests.Orms.Ef6.csproj index 40d995106..4688b28e9 100644 --- a/AgileMapper.UnitTests.Orms.Ef6/AgileMapper.UnitTests.Orms.Ef6.csproj +++ b/AgileMapper.UnitTests.Orms.Ef6/AgileMapper.UnitTests.Orms.Ef6.csproj @@ -46,8 +46,8 @@ ..\AgileMapper.snk - - ..\packages\Effort.EF6.2.0.1\lib\net45\Effort.dll + + ..\packages\Effort.EF6.2.0.5\lib\net45\Effort.dll ..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll @@ -56,7 +56,7 @@ ..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll - ..\packages\NMemory.3.0.0\lib\net45\NMemory.dll + ..\packages\NMemory.3.0.2\lib\net45\NMemory.dll diff --git a/AgileMapper.UnitTests.Orms.Ef6/App.config b/AgileMapper.UnitTests.Orms.Ef6/App.config index b8c386d9b..c4ec3fd85 100644 --- a/AgileMapper.UnitTests.Orms.Ef6/App.config +++ b/AgileMapper.UnitTests.Orms.Ef6/App.config @@ -9,6 +9,10 @@ + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/packages.config b/AgileMapper.UnitTests.Orms.Ef6/packages.config index ea6621399..6073e0bac 100644 --- a/AgileMapper.UnitTests.Orms.Ef6/packages.config +++ b/AgileMapper.UnitTests.Orms.Ef6/packages.config @@ -1,8 +1,8 @@  - + - + diff --git a/AgileMapper.UnitTests.Orms.EfCore1/AgileMapper.UnitTests.Orms.EfCore1.csproj b/AgileMapper.UnitTests.Orms.EfCore1/AgileMapper.UnitTests.Orms.EfCore1.csproj index c5e9dc404..7897c7715 100644 --- a/AgileMapper.UnitTests.Orms.EfCore1/AgileMapper.UnitTests.Orms.EfCore1.csproj +++ b/AgileMapper.UnitTests.Orms.EfCore1/AgileMapper.UnitTests.Orms.EfCore1.csproj @@ -89,19 +89,21 @@ ..\packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll True - - ..\packages\System.Collections.Immutable.1.4.0\lib\netstandard2.0\System.Collections.Immutable.dll + + ..\packages\System.Collections.Immutable.1.5.0\lib\netstandard2.0\System.Collections.Immutable.dll ..\packages\System.ComponentModel.Annotations.4.4.0\lib\net461\System.ComponentModel.Annotations.dll - - ..\packages\System.Console.4.3.0\lib\net46\System.Console.dll + + ..\packages\System.Console.4.3.1\lib\net46\System.Console.dll + True + True - - ..\packages\System.Diagnostics.DiagnosticSource.4.4.1\lib\net46\System.Diagnostics.DiagnosticSource.dll + + ..\packages\System.Diagnostics.DiagnosticSource.4.5.1\lib\net46\System.Diagnostics.DiagnosticSource.dll ..\packages\System.Diagnostics.Tracing.4.3.0\lib\net462\System.Diagnostics.Tracing.dll @@ -109,8 +111,8 @@ ..\packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll - - ..\packages\System.Interactive.Async.3.1.1\lib\net46\System.Interactive.Async.dll + + ..\packages\System.Interactive.Async.3.2.0\lib\net46\System.Interactive.Async.dll ..\packages\System.IO.4.3.0\lib\net462\System.IO.dll @@ -129,8 +131,9 @@ ..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll - - ..\packages\System.Net.Http.4.3.3\lib\net46\System.Net.Http.dll + + ..\packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll + True True @@ -140,14 +143,18 @@ ..\packages\System.Reflection.4.3.0\lib\net462\System.Reflection.dll - - ..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll + + ..\packages\System.Runtime.4.3.1\lib\net462\System.Runtime.dll + True + True - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.4.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - - ..\packages\System.Runtime.Extensions.4.3.0\lib\net462\System.Runtime.Extensions.dll + + ..\packages\System.Runtime.Extensions.4.3.1\lib\net462\System.Runtime.Extensions.dll + True + True ..\packages\System.Runtime.InteropServices.4.3.0\lib\net462\System.Runtime.InteropServices.dll @@ -173,7 +180,9 @@ - ..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll + ..\packages\System.Xml.ReaderWriter.4.3.1\lib\net46\System.Xml.ReaderWriter.dll + True + True ..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll diff --git a/AgileMapper.UnitTests.Orms.EfCore1/app.config b/AgileMapper.UnitTests.Orms.EfCore1/app.config index ad5ef9c84..b3f7d15cd 100644 --- a/AgileMapper.UnitTests.Orms.EfCore1/app.config +++ b/AgileMapper.UnitTests.Orms.EfCore1/app.config @@ -32,11 +32,11 @@ - + - + @@ -54,6 +54,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/packages.config b/AgileMapper.UnitTests.Orms.EfCore1/packages.config index 71e817721..eaf96def4 100644 --- a/AgileMapper.UnitTests.Orms.EfCore1/packages.config +++ b/AgileMapper.UnitTests.Orms.EfCore1/packages.config @@ -17,16 +17,16 @@ - + - + - + - + @@ -35,7 +35,7 @@ - + @@ -43,9 +43,9 @@ - - - + + + @@ -60,7 +60,7 @@ - + diff --git a/AgileMapper.UnitTests.Orms.EfCore2/AgileMapper.UnitTests.Orms.EfCore2.csproj b/AgileMapper.UnitTests.Orms.EfCore2/AgileMapper.UnitTests.Orms.EfCore2.csproj index e6ebc1529..6852fcf27 100644 --- a/AgileMapper.UnitTests.Orms.EfCore2/AgileMapper.UnitTests.Orms.EfCore2.csproj +++ b/AgileMapper.UnitTests.Orms.EfCore2/AgileMapper.UnitTests.Orms.EfCore2.csproj @@ -88,33 +88,35 @@ ..\packages\Remotion.Linq.2.2.0\lib\net45\Remotion.Linq.dll - - ..\packages\System.Collections.Immutable.1.4.0\lib\netstandard2.0\System.Collections.Immutable.dll + + ..\packages\System.Collections.Immutable.1.5.0\lib\netstandard2.0\System.Collections.Immutable.dll - - ..\packages\System.ComponentModel.Annotations.4.4.0\lib\net461\System.ComponentModel.Annotations.dll + + ..\packages\System.ComponentModel.Annotations.4.5.0\lib\net461\System.ComponentModel.Annotations.dll - - ..\packages\System.Diagnostics.DiagnosticSource.4.4.1\lib\net46\System.Diagnostics.DiagnosticSource.dll + + ..\packages\System.Diagnostics.DiagnosticSource.4.5.1\lib\net46\System.Diagnostics.DiagnosticSource.dll - - ..\packages\System.Interactive.Async.3.1.1\lib\net46\System.Interactive.Async.dll + + ..\packages\System.Interactive.Async.3.2.0\lib\net46\System.Interactive.Async.dll ..\packages\System.Reflection.4.3.0\lib\net462\System.Reflection.dll True - - ..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll + + ..\packages\System.Runtime.4.3.1\lib\net462\System.Runtime.dll + True True - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.4.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - - ..\packages\System.Runtime.Extensions.4.3.0\lib\net462\System.Runtime.Extensions.dll + + ..\packages\System.Runtime.Extensions.4.3.1\lib\net462\System.Runtime.Extensions.dll + True True @@ -179,6 +181,7 @@ + diff --git a/AgileMapper.UnitTests.Orms.EfCore2/WhenMappingOverEnumerables.cs b/AgileMapper.UnitTests.Orms.EfCore2/WhenMappingOverEnumerables.cs new file mode 100644 index 000000000..5c3967c68 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/WhenMappingOverEnumerables.cs @@ -0,0 +1,84 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2 +{ + using System.Linq; + using System.Threading.Tasks; + using Common; + using Infrastructure; + using Microsoft.EntityFrameworkCore; + using Orms.Infrastructure; + using TestClasses; + using Xunit; + + public class WhenMappingOverEnumerables : OrmTestClassBase + { + public WhenMappingOverEnumerables(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldUpdateADbSetLocalCollection() + { + return RunTest(async (writeContext, mapper) => + { + await writeContext.Persons.AddRangeAsync( + new Person { Name = "One", Address = new Address { Line1 = "One Line 1", Line2 = "One Line 2" } }, + new Person { Name = "Two" }, + new Person { Name = "Three", Address = new Address { Line1 = "Three Line 1" } }, + new Person { Name = "Four" }); + + await writeContext.SaveChangesAsync(); + + using (var readContext = new EfCore2TestDbContext()) + { + await readContext.Persons.LoadAsync(); + + var localPersons = readContext.Persons.Local; + + var orderedPersons = localPersons.OrderBy(p => p.PersonId).ToArray(); + + orderedPersons.Length.ShouldBe(4); + orderedPersons.First().Name.ShouldBe("One"); + orderedPersons.Second().Name.ShouldBe("Two"); + orderedPersons.Third().Name.ShouldBe("Three"); + orderedPersons.Fourth().Name.ShouldBe("Four"); + + mapper.WhenMapping.InstancesOf().IdentifyUsing(p => p.Name); + + var updatedPersons = new[] + { + new Person { Name = "One" }, + new Person { Name = "Two", Address = new Address { Line1 = "Two Line 1" } }, + new Person { Name = "Three", Address = new Address { Line1 = "Three Line 1", Line2 = "Three Line 2" } }, + new Person { Name = "Five", Address = new Address { Line1 = "Five Line 1" } } + }; + + mapper.Map(updatedPersons).Over(localPersons); + + await readContext.SaveChangesAsync(); + + orderedPersons = localPersons.OrderBy(p => p.PersonId).ToArray(); + + orderedPersons.Length.ShouldBe(4); + orderedPersons.First().Name.ShouldBe("One"); + orderedPersons.First().Address.ShouldBeNull(); + + orderedPersons.Second().Name.ShouldBe("Two"); + orderedPersons.Second().Address.ShouldNotBeNull(); + orderedPersons.Second().Address.Line1.ShouldBe("Two Line 1"); + orderedPersons.Second().Address.Line2.ShouldBeNull(); + + orderedPersons.Third().Name.ShouldBe("Three"); + orderedPersons.Third().Address.ShouldNotBeNull(); + orderedPersons.Third().Address.Line1.ShouldBe("Three Line 1"); + orderedPersons.Third().Address.Line2.ShouldBe("Three Line 2"); + + orderedPersons.Fourth().Name.ShouldBe("Five"); + orderedPersons.Fourth().Address.ShouldNotBeNull(); + orderedPersons.Fourth().Address.Line1.ShouldBe("Five Line 1"); + orderedPersons.Fourth().Address.Line2.ShouldBeNull(); + } + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/app.config b/AgileMapper.UnitTests.Orms.EfCore2/app.config index eafddf1b6..8804e24d2 100644 --- a/AgileMapper.UnitTests.Orms.EfCore2/app.config +++ b/AgileMapper.UnitTests.Orms.EfCore2/app.config @@ -26,6 +26,26 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/packages.config b/AgileMapper.UnitTests.Orms.EfCore2/packages.config index 6a7fabd3b..5c9c707fd 100644 --- a/AgileMapper.UnitTests.Orms.EfCore2/packages.config +++ b/AgileMapper.UnitTests.Orms.EfCore2/packages.config @@ -13,20 +13,20 @@ - - + + - - + + - - - + + + diff --git a/AgileMapper.UnitTests.Orms/app.config b/AgileMapper.UnitTests.Orms/app.config index b1d125491..dc349b495 100644 --- a/AgileMapper.UnitTests.Orms/app.config +++ b/AgileMapper.UnitTests.Orms/app.config @@ -6,6 +6,10 @@ + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj index 9a22ca05a..8d6904379 100644 --- a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj +++ b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj @@ -44,8 +44,8 @@ ..\packages\AgileObjects.NetStandardPolyfills.1.4.0\lib\net40\AgileObjects.NetStandardPolyfills.dll - - ..\packages\AgileObjects.ReadableExpressions.2.1.0\lib\net40\AgileObjects.ReadableExpressions.dll + + ..\packages\AgileObjects.ReadableExpressions.2.1.1\lib\net40\AgileObjects.ReadableExpressions.dll ..\packages\Microsoft.Extensions.Primitives.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll @@ -55,8 +55,8 @@ - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.4.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll diff --git a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringTypeIdentifiersInline.cs b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringTypeIdentifiersInline.cs index 8c7daf1bd..b060c15e0 100644 --- a/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringTypeIdentifiersInline.cs +++ b/AgileMapper.UnitTests/Configuration/Inline/WhenConfiguringTypeIdentifiersInline.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; + using AgileMapper.Configuration; using Common; using TestClasses; #if !NET35 @@ -81,5 +82,141 @@ public void ShouldExtendConfiguredIdentifiersInline() mapper.InlineContexts().ShouldHaveSingleItem(); } } + + [Fact] + public void ShouldUseACompositeIdentifierInline() + { + using (var mapper = Mapper.CreateNew()) + { + var source = new[] + { + new WeddingDto + { + BrideName = "Nat", + GroomName = "Andy", + BrideAddressLine1 = "Nat + Andy's House", + GroomAddressLine1 = "Nat + Andy's House" + }, + new WeddingDto + { + BrideName = "Timea", + GroomName = "David", + BrideAddressLine1 = "Timea + David's House", + GroomAddressLine1 = "Timea + David's House" + } + }; + + var target = new List + { + new WeddingDto + { + BrideName = "Nat", + GroomName = "Andy" + }, + new WeddingDto + { + BrideName = "Kate", + GroomName = "Steve" + } + }; + + mapper.Map(source).OnTo(target, cfg => cfg + .WhenMapping + .InstancesOf() + .IdentifyUsing(a => a.BrideName, a => a.GroomName)); + + target.Count.ShouldBe(3); + + target.First().BrideName.ShouldBe("Nat"); + target.First().GroomName.ShouldBe("Andy"); + target.First().BrideAddressLine1.ShouldBe("Nat + Andy's House"); + target.First().GroomAddressLine1.ShouldBe("Nat + Andy's House"); + + target.Second().BrideName.ShouldBe("Kate"); + target.Second().GroomName.ShouldBe("Steve"); + target.Second().BrideAddressLine1.ShouldBeNull(); + target.Second().GroomAddressLine1.ShouldBeNull(); + + target.Third().BrideName.ShouldBe("Timea"); + target.Third().GroomName.ShouldBe("David"); + target.Third().BrideAddressLine1.ShouldBe("Timea + David's House"); + target.Third().GroomAddressLine1.ShouldBe("Timea + David's House"); + } + } + + [Fact] + public void ShouldUseConfiguredIdentifierInCompositeIdentifierInline() + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping + .InstancesOf>() + .IdentifyUsing(ptf => ptf.Value1); + + var source = new[] + { + new PublicTwoFields> + { + Value1 = 111, + Value2 = new PublicTwoFields { Value1 = 222, Value2 = 333 } + }, + new PublicTwoFields> + { + Value1 = null, + Value2 = null + }, + }; + + var target = new List>> + { + new PublicTwoFields> + { + Value1 = 111, + Value2 = new PublicTwoFields { Value1 = 222 } + } + }; + + var itemOne = target.First(); + + mapper.Map(source).OnTo(target, cfg => cfg + .WhenMapping + .InstancesOf>>() + .IdentifyUsing(ptf => ptf.Value1, ptf => ptf.Value2)); + + target.Count.ShouldBe(2); + + target.First().ShouldBeSameAs(itemOne); + target.First().Value1.ShouldBe(111); + target.First().Value2.ShouldNotBeNull(); + target.First().Value2.Value1.ShouldBe(222); + target.First().Value2.Value2.ShouldBe(333); + + target.Second().Value1.ShouldBeNull(); + } + } + + [Fact] + public void ShouldErrorIfUnidentifiableComplexTypeIdentifierSuppliedInline() + { + var idsEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + var source = new[] + { + new PublicTwoFields>() + }; + + mapper.Map(source).Over(new List>>(1), cfg => cfg + .WhenMapping + .InstancesOf>>() + .IdentifyUsing(ptf => ptf.Value1, ptf => ptf.Value2)); + } + }); + + idsEx.Message.ShouldContain("Unable to determine identifier"); + idsEx.Message.ShouldContain("ptf.Value2 of Type 'PublicField'"); + idsEx.InnerException.ShouldBeOfType(); + } } } diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringTypeIdentifiers.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringTypeIdentifiers.cs index 204c46292..4ef6f83f8 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringTypeIdentifiers.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringTypeIdentifiers.cs @@ -93,39 +93,129 @@ public void ShouldUseAConfiguredIdentifierExpression() } [Fact] - public void ShouldErrorIfRedundantIdentifierSpecified() + public void ShouldUseACompositeIdentifierWithAnEntityKey() { using (var mapper = Mapper.CreateNew()) { - var idEx = Should.Throw(() => - mapper.WhenMapping - .InstancesOf() - .IdentifyUsing(p => p.Id)); + mapper.WhenMapping + .InstancesOf>() + .IdentifyUsing(ptf => ptf.Value1, ptf => ptf.Value2); + + var source = new[] + { + new PublicTwoFields + { + Value1 = 123, + Value2 = new Product { ProductId = "321", Price = 99.99 } + }, + new PublicTwoFields + { + Value1 = 456, + Value2 = new Product { ProductId = "654", Price = 11.99 } + } + }; + + var target = new List> + { + new PublicTwoFields + { + Value1 = 123, + Value2 = new Product { ProductId = "333", Price = 10.99 } + }, + new PublicTwoFields + { + Value1 = 456, + Value2 = new Product { ProductId = "654", Price = 10.99 } + } + }; + + var itemOne = target.First(); + var itemTwo = target.Second(); + + mapper.Map(source).Over(target); + + target.Count.ShouldBe(2); - idEx.Message.ShouldContain("Id is automatically used as the identifier"); - idEx.Message.ShouldContain("does not need to be configured"); + target.First().ShouldBeSameAs(itemTwo); + target.First().Value1.ShouldBe(456); + target.First().Value2.ShouldNotBeNull(); + target.First().Value2.ProductId.ShouldBe("654"); + target.First().Value2.Price.ShouldBe(11.99); + + target.Second().ShouldNotBeSameAs(itemOne); + target.Second().Value1.ShouldBe(123); + target.Second().Value2.ShouldNotBeNull(); + target.Second().Value2.ProductId.ShouldBe("321"); + target.Second().Value2.Price.ShouldBe(99.99); } } [Fact] - public void ShouldErrorIfRedundantCustomNamingIdentifierSpecified() + public void ShouldErrorIfNoCompositeIdentifiersSupplied() { - using (var mapper = Mapper.CreateNew()) + var idsEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.InstancesOf().IdentifyUsing(); + } + }); + + idsEx.Message.ShouldContain("composite identifier values must be specified"); + idsEx.InnerException.ShouldBeOfType(); + } + + [Fact] + public void ShouldErrorIfNullCompositeIdentifierSupplied() + { + var idsEx = Should.Throw(() => { - mapper.WhenMapping.UseNamePattern("^_(.+)_$"); + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.InstancesOf().IdentifyUsing(p => p.Name, null); + } + }); + + idsEx.Message.ShouldContain("composite identifier values must be non-null"); + idsEx.InnerException.ShouldBeOfType(); + } + + [Fact] + public void ShouldErrorIfRedundantIdentifierSupplied() + { + var idEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.InstancesOf().IdentifyUsing(p => p.Id); + } + }); + + idEx.Message.ShouldContain("Id is automatically used as the identifier"); + idEx.Message.ShouldContain("does not need to be configured"); + } + + [Fact] + public void ShouldErrorIfRedundantCustomNamingIdentifierSupplied() + { + var idEx = Should.Throw(() => + { + using (var mapper = Mapper.CreateNew()) + { + mapper.WhenMapping.UseNamePattern("^_(.+)_$"); - var idEx = Should.Throw(() => mapper.WhenMapping .InstancesOf(new { _Id_ = default(int) }) - .IdentifyUsing(d => d._Id_)); + .IdentifyUsing(d => d._Id_); + } + }); - idEx.Message.ShouldContain("_Id_ is automatically used as the identifier"); - idEx.Message.ShouldContain("does not need to be configured"); - } + idEx.Message.ShouldContain("_Id_ is automatically used as the identifier"); + idEx.Message.ShouldContain("does not need to be configured"); } [Fact] - public void ShouldErrorIfMultipleIdentifiersSpecifiedForSameType() + public void ShouldErrorIfMultipleIdentifiersSuppliedForSameType() { Should.Throw(() => { diff --git a/AgileMapper.UnitTests/app.config b/AgileMapper.UnitTests/app.config index b1d125491..dc349b495 100644 --- a/AgileMapper.UnitTests/app.config +++ b/AgileMapper.UnitTests/app.config @@ -6,6 +6,10 @@ + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests/packages.config b/AgileMapper.UnitTests/packages.config index 1c42562b2..d7c2d5300 100644 --- a/AgileMapper.UnitTests/packages.config +++ b/AgileMapper.UnitTests/packages.config @@ -1,9 +1,9 @@  - + - + diff --git a/AgileMapper.sln b/AgileMapper.sln index 135dc7bf5..200dde6ff 100644 --- a/AgileMapper.sln +++ b/AgileMapper.sln @@ -53,6 +53,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AgileMapper.PerformanceTest EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AgileMapper.PerformanceTester.NetCore21", "AgileMapper.PerformanceTester.NetCore21\AgileMapper.PerformanceTester.NetCore21.csproj", "{B5BA0B88-48C7-47AE-9236-719BFE67B0A9}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AgileMapper.UnitTests.NetCore2.2", "AgileMapper.UnitTests.NetCore2.2\AgileMapper.UnitTests.NetCore2.2.csproj", "{D1AB8AB2-CFF5-4AB0-8D06-3ADAD78D6B43}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -139,6 +141,10 @@ Global {B5BA0B88-48C7-47AE-9236-719BFE67B0A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {B5BA0B88-48C7-47AE-9236-719BFE67B0A9}.Release|Any CPU.ActiveCfg = Release|Any CPU {B5BA0B88-48C7-47AE-9236-719BFE67B0A9}.Release|Any CPU.Build.0 = Release|Any CPU + {D1AB8AB2-CFF5-4AB0-8D06-3ADAD78D6B43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1AB8AB2-CFF5-4AB0-8D06-3ADAD78D6B43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1AB8AB2-CFF5-4AB0-8D06-3ADAD78D6B43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1AB8AB2-CFF5-4AB0-8D06-3ADAD78D6B43}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AgileMapper/AgileMapper.csproj b/AgileMapper/AgileMapper.csproj index 89c351a9e..cb16a733f 100644 --- a/AgileMapper/AgileMapper.csproj +++ b/AgileMapper/AgileMapper.csproj @@ -12,27 +12,20 @@ AgileMapper Mapper, Mapping, Mappings, ViewModel, DTO, NetStandard https://github.com/AgileObjects/AgileMapper - https://github.com/agileobjects/AgileMapper/blob/master/LICENCE.md $(PackageTargetFallback);dnxcore50 1.6.1 false false AgileObjects.AgileMapper Copyright © AgileObjects Ltd 2019 - - Support for simple type .ToTarget() configuration -- Support for automatic use of user-defined operators -- Support for configured complex-to-simple Type enumerable projection sources -- Improved null or unconvertible enumerable element handling -- Fixing object-to-object Dictionary mapping bug #110 -- Fixing repeated DTO mapping bug #115 -- Performance improvements -- Updating to ReadableExpressions v2.1 - 1.1.0 + - Improved support for non-standard collections +- Support for composite keys in .IdentityUsing() +- Updating to ReadableExpressions v2.1.1 - + diff --git a/AgileMapper/Api/Configuration/InstanceConfigurator.cs b/AgileMapper/Api/Configuration/InstanceConfigurator.cs index 0b944614c..b7f39336c 100644 --- a/AgileMapper/Api/Configuration/InstanceConfigurator.cs +++ b/AgileMapper/Api/Configuration/InstanceConfigurator.cs @@ -1,12 +1,25 @@ namespace AgileObjects.AgileMapper.Api.Configuration { using System; - using System.Linq.Expressions; + using System.Collections.Generic; using AgileMapper.Configuration; -#if NET35 + using Extensions; using Extensions.Internal; -#endif using Members; + using System.Linq.Expressions; + using ReadableExpressions; + using ReadableExpressions.Extensions; +#if NET35 + using static Microsoft.Scripting.Ast.Expression; + using Expression = Microsoft.Scripting.Ast.Expression; + using ExpressionType = Microsoft.Scripting.Ast.ExpressionType; + using UnaryExpression = Microsoft.Scripting.Ast.UnaryExpression; +#else + using static System.Linq.Expressions.Expression; + using Expression = System.Linq.Expressions.Expression; + using ExpressionType = System.Linq.Expressions.ExpressionType; + using UnaryExpression = System.Linq.Expressions.UnaryExpression; +#endif /// /// Provides options for configuring mappings of the type specified by the type argument. @@ -41,6 +54,113 @@ public void IdentifyUsing(Expression> idExpression) ); } + /// + /// Use a composite identifier composed of the given to + /// uniquely identify instances of the type being configured. + /// + /// + /// The expressions to use to uniquely identify instances of the type being configured. + /// + public void IdentifyUsing(params Expression>[] idExpressions) + { + if (idExpressions.NoneOrNull()) + { + throw new MappingConfigurationException( + "Two or more composite identifier values must be specified.", + new ArgumentException(nameof(idExpressions))); + } + + if (idExpressions.Any(a => a == null)) + { + throw new MappingConfigurationException( + "All supplied composite identifier values must be non-null.", + new ArgumentNullException(nameof(idExpressions))); + } + + var idParts = idExpressions +#if NET35 + .ProjectToArray(id => id.ToDlrExpression()); +#else + ; +#endif + var compositeIdParts = new List((idParts.Length * 2) - 1); + var entityParameter = idParts.First().Parameters.First(); + + compositeIdParts.Add(GetIdPartOrThrow(idParts.First().Body)); + + for (var i = 1; i < idParts.Length;) + { + var idPart = GetIdPartOrThrow(idParts[i++].ReplaceParameterWith(entityParameter)); + + compositeIdParts.Add(StringExpressionExtensions.Underscore); + compositeIdParts.Add(idPart); + } + + var compositeId = compositeIdParts.GetStringConcatCall(); + + var compositeIdLambda = Lambda>(compositeId, entityParameter); + + _configInfo.MapperContext.UserConfigurations.Identifiers.Add(typeof(TObject), compositeIdLambda); + } + + private Expression GetIdPartOrThrow(Expression idPart) + { + if (idPart.Type == typeof(object)) + { + if (idPart.NodeType == ExpressionType.Convert) + { + idPart = ((UnaryExpression)idPart).Operand; + } + + return GetStringIdPart(idPart); + } + + if (idPart.Type.IsSimple()) + { + return GetStringIdPart(idPart); + } + + var typeIdentifier = _configInfo + .MapperContext + .GetIdentifierOrNull(idPart); + + if (typeIdentifier != null) + { + return GetStringIdPart(typeIdentifier); + } + + // ReSharper disable once NotResolvedInText + throw new MappingConfigurationException( + "Unable to determine identifier for composite identifier part " + + $"{idPart.ToReadableString()} of Type '{idPart.Type.GetFriendlyName()}'", + new ArgumentNullException("idExpressions")); + } + + private Expression GetStringIdPart(Expression idPart) + { + if (idPart.Type != typeof(string)) + { + idPart = _configInfo.MapperContext.GetValueConversion(idPart, typeof(string)); + } + + var idPartNestedAccessesChecks = _configInfo + .RuleSet + .GetExpressionInfoFor(idPart) + .NestedAccessChecks; + + if (idPartNestedAccessesChecks == null) + { + return idPart; + } + + idPart = Condition( + idPartNestedAccessesChecks, + idPart, + StringExpressionExtensions.EmptyString); + + return idPart; + } + /// /// Use the given expression to create instances of the type being configured. /// The factory expression is passed a context object containing the current mapping's source and target diff --git a/AgileMapper/Extensions/CollectionData.cs b/AgileMapper/Extensions/CollectionData.cs index 708ef1b3e..427382ad3 100644 --- a/AgileMapper/Extensions/CollectionData.cs +++ b/AgileMapper/Extensions/CollectionData.cs @@ -8,7 +8,8 @@ namespace AgileObjects.AgileMapper.Extensions using NetStandardPolyfills; /// - /// Untyped factory class for creating instances. + /// Untyped factory class for creating instances. This class + /// supports mapping and is not intended to be used from your code. /// public static class CollectionData { @@ -87,28 +88,22 @@ public static CollectionData Create( continue; } - if (targetsById.TryGetValue(sourceItemId, out var targetsWithId)) + if (!targetsById.TryGetValue(sourceItemId, out var targetsWithId) || + EqualityComparer.Default.Equals(sourceItemId, default(TId))) { - if (EqualityComparer.Default.Equals(sourceItemId, default(TId))) - { - newSourceItems.Add(sourceItem); - continue; - } - - var targetItem = targetsWithId[0]; - - absentTargetItems.Remove(targetItem); - intersection.Add(Tuple.Create(sourceItem, targetItem)); - targetsWithId.Remove(targetItem); - - if (targetsWithId.Count == 0) - { - targetsById.Remove(sourceItemId); - } + newSourceItems.Add(sourceItem); + continue; } - else + + var targetItem = targetsWithId[0]; + + absentTargetItems.Remove(targetItem); + intersection.Add(Tuple.Create(sourceItem, targetItem)); + targetsWithId.Remove(targetItem); + + if (targetsWithId.Count == 0) { - newSourceItems.Add(sourceItem); + targetsById.Remove(sourceItemId); } } @@ -131,7 +126,8 @@ private static Dictionary> GetItemsById(IEnumerable } /// - /// Helper class for merging or updating collections. + /// Helper class for merging or updating collections. This class supports mapping and is not + /// intended to be used from your code. /// /// The type of object stored in the source collection. /// The type of object stored in the target collection. diff --git a/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs b/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs index b5dcbc7ee..d1d22f324 100644 --- a/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs +++ b/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs @@ -13,6 +13,9 @@ internal static class StringExpressionExtensions { + public static readonly Expression EmptyString = Expression.Field(null, typeof(string), "Empty"); + public static readonly Expression Underscore = "_".ToConstantExpression(); + private static readonly MethodInfo _stringJoinMethod; private static readonly MethodInfo[] _stringConcatMethods; @@ -71,10 +74,9 @@ public static Expression GetStringConcatCall(this IList expressions) return Expression.Call(null, concatMethod, expressions); } - var emptyString = Expression.Field(null, typeof(string), "Empty"); var newStringArray = Expression.NewArrayInit(typeof(string), expressions); - return Expression.Call(null, _stringJoinMethod, emptyString, newStringArray); + return Expression.Call(null, _stringJoinMethod, EmptyString, newStringArray); } private static void OptimiseForStringConcat(IList expressions) @@ -106,7 +108,10 @@ private static void OptimiseForStringConcat(IList expressions) currentNamePart = string.Empty; } - expressions.Insert(0, currentNamePart.ToConstantExpression()); + if (currentNamePart != string.Empty) + { + expressions.Insert(0, currentNamePart.ToConstantExpression()); + } } public static Expression GetFirstOrDefaultCall(this Expression stringAccess) diff --git a/AgileMapper/Extensions/PublicTypeExtensions.cs b/AgileMapper/Extensions/PublicTypeExtensions.cs index 3cb7df8d1..08406207a 100644 --- a/AgileMapper/Extensions/PublicTypeExtensions.cs +++ b/AgileMapper/Extensions/PublicTypeExtensions.cs @@ -24,11 +24,6 @@ public static bool IsSimple(this Type type) { type = type.GetNonNullableType(); - if (type == typeof(ValueType)) - { - return true; - } - if (type.GetTypeCode() != NetStandardTypeCode.Object) { return true; @@ -36,7 +31,8 @@ public static bool IsSimple(this Type type) if ((type == typeof(Guid)) || (type == typeof(TimeSpan)) || - (type == typeof(DateTimeOffset))) + (type == typeof(DateTimeOffset)) || + (type == typeof(ValueType))) { return true; } diff --git a/AgileMapper/MapperContextExtensions.cs b/AgileMapper/MapperContextExtensions.cs new file mode 100644 index 000000000..c4b58e663 --- /dev/null +++ b/AgileMapper/MapperContextExtensions.cs @@ -0,0 +1,41 @@ +namespace AgileObjects.AgileMapper +{ + using System; + using Caching; + using Extensions.Internal; + using Members; +#if NET35 + using Microsoft.Scripting.Ast; +#else + using System.Linq.Expressions; +#endif + + internal static class MapperContextExtensions + { + public static Expression GetIdentifierOrNull( + this MapperContext context, + Expression subject, + ICache cache = null) + { + var typeIdsCache = cache ?? context.Cache.CreateScoped(default(HashCodeComparer)); + + return typeIdsCache.GetOrAdd(TypeKey.ForTypeId(subject.Type), key => + { + var configuredIdentifier = + context.UserConfigurations.Identifiers.GetIdentifierOrNullFor(key.Type); + + if (configuredIdentifier != null) + { + return configuredIdentifier.ReplaceParameterWith(subject); + } + + var identifier = context.Naming.GetIdentifierOrNull(key.Type); + + return identifier?.GetAccess(subject); + }); + } + + public static Expression GetValueConversion(this MapperContext context, Expression value, Type targetType) + => context.ValueConverters.GetConversion(value, targetType); + } +} diff --git a/AgileMapper/MappingRuleSetCollection.cs b/AgileMapper/MappingRuleSetCollection.cs index 1b9dce7ec..8e5baa4da 100644 --- a/AgileMapper/MappingRuleSetCollection.cs +++ b/AgileMapper/MappingRuleSetCollection.cs @@ -16,7 +16,7 @@ internal class MappingRuleSetCollection private static readonly MappingRuleSet _createNew = new MappingRuleSet( Constants.CreateNew, - MappingRuleSetSettings.ForInMemoryMapping(), + MappingRuleSetSettings.ForInMemoryMapping(allowCloneEntityKeyMapping: true), default(CopySourceEnumerablePopulationStrategy), default(MapRepeatedCallRepeatMappingStrategy), DefaultMemberPopulationFactory.Instance, @@ -38,6 +38,7 @@ internal class MappingRuleSetCollection { UseMemberInitialisation = true, UseSingleRootMappingExpression = true, + AllowCloneEntityKeyMapping = true, GuardAccessTo = value => value.Type.IsComplex(), ExpressionIsSupported = value => value.CanBeProjected(), AllowEnumerableAssignment = true diff --git a/AgileMapper/MappingRuleSetSettings.cs b/AgileMapper/MappingRuleSetSettings.cs index be12f844c..d21b3775f 100644 --- a/AgileMapper/MappingRuleSetSettings.cs +++ b/AgileMapper/MappingRuleSetSettings.cs @@ -9,7 +9,9 @@ namespace AgileObjects.AgileMapper internal class MappingRuleSetSettings { - public static MappingRuleSetSettings ForInMemoryMapping(bool rootHasPopulatedTarget = false) + public static MappingRuleSetSettings ForInMemoryMapping( + bool rootHasPopulatedTarget = false, + bool allowCloneEntityKeyMapping = false) { return new MappingRuleSetSettings { @@ -18,6 +20,7 @@ public static MappingRuleSetSettings ForInMemoryMapping(bool rootHasPopulatedTar SourceElementsCouldBeNull = true, UseTryCatch = true, CheckDerivedSourceTypes = true, + AllowCloneEntityKeyMapping = allowCloneEntityKeyMapping, GuardAccessTo = value => true, ExpressionIsSupported = value => true, AllowObjectTracking = true, @@ -41,6 +44,8 @@ public static MappingRuleSetSettings ForInMemoryMapping(bool rootHasPopulatedTar public bool CheckDerivedSourceTypes { get; set; } + public bool AllowCloneEntityKeyMapping { get; set; } + public Func GuardAccessTo { get; set; } public Func ExpressionIsSupported { get; set; } diff --git a/AgileMapper/Members/ExpressionInfoFinder.cs b/AgileMapper/Members/ExpressionInfoFinder.cs index a2b06d3a4..ca5577b47 100644 --- a/AgileMapper/Members/ExpressionInfoFinder.cs +++ b/AgileMapper/Members/ExpressionInfoFinder.cs @@ -22,6 +22,11 @@ internal class ExpressionInfoFinder public static readonly ExpressionInfo EmptyExpressionInfo = new ExpressionInfo(null, Enumerable.EmptyArray); + public static ExpressionInfoFinder Default => + _default ?? (_default = new ExpressionInfoFinder(mappingDataObject: null)); + + private static ExpressionInfoFinder _default; + private readonly Expression _mappingDataObject; public ExpressionInfoFinder(Expression mappingDataObject) @@ -355,7 +360,8 @@ private bool GuardMemberAccess(Expression memberAccess) return false; } - if (memberAccess.Type.CannotBeNull() || !memberAccess.IsRootedIn(_mappingDataObject)) + if (memberAccess.Type.CannotBeNull() || + ((_mappingDataObject != null) && !memberAccess.IsRootedIn(_mappingDataObject))) { return false; } diff --git a/AgileMapper/Members/MemberIdentifierSet.cs b/AgileMapper/Members/MemberIdentifierSet.cs index f5f16e3fa..78fc97d18 100644 --- a/AgileMapper/Members/MemberIdentifierSet.cs +++ b/AgileMapper/Members/MemberIdentifierSet.cs @@ -45,7 +45,8 @@ private void ThrowIfIdentifierIsRedundant(Type type, LambdaExpression idMember) var defaultIdentifier = _mapperContext.Naming.GetIdentifierOrNull(type); - if (defaultIdentifier.Name != idMember.Body.GetMemberName()) + if ((defaultIdentifier == null) || + (defaultIdentifier.Name != idMember.Body.GetMemberName())) { return; } diff --git a/AgileMapper/Members/MemberMapperDataExtensions.cs b/AgileMapper/Members/MemberMapperDataExtensions.cs index 53c0a5d77..71637be75 100644 --- a/AgileMapper/Members/MemberMapperDataExtensions.cs +++ b/AgileMapper/Members/MemberMapperDataExtensions.cs @@ -134,8 +134,20 @@ public static ExpressionInfoFinder.ExpressionInfo GetExpressionInfoFor( Expression value, bool targetCanBeNull) { - return mapperData.RuleSet.Settings.GuardAccessTo(value) - ? mapperData.ExpressionInfoFinder.FindIn(value, targetCanBeNull) + return mapperData.RuleSet.GetExpressionInfoFor( + value, + mapperData.ExpressionInfoFinder, + targetCanBeNull); + } + + public static ExpressionInfoFinder.ExpressionInfo GetExpressionInfoFor( + this MappingRuleSet ruleSet, + Expression value, + ExpressionInfoFinder infoFinder = null, + bool targetCanBeNull = false) + { + return ruleSet.Settings?.GuardAccessTo(value) != false + ? (infoFinder ?? ExpressionInfoFinder.Default).FindIn(value, targetCanBeNull) : ExpressionInfoFinder.EmptyExpressionInfo; } @@ -210,16 +222,26 @@ public static bool TargetMemberIsUnmappable( return false; } - if ((mapperData.TargetType != mapperData.SourceType) && - targetMember.LeafMember.IsEntityId() && - !userConfigurations.MapEntityKeys(mapperData) && - configuredDataSourcesFactory.Invoke(mapperData).None()) + if (!targetMember.LeafMember.IsEntityId() || + userConfigurations.MapEntityKeys(mapperData) || + configuredDataSourcesFactory.Invoke(mapperData).Any()) { - reason = "Entity key member"; - return true; + return targetMember.IsUnmappable(out reason); + } + + // If we're here: + // 1. TargetMember is an Entity key + // 2. No configuration exists to allow Entity key Mapping + // 3. No configured data sources exist + + if (mapperData.RuleSet.Settings.AllowCloneEntityKeyMapping && + (mapperData.SourceType == mapperData.TargetType)) + { + return targetMember.IsUnmappable(out reason); } - return targetMember.IsUnmappable(out reason); + reason = "Entity key member"; + return true; } [DebuggerStepThrough] @@ -391,7 +413,7 @@ public static bool CanConvert(this IMemberMapperData mapperData, Type sourceType => mapperData.MapperContext.ValueConverters.CanConvert(sourceType, targetType); public static Expression GetValueConversion(this IMemberMapperData mapperData, Expression value, Type targetType) - => mapperData.MapperContext.ValueConverters.GetConversion(value, targetType); + => mapperData.MapperContext.GetValueConversion(value, targetType); public static Expression GetMappingCallbackOrNull( this IBasicMapperData basicData, diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs index 94960e288..a8fccb08a 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs @@ -179,8 +179,8 @@ public bool ElementsAreIdentifiable private bool DetermineIfElementsAreIdentifiable() { - if ((Context.SourceElementType == typeof(object)) || - (Context.TargetElementType == typeof(object))) + if ((Context.SourceElementType == typeof(string)) || + (Context.TargetElementType == typeof(string))) { return false; } @@ -190,9 +190,15 @@ private bool DetermineIfElementsAreIdentifiable() { return false; } + + if ((Context.SourceElementType == typeof(object)) || + (Context.TargetElementType == typeof(object))) + { + return false; + } var typeIdsCache = MapperData.MapperContext.Cache.CreateScoped(default(HashCodeComparer)); - var sourceElementId = GetIdentifierOrNull(Context.SourceElementType, _sourceElementParameter, MapperData, typeIdsCache); + var sourceElementId = MapperData.MapperContext.GetIdentifierOrNull(_sourceElementParameter, typeIdsCache); if (sourceElementId == null) { @@ -209,7 +215,7 @@ private bool DetermineIfElementsAreIdentifiable() } var targetElementParameter = Context.TargetElementType.GetOrCreateParameter(); - var targetElementId = GetIdentifierOrNull(Context.TargetElementType, targetElementParameter, MapperData, typeIdsCache); + var targetElementId = MapperData.MapperContext.GetIdentifierOrNull(targetElementParameter, typeIdsCache); if (targetElementId == null) { @@ -222,31 +228,6 @@ private bool DetermineIfElementsAreIdentifiable() return _targetElementIdLambda != null; } - private static Expression GetIdentifierOrNull( - Type type, - Expression parameter, - IMemberMapperData mapperData, - ICache cache) - { - return cache.GetOrAdd(TypeKey.ForTypeId(type), key => - { - var configuredIdentifier = - mapperData.MapperContext.UserConfigurations.Identifiers.GetIdentifierOrNullFor(key.Type); - - if (configuredIdentifier != null) - { - return configuredIdentifier.ReplaceParameterWith(parameter); - } - - var identifier = mapperData - .MapperContext - .Naming - .GetIdentifierOrNull(key.Type); - - return identifier?.GetAccess(parameter); - }); - } - private LambdaExpression GetSourceElementIdLambda( ParameterExpression sourceElement, Expression sourceElementId, @@ -394,7 +375,7 @@ private Expression GetTargetVariableValue() { nonNullTargetVariableValue = GetNonNullEnumerableTargetVariableValue(); } - else if (TargetTypeHelper.HasCollectionInterface && TargetTypeHelper.CouldBeReadOnly) + else if (TargetTypeHelper.HasCollectionInterface && TargetTypeHelper.CouldBeReadOnly()) { var isReadOnlyProperty = TargetTypeHelper .CollectionInterfaceType @@ -505,7 +486,7 @@ private Type GetNullTargetVariableType(Type nonNullTargetVariableType) } private bool TargetCouldBeUnusable() - => !MapperData.TargetMember.LeafMember.IsWriteable && TargetTypeHelper.CouldBeReadOnly; + => !MapperData.TargetMember.LeafMember.IsWriteable && TargetTypeHelper.CouldBeReadOnly(); #endregion diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs index 69da44386..710f3d84c 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs @@ -16,6 +16,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables internal class EnumerableTypeHelper { private bool? _isDictionary; + private bool? _couldBeReadOnly; private Type _listType; private Type _listInterfaceType; private Type _collectionType; @@ -48,8 +49,6 @@ public bool IsDictionary public bool IsCollection => EnumerableType.IsAssignableTo(CollectionType); - private bool IsHashSet => EnumerableType == HashSetType; - public bool IsReadOnlyCollection => EnumerableType == ReadOnlyCollectionType; public bool IsEnumerableInterface => EnumerableType == EnumerableInterfaceType; @@ -59,14 +58,31 @@ public bool IsDictionary #if FEATURE_ISET private bool HasSetInterface => EnumerableType.IsAssignableTo(SetInterfaceType); #else - private bool HasSetInterface => IsHashSet; + private bool HasSetInterface => EnumerableType == HashSetType; #endif public bool IsReadOnly => IsArray || IsReadOnlyCollection; public bool IsDeclaredReadOnly => IsReadOnly || IsEnumerableInterface || IsReadOnlyCollectionInterface(); - public bool CouldBeReadOnly => !(IsList || IsHashSet || IsCollection); + public bool CouldBeReadOnly() + { + if (_couldBeReadOnly.HasValue) + { + return _couldBeReadOnly.Value; + } + + if (EnumerableType.IsInterface()) + { + // If the declared Type is an interface it could have an 'Add' method + // while the underlying, implementing Type is actually readonly: + return (_couldBeReadOnly = true).Value; + } + + // If the declared Type declares an 'Add' method, assume it's not readonly; + // Array implements ICollection<>, but its Add method is implemented explicitly: + return (_couldBeReadOnly = EnumerableType.GetPublicInstanceMethods("Add").None()).Value; + } private bool IsReadOnlyCollectionInterface() { diff --git a/NuGet/AgileObjects.AgileMapper.1.2.0.nupkg b/NuGet/AgileObjects.AgileMapper.1.2.0.nupkg new file mode 100644 index 000000000..2cc077f88 Binary files /dev/null and b/NuGet/AgileObjects.AgileMapper.1.2.0.nupkg differ diff --git a/VersionInfo.cs b/VersionInfo.cs index bf454fe34..57cec91b0 100644 --- a/VersionInfo.cs +++ b/VersionInfo.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("1.1.0")] -[assembly: AssemblyFileVersion("1.1.0")] \ No newline at end of file +[assembly: AssemblyVersion("1.2.0")] +[assembly: AssemblyFileVersion("1.2.0")] \ No newline at end of file diff --git a/common.props b/common.props index 7222de1ea..9f00eb229 100644 --- a/common.props +++ b/common.props @@ -9,9 +9,9 @@ true git https://github.com/AgileObjects/AgileMapper - 1.1.0 - 1.1.0.0 - 1.1.0.0 + 1.2.0 + 1.2.0.0 + 1.2.0.0 \ No newline at end of file