diff --git a/AgileMapper.UnitTests.MoreTestClasses/AgileMapper.UnitTests.MoreTestClasses.csproj b/AgileMapper.UnitTests.MoreTestClasses/AgileMapper.UnitTests.MoreTestClasses.csproj index c8e2aeebf..a4b214a7a 100644 --- a/AgileMapper.UnitTests.MoreTestClasses/AgileMapper.UnitTests.MoreTestClasses.csproj +++ b/AgileMapper.UnitTests.MoreTestClasses/AgileMapper.UnitTests.MoreTestClasses.csproj @@ -7,4 +7,8 @@ AgileObjects.AgileMapper.UnitTests.MoreTestClasses + + + + diff --git a/AgileMapper.UnitTests.MoreTestClasses/MappingExtensions.cs b/AgileMapper.UnitTests.MoreTestClasses/MappingExtensions.cs new file mode 100644 index 000000000..c81603064 --- /dev/null +++ b/AgileMapper.UnitTests.MoreTestClasses/MappingExtensions.cs @@ -0,0 +1,26 @@ +namespace AgileObjects.AgileMapper.UnitTests.MoreTestClasses +{ + using System; + using System.Collections.Generic; + using System.Linq; + + public static class MappingExtensions + { + public static object RootMapperCountShouldBeOne(this IMapper mapper) + { + return RootMapperCountShouldBe(mapper, 1).First(); + } + + public static ICollection RootMapperCountShouldBe(this IMapper mapper, int expected) + { + var rootMappers = ((Mapper)mapper).Context.ObjectMapperFactory.RootMappers.ToArray(); + + if (rootMappers.Length == expected) + { + return rootMappers; + } + + throw new Exception($"Expected {expected} mappers, got {rootMappers.Length}"); + } + } +} diff --git a/AgileMapper.UnitTests.MoreTestClasses/packages.config b/AgileMapper.UnitTests.MoreTestClasses/packages.config new file mode 100644 index 000000000..ff08b12e1 --- /dev/null +++ b/AgileMapper.UnitTests.MoreTestClasses/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.NetCore2/AgileMapper.UnitTests.NetCore2.csproj b/AgileMapper.UnitTests.NetCore2/AgileMapper.UnitTests.NetCore2.csproj index 505e9c2a5..26a124ab8 100644 --- a/AgileMapper.UnitTests.NetCore2/AgileMapper.UnitTests.NetCore2.csproj +++ b/AgileMapper.UnitTests.NetCore2/AgileMapper.UnitTests.NetCore2.csproj @@ -7,7 +7,7 @@ AgileObjects.AgileMapper.UnitTests.NetCore2 AgileObjects.AgileMapper.UnitTests.NetCore2 true - 2.0.0 + 2.0.5 false false false diff --git a/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj b/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj index cfd361d46..d09b43f14 100644 --- a/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj +++ b/AgileMapper.UnitTests.NonParallel/AgileMapper.UnitTests.NonParallel.csproj @@ -1,5 +1,5 @@  - + @@ -14,6 +14,9 @@ AgileObjects.AgileMapper.UnitTests.NonParallel v4.6.1 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + False + UnitTest @@ -100,9 +103,6 @@ - - - diff --git a/AgileMapper.UnitTests.NonParallel/packages.config b/AgileMapper.UnitTests.NonParallel/packages.config index 7250c09dd..d1e54e0c5 100644 --- a/AgileMapper.UnitTests.NonParallel/packages.config +++ b/AgileMapper.UnitTests.NonParallel/packages.config @@ -3,7 +3,7 @@ - + diff --git a/AgileMapper.UnitTests.Orms.EFCore2.NetCore2/AgileMapper.UnitTests.Orms.EfCore2.NetCore2.csproj b/AgileMapper.UnitTests.Orms.EFCore2.NetCore2/AgileMapper.UnitTests.Orms.EfCore2.NetCore2.csproj new file mode 100644 index 000000000..8dcd8dc39 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EFCore2.NetCore2/AgileMapper.UnitTests.Orms.EfCore2.NetCore2.csproj @@ -0,0 +1,71 @@ + + + + + netcoreapp2.0 + true + AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.NetCore2 + AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.NetCore2 + true + 2.0.5 + false + false + false + false + false + false + false + AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.NetCore2 + + + + TRACE;DEBUG;NETCOREAPP2_0;NET_STANDARD + + + + TRACE;RELEASE;NETCOREAPP2_0;NET_STANDARD + + + + + + + + + + + + + + + + + + + + + + + + + %(RecursiveDir)%(Filename)%(Extension) + + + + + + + + + + + + + + + + + + + + diff --git a/AgileMapper.UnitTests.Orms.Ef5.LocalDb/AgileMapper.UnitTests.Orms.Ef5.LocalDb.csproj b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/AgileMapper.UnitTests.Orms.Ef5.LocalDb.csproj new file mode 100644 index 000000000..66af87562 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/AgileMapper.UnitTests.Orms.Ef5.LocalDb.csproj @@ -0,0 +1,116 @@ + + + + + + Debug + AnyCPU + {48B855A9-F42C-4D2A-9549-97ED27D59336} + Library + Properties + AgileObjects.AgileMapper.UnitTests.Orms.Ef5.LocalDb + AgileObjects.AgileMapper.UnitTests.Orms.Ef5.LocalDb + v4.6.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + False + UnitTest + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + true + + + ..\AgileMapper.snk + + + + ..\packages\AgileObjects.NetStandardPolyfills.1.3.0\lib\net40\AgileObjects.NetStandardPolyfills.dll + + + ..\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll + + + + + + + + + + + + ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll + + + ..\packages\xunit.assert.2.3.1\lib\netstandard1.1\xunit.assert.dll + + + ..\packages\xunit.extensibility.core.2.3.1\lib\netstandard1.1\xunit.core.dll + + + ..\packages\xunit.extensibility.execution.2.3.1\lib\net452\xunit.execution.desktop.dll + + + + + CommonAssemblyInfo.cs + + + VersionInfo.cs + + + + + + + + + + + + + {d87103fd-3851-4724-bd8f-9cef19c8f193} + AgileMapper.UnitTests.Orms.Ef5 + + + {66522d44-19f5-4af5-9d43-483a3cd6f958} + AgileMapper.UnitTests.Orms + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5.LocalDb/App.config b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/App.config new file mode 100644 index 000000000..da5fa9f0e --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/App.config @@ -0,0 +1,22 @@ + + + + +
+ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Configuration/WhenConfiguringDataSources.cs b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Configuration/WhenConfiguringDataSources.cs new file mode 100644 index 000000000..31f8237f2 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Configuration/WhenConfiguringDataSources.cs @@ -0,0 +1,24 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.LocalDb.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Orms.Infrastructure; + using Xunit; + + public class WhenConfiguringDataSources : WhenConfiguringDataSources + { + public WhenConfiguringDataSources(LocalDbTestContext context) + : base(context) + { + } + + // Executed in LocalDb because the configured conditions require string conversions + + [Fact] + public Task ShouldApplyAConfiguredMember() => DoShouldApplyAConfiguredMember(); + + [Fact] + public Task ShouldApplyMultipleConfiguredMembers() => DoShouldApplyMultipleConfiguredMembers(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Configuration/WhenConfiguringStringFormatting.cs b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Configuration/WhenConfiguringStringFormatting.cs new file mode 100644 index 000000000..041e8259e --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Configuration/WhenConfiguringStringFormatting.cs @@ -0,0 +1,26 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.LocalDb.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Orms.Infrastructure; + using Xunit; + + public class WhenConfiguringStringFormatting : WhenConfiguringStringFormatting + { + public WhenConfiguringStringFormatting(LocalDbTestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldFormatDateTimes() + => DoShouldFormatDateTimes(d => d.ToString("yyyy-%M-%d %H:%m:%s")); + + [Fact] + public Task ShouldFormatDecimals() => DoShouldFormatDecimals(d => d.ToString(".000000")); + + [Fact] + public Task ShouldFormatDoubles() => DoShouldFormatDoubles(d => d.ToString(".000000")); + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Infrastructure/Ef5TestLocalDbContext.cs b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Infrastructure/Ef5TestLocalDbContext.cs new file mode 100644 index 000000000..c37d5210f --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Infrastructure/Ef5TestLocalDbContext.cs @@ -0,0 +1,19 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.LocalDb.Infrastructure +{ + using System.Data.SqlClient; + using Ef5.Infrastructure; + using Orms; + using Orms.Infrastructure; + + public class Ef5TestLocalDbContext : Ef5TestDbContext, ITestLocalDbContext + { + public Ef5TestLocalDbContext() + : base(new SqlConnection(TestConstants.GetLocalDbConnectionString())) + { + } + + void ITestLocalDbContext.CreateDatabase() => Database.Create(); + + void ITestLocalDbContext.DeleteDatabase() => Database.Delete(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Infrastructure/Ef5TestLocalDbDefinition.cs b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Infrastructure/Ef5TestLocalDbDefinition.cs new file mode 100644 index 000000000..476fc16a9 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Infrastructure/Ef5TestLocalDbDefinition.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.LocalDb.Infrastructure +{ + using Orms; + using Orms.Infrastructure; + using Xunit; + + [CollectionDefinition(TestConstants.OrmCollectionName)] + public class Ef5TestLocalDbDefinition : ICollectionFixture> + { + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Properties/AssemblyInfo.cs b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..1d6e29c10 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("AgileObjects.AgileMapper.UnitTests.Orms.Ef5.LocalDb")] +[assembly: AssemblyDescription("AgileObjects.AgileMapper.UnitTests.Orms.Ef5.LocalDb")] + +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5.LocalDb/SimpleTypeConversion/WhenConvertingToDateTimes.cs b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/SimpleTypeConversion/WhenConvertingToDateTimes.cs new file mode 100644 index 000000000..c486e243f --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/SimpleTypeConversion/WhenConvertingToDateTimes.cs @@ -0,0 +1,31 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.LocalDb.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToDateTimes : + WhenConvertingToDateTimes, + IStringConverterTest, + IStringConversionValidatorTest + { + public WhenConvertingToDateTimes(LocalDbTestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAParseableString() + => RunShouldProjectAParseableStringToADateTime(); + + [Fact] + public Task ShouldProjectANullString() + => RunShouldProjectANullStringToADateTime(); + + [Fact] + public Task ShouldProjectAnUnparseableString() + => RunShouldProjectAnUnparseableStringToADateTime(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5.LocalDb/SimpleTypeConversion/WhenConvertingToEnums.cs b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/SimpleTypeConversion/WhenConvertingToEnums.cs new file mode 100644 index 000000000..779292894 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/SimpleTypeConversion/WhenConvertingToEnums.cs @@ -0,0 +1,14 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.LocalDb.SimpleTypeConversion +{ + using Infrastructure; + using Orms.Infrastructure; + using Orms.SimpleTypeConversion; + + public class WhenConvertingToEnums : WhenConvertingToEnums + { + public WhenConvertingToEnums(LocalDbTestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef5.LocalDb/SimpleTypeConversion/WhenConvertingToStrings.cs b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/SimpleTypeConversion/WhenConvertingToStrings.cs new file mode 100644 index 000000000..d65585a03 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/SimpleTypeConversion/WhenConvertingToStrings.cs @@ -0,0 +1,28 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.LocalDb.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToStrings : WhenConvertingToStrings + { + public WhenConvertingToStrings(LocalDbTestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectADecimalToAString() + => DoShouldProjectADecimalToAString(d => d.ToString("0.00") + "0000"); + + [Fact] + public Task ShouldProjectADoubleToAString() + => DoShouldProjectADoubleToAString(d => d.ToString("0.00") + "0000"); + + [Fact] + public Task ShouldProjectADateTimeToAString() + => DoShouldProjectADateTimeToAString(d => d.ToString("yyyy-%M-%d %H:%m:%s")); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5.LocalDb/packages.config b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/packages.config new file mode 100644 index 000000000..675a24a00 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5.LocalDb/packages.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ 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 new file mode 100644 index 000000000..3817966ae --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/AgileMapper.UnitTests.Orms.Ef5.csproj @@ -0,0 +1,143 @@ + + + + + + + Debug + AnyCPU + {D87103FD-3851-4724-BD8F-9CEF19C8F193} + Library + Properties + AgileObjects.AgileMapper.UnitTests.Orms.Ef5 + AgileObjects.AgileMapper.UnitTests.Orms.Ef5 + v4.6.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + False + UnitTest + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + true + + + ..\AgileMapper.snk + + + + ..\packages\Effort.1.3.0\lib\net45\Effort.dll + + + ..\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll + + + ..\packages\NMemory.1.1.2\lib\net45\NMemory.dll + + + + + + + + + + + + + ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll + + + ..\packages\xunit.assert.2.3.1\lib\netstandard1.1\xunit.assert.dll + + + ..\packages\xunit.extensibility.core.2.3.1\lib\netstandard1.1\xunit.core.dll + + + ..\packages\xunit.extensibility.execution.2.3.1\lib\net452\xunit.execution.desktop.dll + + + + + Properties\CommonAssemblyInfo.cs + + + Properties\VersionInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {66522d44-19f5-4af5-9d43-483a3cd6f958} + AgileMapper.UnitTests.Orms + + + {46d95c53-b4cb-4ee7-9573-5d3ef96099c0} + AgileMapper + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/App.config b/AgileMapper.UnitTests.Orms.Ef5/App.config new file mode 100644 index 000000000..3d5ee2c10 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/App.config @@ -0,0 +1,25 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs b/AgileMapper.UnitTests.Orms.Ef5/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs new file mode 100644 index 000000000..389e230e1 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs @@ -0,0 +1,20 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Configuration.Inline +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration.Inline; + using Xunit; + + public class WhenConfiguringConstructorDataSourcesInline + : WhenConfiguringConstructorDataSourcesInline + { + public WhenConfiguringConstructorDataSourcesInline(InMemoryEf5TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorApplyingAConfiguredConstantByParameterTypeInline() + => RunShouldErrorApplyingAConfiguredConstantByParameterTypeInline(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/Configuration/Inline/WhenConfiguringDataSourcesInline.cs b/AgileMapper.UnitTests.Orms.Ef5/Configuration/Inline/WhenConfiguringDataSourcesInline.cs new file mode 100644 index 000000000..8e243be65 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Configuration/Inline/WhenConfiguringDataSourcesInline.cs @@ -0,0 +1,14 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Configuration.Inline +{ + using Infrastructure; + using Orms.Configuration.Inline; + + public class WhenConfiguringDataSourcesInline + : WhenConfiguringDataSourcesInline + { + public WhenConfiguringDataSourcesInline(InMemoryEf5TestContext context) + : base(context) + { + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringConstructorDataSources.cs b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringConstructorDataSources.cs new file mode 100644 index 000000000..bc95bc683 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringConstructorDataSources.cs @@ -0,0 +1,23 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringConstructorDataSources : WhenConfiguringConstructorDataSources + { + public WhenConfiguringConstructorDataSources(InMemoryEf5TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorApplyingAConfiguredConstantByParameterType() + => RunShouldErrorApplyingAConfiguredConstantByParameterType(); + + [Fact] + public Task ShouldErrorApplyingAConfiguredExpressionByParameterName() + => RunShouldErrorApplyingAConfiguredExpressionByParameterName(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringDataSources.cs b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringDataSources.cs new file mode 100644 index 000000000..7dd282ace --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringDataSources.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Configuration +{ + using Infrastructure; + using Orms.Configuration; + + public class WhenConfiguringDataSources : WhenConfiguringDataSources + { + public WhenConfiguringDataSources(InMemoryEf5TestContext context) + : base(context) + { + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringDerivedTypes.cs b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringDerivedTypes.cs new file mode 100644 index 000000000..ab0a2ecc6 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringDerivedTypes.cs @@ -0,0 +1,25 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringDerivedTypes : WhenConfiguringDerivedTypes + { + public WhenConfiguringDerivedTypes(InMemoryEf5TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingToConfiguredDerivedTypes() => RunShouldErrorProjectingToConfiguredDerivedTypes(); + + [Fact] + public Task ShouldErrorProjectingToAFallbackDerivedType() => RunShouldErrorProjectingToAFallbackDerivedType(); + + [Fact] + public Task ShouldErrorAttemptingToNotApplyDerivedSourceTypePairing() + => RunShouldErrorAttemptingToNotApplyDerivedSourceTypePairing(); + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringEnumMapping.cs b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringEnumMapping.cs new file mode 100644 index 000000000..de013d1b4 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringEnumMapping.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Configuration +{ + using Infrastructure; + using Orms.Configuration; + + public class WhenConfiguringEnumMapping : WhenConfiguringEnumMapping + { + public WhenConfiguringEnumMapping(InMemoryEf5TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringObjectCreation.cs b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringObjectCreation.cs new file mode 100644 index 000000000..19992aaa6 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenConfiguringObjectCreation.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringObjectCreation : WhenConfiguringObjectCreation + { + public WhenConfiguringObjectCreation(InMemoryEf5TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorUsingAConditionalObjectFactory() => RunShouldErrorUsingAConditionalObjectFactory(); + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenIgnoringMembers.cs b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenIgnoringMembers.cs new file mode 100644 index 000000000..810d93eec --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenIgnoringMembers.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Configuration +{ + using Infrastructure; + using Orms.Configuration; + + public class WhenIgnoringMembers : WhenIgnoringMembers + { + public WhenIgnoringMembers(InMemoryEf5TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenMappingToNull.cs b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenMappingToNull.cs new file mode 100644 index 000000000..aea186bf9 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Configuration/WhenMappingToNull.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenMappingToNull : WhenMappingToNull + { + public WhenMappingToNull(InMemoryEf5TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorApplyingAUserConfiguration() => RunShouldErrorApplyingAUserConfiguration(); + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef5/Infrastructure/Ef5DbSetWrapper.cs b/AgileMapper.UnitTests.Orms.Ef5/Infrastructure/Ef5DbSetWrapper.cs new file mode 100644 index 000000000..74eb8f10c --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Infrastructure/Ef5DbSetWrapper.cs @@ -0,0 +1,49 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Infrastructure +{ + using System; + using System.Data.Entity; + using System.Linq; + using System.Linq.Expressions; + using System.Threading.Tasks; + using Orms.Infrastructure; + + public class Ef5DbSetWrapper : DbSetWrapperBase + where TEntity : class + { + private readonly DbSet _dbSet; + + public Ef5DbSetWrapper(DbContext context) + : base(context.Set()) + { + _dbSet = context.Set(); + } + + public override void Include(Expression> navigationPropertyPath) + => _dbSet.Include(navigationPropertyPath); + + public override Task Add(TEntity itemToAdd) + { + _dbSet.Add(itemToAdd); + + return Task.CompletedTask; + } + + public override Task AddRange(TEntity[] itemsToAdd) + { + foreach (var entity in itemsToAdd) + { + _dbSet.Add(entity); + } + + return Task.CompletedTask; + } + + public override void Clear() + { + foreach (var entity in _dbSet.ToArray()) + { + _dbSet.Remove(entity); + } + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/Infrastructure/Ef5TestDbContext.cs b/AgileMapper.UnitTests.Orms.Ef5/Infrastructure/Ef5TestDbContext.cs new file mode 100644 index 000000000..6c1b7f9c3 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Infrastructure/Ef5TestDbContext.cs @@ -0,0 +1,139 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Infrastructure +{ + using System.Data.Common; + using System.Data.Entity; + using System.Threading.Tasks; + using Effort; + using Orms.Infrastructure; + using TestClasses; + + public class Ef5TestDbContext : DbContext, ITestDbContext + { + public Ef5TestDbContext() + : this(DbConnectionFactory.CreateTransient()) + { + } + + protected Ef5TestDbContext(DbConnection dbConnection) + : base(dbConnection, true) + { + } + + public DbSet Animals { get; set; } + + public DbSet Shapes { get; set; } + + public DbSet Companies { get; set; } + + public DbSet Employees { get; set; } + + public DbSet Categories { get; set; } + + public DbSet Products { get; set; } + + public DbSet Accounts { get; set; } + + public DbSet Persons { get; set; } + + public DbSet
Addresses { get; set; } + + public DbSet Rotas { get; set; } + + public DbSet RotaEntries { get; set; } + + public DbSet Orders { get; set; } + + public DbSet OrderItems { get; set; } + + public DbSet BoolItems { get; set; } + + public DbSet ByteItems { get; set; } + + public DbSet ShortItems { get; set; } + + public DbSet IntItems { get; set; } + + public DbSet LongItems { get; set; } + + public DbSet DecimalItems { get; set; } + + public DbSet DoubleItems { get; set; } + + public DbSet DateTimeItems { get; set; } + + public DbSet StringItems { get; set; } + + public DbSet TitleItems { get; set; } + + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasMany(c => c.SubCategories) + .WithOptional(c => c.ParentCategory) + .HasForeignKey(c => c.ParentCategoryId); + + modelBuilder.Entity() + .HasKey(aa => new { aa.AccountId, aa.AddressId }); + + base.OnModelCreating(modelBuilder); + } + + #region ITestDbContext Members + + IDbSetWrapper ITestDbContext.Animals => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Shapes => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Companies => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Employees => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Categories => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Products => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Accounts => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Persons => new Ef5DbSetWrapper(this); + + IDbSetWrapper
ITestDbContext.Addresses => new Ef5DbSetWrapper
(this); + + IDbSetWrapper ITestDbContext.Rotas => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.RotaEntries => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Orders => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.OrderItems => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.BoolItems => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.ByteItems => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.ShortItems => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.IntItems => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.LongItems => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.DecimalItems => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.DoubleItems => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.DateTimeItems => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.StringItems => new Ef5DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.TitleItems => new Ef5DbSetWrapper(this); + + Task ITestDbContext.SaveChanges() + { + SaveChanges(); + + return Task.CompletedTask; + } + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/Infrastructure/InMemoryEf5TestContext.cs b/AgileMapper.UnitTests.Orms.Ef5/Infrastructure/InMemoryEf5TestContext.cs new file mode 100644 index 000000000..fa834fc6b --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Infrastructure/InMemoryEf5TestContext.cs @@ -0,0 +1,8 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Infrastructure +{ + using Orms.Infrastructure; + + public class InMemoryEf5TestContext : InMemoryOrmTestContext + { + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/Infrastructure/InMemoryEf5TestDefinition.cs b/AgileMapper.UnitTests.Orms.Ef5/Infrastructure/InMemoryEf5TestDefinition.cs new file mode 100644 index 000000000..cc28a6963 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Infrastructure/InMemoryEf5TestDefinition.cs @@ -0,0 +1,10 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Infrastructure +{ + using Orms; + using Xunit; + + [CollectionDefinition(TestConstants.OrmCollectionName)] + public class InMemoryEf5TestDefinition : ICollectionFixture + { + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/Properties/AssemblyInfo.cs b/AgileMapper.UnitTests.Orms.Ef5/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..34dc22ba0 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("AgileObjects.AgileMapper.UnitTests.Ef6")] +[assembly: AssemblyDescription("AgileObjects.AgileMapper.UnitTests.Ef6")] + +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/Recursion/WhenProjectingCircularReferences.cs b/AgileMapper.UnitTests.Orms.Ef5/Recursion/WhenProjectingCircularReferences.cs new file mode 100644 index 000000000..08108db1d --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/Recursion/WhenProjectingCircularReferences.cs @@ -0,0 +1,21 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.Recursion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Recursion; + using Xunit; + + public class WhenProjectingCircularReferences : + WhenProjectingCircularReferences, + IOneToManyRecursionProjectionFailureTest + { + public WhenProjectingCircularReferences(InMemoryEf5TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingAOneToManyRelationshipToZeroethRecursionDepth() + => DoShouldErrorProjectingAOneToManyRelationshipToZeroethRecursionDepth(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToBools.cs b/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToBools.cs new file mode 100644 index 000000000..ca106f8c6 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToBools.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.SimpleTypeConversion +{ + using Infrastructure; + using Orms.SimpleTypeConversion; + + public class WhenConvertingToBools : WhenConvertingToBools + { + public WhenConvertingToBools(InMemoryEf5TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToDateTimes.cs b/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToDateTimes.cs new file mode 100644 index 000000000..042e9b456 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToDateTimes.cs @@ -0,0 +1,30 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToDateTimes : + WhenConvertingToDateTimes, + IStringConversionFailureTest, + IStringConversionValidationFailureTest + { + public WhenConvertingToDateTimes(InMemoryEf5TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingAParseableString() + => RunShouldErrorProjectingAParseableStringToADateTime(); + + [Fact] + public Task ShouldErrorProjectingANullString() + => RunShouldErrorProjectingANullStringToADateTime(); + + [Fact] + public Task ShouldErrorProjectingAnUnparseableString() + => RunShouldErrorProjectingAnUnparseableStringToADateTime(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToDoubles.cs b/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToDoubles.cs new file mode 100644 index 000000000..c7981d37a --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToDoubles.cs @@ -0,0 +1,31 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToDoubles : + WhenConvertingToDoubles, + IStringConversionFailureTest, + IStringConversionValidationFailureTest + + { + public WhenConvertingToDoubles(InMemoryEf5TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingAParseableString() + => RunShouldErrorProjectingAParseableStringToADouble(); + + [Fact] + public Task ShouldErrorProjectingANullString() + => RunShouldErrorProjectingANullStringToADouble(); + + [Fact] + public Task ShouldErrorProjectingAnUnparseableString() + => RunShouldErrorProjectingAnUnparseableStringToADouble(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToGuids.cs b/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToGuids.cs new file mode 100644 index 000000000..315bfc799 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToGuids.cs @@ -0,0 +1,25 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToGuids : + WhenConvertingToGuids, + IStringConversionFailureTest + { + public WhenConvertingToGuids(InMemoryEf5TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingAParseableString() + => RunShouldErrorProjectingAParseableStringToAGuid(); + + [Fact] + public Task ShouldErrorProjectingANullString() + => RunShouldErrorProjectingANullStringToAGuid(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToInts.cs b/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToInts.cs new file mode 100644 index 000000000..1e0464066 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/SimpleTypeConversion/WhenConvertingToInts.cs @@ -0,0 +1,31 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToInts : + WhenConvertingToInts, + IStringConversionFailureTest, + IStringConversionValidationFailureTest + + { + public WhenConvertingToInts(InMemoryEf5TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingAParseableString() + => RunShouldErrorProjectingAParseableStringToAnInt(); + + [Fact] + public Task ShouldErrorProjectingANullString() + => RunShouldErrorProjectingANullStringToAnInt(); + + [Fact] + public Task ShouldErrorProjectingAnUnparseableString() + => RunShouldErrorProjectingAnUnparseableStringToAnInt(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/WhenProjectingFlatTypes.cs b/AgileMapper.UnitTests.Orms.Ef5/WhenProjectingFlatTypes.cs new file mode 100644 index 000000000..fa1abb5cd --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/WhenProjectingFlatTypes.cs @@ -0,0 +1,19 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5 +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms; + using Xunit; + + public class WhenProjectingFlatTypes : WhenProjectingFlatTypes + { + public WhenProjectingFlatTypes(InMemoryEf5TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingStructCtorParameters() + => RunShouldErrorProjectingStructCtorParameters(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/WhenProjectingToComplexTypeMembers.cs b/AgileMapper.UnitTests.Orms.Ef5/WhenProjectingToComplexTypeMembers.cs new file mode 100644 index 000000000..1a1ab4408 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/WhenProjectingToComplexTypeMembers.cs @@ -0,0 +1,15 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5 +{ + using Infrastructure; + using Orms; + + public class WhenProjectingToComplexTypeMembers : WhenProjectingToComplexTypeMembers + { + public WhenProjectingToComplexTypeMembers(InMemoryEf5TestContext context) + : base(context) + { + } + + public override bool QueryProviderNonEntityNullConstants => false; + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/WhenProjectingToEnumerableMembers.cs b/AgileMapper.UnitTests.Orms.Ef5/WhenProjectingToEnumerableMembers.cs new file mode 100644 index 000000000..5ad85f04f --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/WhenProjectingToEnumerableMembers.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5 +{ + using System.Threading.Tasks; + using Infrastructure; + using Xunit; + + public class WhenProjectingToEnumerableMembers : WhenProjectingToEnumerableMembers + { + public WhenProjectingToEnumerableMembers(InMemoryEf5TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingToAComplexTypeCollectionMember() + => RunShouldErrorProjectingToAComplexTypeCollectionMember(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef5/WhenProjectingToFlatTypes.cs b/AgileMapper.UnitTests.Orms.Ef5/WhenProjectingToFlatTypes.cs new file mode 100644 index 000000000..3358a6c4c --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/WhenProjectingToFlatTypes.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5 +{ + using Infrastructure; + + public class WhenProjectingToFlatTypes : WhenProjectingToFlatTypes + { + public WhenProjectingToFlatTypes(InMemoryEf5TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef5/WhenValidatingProjections.cs b/AgileMapper.UnitTests.Orms.Ef5/WhenValidatingProjections.cs new file mode 100644 index 000000000..567452973 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/WhenValidatingProjections.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5 +{ + using Infrastructure; + + public class WhenValidatingProjections : WhenValidatingProjections + { + public WhenValidatingProjections(InMemoryEf5TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef5/WhenViewingProjectionPlans.cs b/AgileMapper.UnitTests.Orms.Ef5/WhenViewingProjectionPlans.cs new file mode 100644 index 000000000..39396c1cb --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/WhenViewingProjectionPlans.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef5 +{ + using Infrastructure; + + public class WhenViewingProjectionPlans : WhenViewingProjectionPlans + { + public WhenViewingProjectionPlans(InMemoryEf5TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef5/packages.config b/AgileMapper.UnitTests.Orms.Ef5/packages.config new file mode 100644 index 000000000..52c92f5ea --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef5/packages.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6.LocalDb/AgileMapper.UnitTests.Orms.Ef6.LocalDb.csproj b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/AgileMapper.UnitTests.Orms.Ef6.LocalDb.csproj new file mode 100644 index 000000000..ae3b39de6 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/AgileMapper.UnitTests.Orms.Ef6.LocalDb.csproj @@ -0,0 +1,115 @@ + + + + + + Debug + AnyCPU + {B75D1A61-006A-4951-82FE-A2943296A872} + Library + Properties + AgileObjects.AgileMapper.UnitTests.Orms.Ef6.LocalDb + AgileObjects.AgileMapper.UnitTests.Orms.Ef6.LocalDb + v4.6.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + False + UnitTest + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + true + + + ..\AgileMapper.snk + + + + ..\packages\AgileObjects.NetStandardPolyfills.1.3.0\lib\net40\AgileObjects.NetStandardPolyfills.dll + + + ..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + + + ..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + + + + + + + + + + + ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll + + + ..\packages\xunit.assert.2.3.1\lib\netstandard1.1\xunit.assert.dll + + + ..\packages\xunit.extensibility.core.2.3.1\lib\netstandard1.1\xunit.core.dll + + + ..\packages\xunit.extensibility.execution.2.3.1\lib\net452\xunit.execution.desktop.dll + + + + + CommonAssemblyInfo.cs + + + VersionInfo.cs + + + + + + + + + + {63b8975d-0cde-48f5-8ca9-8af8fe729610} + AgileMapper.UnitTests.Orms.Ef6 + + + {66522d44-19f5-4af5-9d43-483a3cd6f958} + AgileMapper.UnitTests.Orms + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6.LocalDb/App.config b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/App.config new file mode 100644 index 000000000..a653955ca --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/App.config @@ -0,0 +1,17 @@ + + + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6.LocalDb/Infrastructure/Ef6TestLocalDbContext.cs b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/Infrastructure/Ef6TestLocalDbContext.cs new file mode 100644 index 000000000..fde6e1b82 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/Infrastructure/Ef6TestLocalDbContext.cs @@ -0,0 +1,19 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.LocalDb.Infrastructure +{ + using System.Data.SqlClient; + using Ef6.Infrastructure; + using Orms; + using Orms.Infrastructure; + + public class Ef6TestLocalDbContext : Ef6TestDbContext, ITestLocalDbContext + { + public Ef6TestLocalDbContext() + : base(new SqlConnection(TestConstants.GetLocalDbConnectionString())) + { + } + + void ITestLocalDbContext.CreateDatabase() => Database.Create(); + + void ITestLocalDbContext.DeleteDatabase() => Database.Delete(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6.LocalDb/Infrastructure/Ef6TestLocalDbDefinition.cs b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/Infrastructure/Ef6TestLocalDbDefinition.cs new file mode 100644 index 000000000..92ed7610c --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/Infrastructure/Ef6TestLocalDbDefinition.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.LocalDb.Infrastructure +{ + using Orms; + using Orms.Infrastructure; + using Xunit; + + [CollectionDefinition(TestConstants.OrmCollectionName)] + public class Ef6TestLocalDbDefinition : ICollectionFixture> + { + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6.LocalDb/Properties/AssemblyInfo.cs b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..7cad80571 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("AgileObjects.AgileMapper.UnitTests.Orms.Ef6.LocalDb")] +[assembly: AssemblyDescription("AgileObjects.AgileMapper.UnitTests.Orms.Ef6.LocalDb")] + +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6.LocalDb/SimpleTypeConversion/WhenConvertingToDateTimes.cs b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/SimpleTypeConversion/WhenConvertingToDateTimes.cs new file mode 100644 index 000000000..628cebe64 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/SimpleTypeConversion/WhenConvertingToDateTimes.cs @@ -0,0 +1,31 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.LocalDb.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToDateTimes : + WhenConvertingToDateTimes, + IStringConverterTest, + IStringConversionValidatorTest + { + public WhenConvertingToDateTimes(LocalDbTestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAParseableString() + => RunShouldProjectAParseableStringToADateTime(); + + [Fact] + public Task ShouldProjectANullString() + => RunShouldProjectANullStringToADateTime(); + + [Fact] + public Task ShouldProjectAnUnparseableString() + => RunShouldProjectAnUnparseableStringToADateTime(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6.LocalDb/SimpleTypeConversion/WhenConvertingToEnums.cs b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/SimpleTypeConversion/WhenConvertingToEnums.cs new file mode 100644 index 000000000..2cdc0fd75 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/SimpleTypeConversion/WhenConvertingToEnums.cs @@ -0,0 +1,14 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.LocalDb.SimpleTypeConversion +{ + using Infrastructure; + using Orms.Infrastructure; + using Orms.SimpleTypeConversion; + + public class WhenConvertingToEnums : WhenConvertingToEnums + { + public WhenConvertingToEnums(LocalDbTestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef6.LocalDb/packages.config b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/packages.config new file mode 100644 index 000000000..8b1817134 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6.LocalDb/packages.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ 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 new file mode 100644 index 000000000..e52983991 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/AgileMapper.UnitTests.Orms.Ef6.csproj @@ -0,0 +1,147 @@ + + + + + + + Debug + AnyCPU + {63B8975D-0CDE-48F5-8CA9-8AF8FE729610} + Library + Properties + AgileObjects.AgileMapper.UnitTests.Orms.Ef6 + AgileObjects.AgileMapper.UnitTests.Orms.Ef6 + v4.6.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + False + UnitTest + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + true + + + ..\AgileMapper.snk + + + + ..\packages\Effort.EF6.1.3.0\lib\net45\Effort.dll + + + ..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.dll + + + ..\packages\EntityFramework.6.2.0\lib\net45\EntityFramework.SqlServer.dll + + + ..\packages\NMemory.1.1.2\lib\net45\NMemory.dll + + + + + + + + + + + + ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll + + + ..\packages\xunit.assert.2.3.1\lib\netstandard1.1\xunit.assert.dll + + + ..\packages\xunit.extensibility.core.2.3.1\lib\netstandard1.1\xunit.core.dll + + + ..\packages\xunit.extensibility.execution.2.3.1\lib\net452\xunit.execution.desktop.dll + + + + + Properties\CommonAssemblyInfo.cs + + + Properties\VersionInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {66522d44-19f5-4af5-9d43-483a3cd6f958} + AgileMapper.UnitTests.Orms + + + {46d95c53-b4cb-4ee7-9573-5d3ef96099c0} + AgileMapper + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/App.config b/AgileMapper.UnitTests.Orms.Ef6/App.config new file mode 100644 index 000000000..e6079b964 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs b/AgileMapper.UnitTests.Orms.Ef6/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs new file mode 100644 index 000000000..29000c136 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs @@ -0,0 +1,20 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Configuration.Inline +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration.Inline; + using Xunit; + + public class WhenConfiguringConstructorDataSourcesInline + : WhenConfiguringConstructorDataSourcesInline + { + public WhenConfiguringConstructorDataSourcesInline(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorApplyingAConfiguredConstantByParameterTypeInline() + => RunShouldErrorApplyingAConfiguredConstantByParameterTypeInline(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/Configuration/Inline/WhenConfiguringDataSourcesInline.cs b/AgileMapper.UnitTests.Orms.Ef6/Configuration/Inline/WhenConfiguringDataSourcesInline.cs new file mode 100644 index 000000000..3985148d7 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Configuration/Inline/WhenConfiguringDataSourcesInline.cs @@ -0,0 +1,14 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Configuration.Inline +{ + using Infrastructure; + using Orms.Configuration.Inline; + + public class WhenConfiguringDataSourcesInline + : WhenConfiguringDataSourcesInline + { + public WhenConfiguringDataSourcesInline(InMemoryEf6TestContext context) + : base(context) + { + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringConstructorDataSources.cs b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringConstructorDataSources.cs new file mode 100644 index 000000000..0addef066 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringConstructorDataSources.cs @@ -0,0 +1,23 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringConstructorDataSources : WhenConfiguringConstructorDataSources + { + public WhenConfiguringConstructorDataSources(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorApplyingAConfiguredConstantByParameterType() + => RunShouldErrorApplyingAConfiguredConstantByParameterType(); + + [Fact] + public Task ShouldErrorApplyingAConfiguredExpressionByParameterName() + => RunShouldErrorApplyingAConfiguredExpressionByParameterName(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringDataSources.cs b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringDataSources.cs new file mode 100644 index 000000000..ab04c4430 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringDataSources.cs @@ -0,0 +1,21 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringDataSources : WhenConfiguringDataSources + { + public WhenConfiguringDataSources(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldApplyAConfiguredMember() => DoShouldApplyAConfiguredMember(); + + [Fact] + public Task ShouldApplyMultipleConfiguredMembers() => DoShouldApplyMultipleConfiguredMembers(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringDerivedTypes.cs b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringDerivedTypes.cs new file mode 100644 index 000000000..68ab1a7dc --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringDerivedTypes.cs @@ -0,0 +1,25 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringDerivedTypes : WhenConfiguringDerivedTypes + { + public WhenConfiguringDerivedTypes(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingToConfiguredDerivedTypes() => RunShouldErrorProjectingToConfiguredDerivedTypes(); + + [Fact] + public Task ShouldErrorProjectingToAFallbackDerivedType() => RunShouldErrorProjectingToAFallbackDerivedType(); + + [Fact] + public Task ShouldErrorAttemptingToNotApplyDerivedSourceTypePairing() + => RunShouldErrorAttemptingToNotApplyDerivedSourceTypePairing(); + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringEnumMapping.cs b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringEnumMapping.cs new file mode 100644 index 000000000..1e89390b6 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringEnumMapping.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Configuration +{ + using Infrastructure; + using Orms.Configuration; + + public class WhenConfiguringEnumMapping : WhenConfiguringEnumMapping + { + public WhenConfiguringEnumMapping(InMemoryEf6TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringObjectCreation.cs b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringObjectCreation.cs new file mode 100644 index 000000000..2584c5b0a --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringObjectCreation.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringObjectCreation : WhenConfiguringObjectCreation + { + public WhenConfiguringObjectCreation(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorUsingAConditionalObjectFactory() => RunShouldErrorUsingAConditionalObjectFactory(); + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringStringFormatting.cs b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringStringFormatting.cs new file mode 100644 index 000000000..30c191fe0 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenConfiguringStringFormatting.cs @@ -0,0 +1,25 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringStringFormatting : WhenConfiguringStringFormatting + { + public WhenConfiguringStringFormatting(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldFormatDateTimes() + => DoShouldFormatDateTimes(d => d.ToString("MM/dd/yyyy HH:mm:ss")); + + [Fact] + public Task ShouldFormatDecimals() => DoShouldFormatDecimals(d => d + ""); + + [Fact] + public Task ShouldFormatDoubles() => DoShouldFormatDoubles(d => d + ""); + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenIgnoringMembers.cs b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenIgnoringMembers.cs new file mode 100644 index 000000000..6efe8f2ed --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenIgnoringMembers.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Configuration +{ + using Infrastructure; + using Orms.Configuration; + + public class WhenIgnoringMembers : WhenIgnoringMembers + { + public WhenIgnoringMembers(InMemoryEf6TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenMappingToNull.cs b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenMappingToNull.cs new file mode 100644 index 000000000..e17e181cf --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Configuration/WhenMappingToNull.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenMappingToNull : WhenMappingToNull + { + public WhenMappingToNull(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldApplyAUserConfiguration() => RunShouldApplyAUserConfiguration(); + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef6/Infrastructure/Ef6DbSetWrapper.cs b/AgileMapper.UnitTests.Orms.Ef6/Infrastructure/Ef6DbSetWrapper.cs new file mode 100644 index 000000000..8e547c2c9 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Infrastructure/Ef6DbSetWrapper.cs @@ -0,0 +1,39 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Infrastructure +{ + using System; + using System.Data.Entity; + using System.Linq.Expressions; + using System.Threading.Tasks; + using Orms.Infrastructure; + + public class Ef6DbSetWrapper : DbSetWrapperBase + where TEntity : class + { + private readonly DbSet _dbSet; + + public Ef6DbSetWrapper(DbContext context) + : base(context.Set()) + { + _dbSet = context.Set(); + } + + public override void Include(Expression> navigationPropertyPath) + => _dbSet.Include(navigationPropertyPath); + + public override Task Add(TEntity itemToAdd) + { + _dbSet.Add(itemToAdd); + + return Task.CompletedTask; + } + + public override Task AddRange(TEntity[] itemsToAdd) + { + _dbSet.AddRange(itemsToAdd); + + return Task.CompletedTask; + } + + public override void Clear() => _dbSet.RemoveRange(_dbSet); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/Infrastructure/Ef6TestDbContext.cs b/AgileMapper.UnitTests.Orms.Ef6/Infrastructure/Ef6TestDbContext.cs new file mode 100644 index 000000000..34f35ee8a --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Infrastructure/Ef6TestDbContext.cs @@ -0,0 +1,134 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Infrastructure +{ + using System.Data.Common; + using System.Data.Entity; + using System.Threading.Tasks; + using Effort; + using Orms.Infrastructure; + using TestClasses; + + public class Ef6TestDbContext : DbContext, ITestDbContext + { + public Ef6TestDbContext() + : this(DbConnectionFactory.CreateTransient()) + { + } + + protected Ef6TestDbContext(DbConnection dbConnection) + : base(dbConnection, true) + { + } + + public DbSet Animals { get; set; } + + public DbSet Shapes { get; set; } + + public DbSet Companies { get; set; } + + public DbSet Employees { get; set; } + + public DbSet Categories { get; set; } + + public DbSet Products { get; set; } + + public DbSet Accounts { get; set; } + + public DbSet Persons { get; set; } + + public DbSet
Addresses { get; set; } + + public DbSet Rotas { get; set; } + + public DbSet RotaEntries { get; set; } + + public DbSet Orders { get; set; } + + public DbSet OrderItems { get; set; } + + public DbSet BoolItems { get; set; } + + public DbSet ByteItems { get; set; } + + public DbSet ShortItems { get; set; } + + public DbSet IntItems { get; set; } + + public DbSet LongItems { get; set; } + + public DbSet DecimalItems { get; set; } + + public DbSet DoubleItems { get; set; } + + public DbSet DateTimeItems { get; set; } + + public DbSet StringItems { get; set; } + + public DbSet TitleItems { get; set; } + + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasMany(c => c.SubCategories) + .WithOptional(c => c.ParentCategory) + .HasForeignKey(c => c.ParentCategoryId); + + modelBuilder.Entity() + .HasKey(aa => new { aa.AccountId, aa.AddressId }); + + base.OnModelCreating(modelBuilder); + } + + #region ITestDbContext Members + + IDbSetWrapper ITestDbContext.Animals => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Shapes => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Companies => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Employees => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Categories => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Products => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Accounts => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Persons => new Ef6DbSetWrapper(this); + + IDbSetWrapper
ITestDbContext.Addresses => new Ef6DbSetWrapper
(this); + + IDbSetWrapper ITestDbContext.Rotas => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.RotaEntries => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Orders => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.OrderItems => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.BoolItems => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.ByteItems => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.ShortItems => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.IntItems => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.LongItems => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.DecimalItems => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.DoubleItems => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.DateTimeItems => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.StringItems => new Ef6DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.TitleItems => new Ef6DbSetWrapper(this); + + Task ITestDbContext.SaveChanges() => SaveChangesAsync(); + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/Infrastructure/InMemoryEf6TestContext.cs b/AgileMapper.UnitTests.Orms.Ef6/Infrastructure/InMemoryEf6TestContext.cs new file mode 100644 index 000000000..8a5b605a3 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Infrastructure/InMemoryEf6TestContext.cs @@ -0,0 +1,8 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Infrastructure +{ + using Orms.Infrastructure; + + public class InMemoryEf6TestContext : InMemoryOrmTestContext + { + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/Infrastructure/InMemoryEf6TestDefinition.cs b/AgileMapper.UnitTests.Orms.Ef6/Infrastructure/InMemoryEf6TestDefinition.cs new file mode 100644 index 000000000..4093205ca --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Infrastructure/InMemoryEf6TestDefinition.cs @@ -0,0 +1,10 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Infrastructure +{ + using Orms; + using Xunit; + + [CollectionDefinition(TestConstants.OrmCollectionName)] + public class InMemoryEf6TestDefinition : ICollectionFixture + { + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/Properties/AssemblyInfo.cs b/AgileMapper.UnitTests.Orms.Ef6/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..34dc22ba0 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("AgileObjects.AgileMapper.UnitTests.Ef6")] +[assembly: AssemblyDescription("AgileObjects.AgileMapper.UnitTests.Ef6")] + +[assembly: ComVisible(false)] \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/Recursion/WhenProjectingCircularReferences.cs b/AgileMapper.UnitTests.Orms.Ef6/Recursion/WhenProjectingCircularReferences.cs new file mode 100644 index 000000000..fdabe83b9 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/Recursion/WhenProjectingCircularReferences.cs @@ -0,0 +1,21 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.Recursion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Recursion; + using Xunit; + + public class WhenProjectingCircularReferences : + WhenProjectingCircularReferences, + IOneToManyRecursionProjectionFailureTest + { + public WhenProjectingCircularReferences(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingAOneToManyRelationshipToZeroethRecursionDepth() + => DoShouldErrorProjectingAOneToManyRelationshipToZeroethRecursionDepth(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToBools.cs b/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToBools.cs new file mode 100644 index 000000000..fe128a71f --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToBools.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.SimpleTypeConversion +{ + using Infrastructure; + using Orms.SimpleTypeConversion; + + public class WhenConvertingToBools : WhenConvertingToBools + { + public WhenConvertingToBools(InMemoryEf6TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToDateTimes.cs b/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToDateTimes.cs new file mode 100644 index 000000000..ba97aad5e --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToDateTimes.cs @@ -0,0 +1,30 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToDateTimes : + WhenConvertingToDateTimes, + IStringConversionFailureTest, + IStringConversionValidationFailureTest + { + public WhenConvertingToDateTimes(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingAParseableString() + => RunShouldErrorProjectingAParseableStringToADateTime(); + + [Fact] + public Task ShouldErrorProjectingANullString() + => RunShouldErrorProjectingANullStringToADateTime(); + + [Fact] + public Task ShouldErrorProjectingAnUnparseableString() + => RunShouldErrorProjectingAnUnparseableStringToADateTime(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToDoubles.cs b/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToDoubles.cs new file mode 100644 index 000000000..901189f0e --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToDoubles.cs @@ -0,0 +1,31 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToDoubles : + WhenConvertingToDoubles, + IStringConversionFailureTest, + IStringConversionValidationFailureTest + + { + public WhenConvertingToDoubles(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingAParseableString() + => RunShouldErrorProjectingAParseableStringToADouble(); + + [Fact] + public Task ShouldErrorProjectingANullString() + => RunShouldErrorProjectingANullStringToADouble(); + + [Fact] + public Task ShouldErrorProjectingAnUnparseableString() + => RunShouldErrorProjectingAnUnparseableStringToADouble(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToGuids.cs b/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToGuids.cs new file mode 100644 index 000000000..43070ee28 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToGuids.cs @@ -0,0 +1,25 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToGuids : + WhenConvertingToGuids, + IStringConversionFailureTest + { + public WhenConvertingToGuids(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingAParseableString() + => RunShouldErrorProjectingAParseableStringToAGuid(); + + [Fact] + public Task ShouldErrorProjectingANullString() + => RunShouldErrorProjectingANullStringToAGuid(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToInts.cs b/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToInts.cs new file mode 100644 index 000000000..2985146fa --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToInts.cs @@ -0,0 +1,30 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToInts : + WhenConvertingToInts, + IStringConversionFailureTest, + IStringConversionValidationFailureTest + { + public WhenConvertingToInts(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingAParseableString() + => RunShouldErrorProjectingAParseableStringToAnInt(); + + [Fact] + public Task ShouldErrorProjectingANullString() + => RunShouldErrorProjectingANullStringToAnInt(); + + [Fact] + public Task ShouldErrorProjectingAnUnparseableString() + => RunShouldErrorProjectingAnUnparseableStringToAnInt(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToStrings.cs b/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToStrings.cs new file mode 100644 index 000000000..ebc0fb04f --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/SimpleTypeConversion/WhenConvertingToStrings.cs @@ -0,0 +1,25 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToStrings : WhenConvertingToStrings + { + public WhenConvertingToStrings(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectADecimalToAString() => DoShouldProjectADecimalToAString(); + + [Fact] + public Task ShouldProjectADoubleToAString() => DoShouldProjectADoubleToAString(); + + [Fact] + public Task ShouldProjectADateTimeToAString() + => DoShouldProjectADateTimeToAString(d => d.ToString("MM/dd/yyyy HH:mm:ss")); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/WhenProjectingFlatTypes.cs b/AgileMapper.UnitTests.Orms.Ef6/WhenProjectingFlatTypes.cs new file mode 100644 index 000000000..aefe6c03e --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/WhenProjectingFlatTypes.cs @@ -0,0 +1,19 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6 +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms; + using Xunit; + + public class WhenProjectingFlatTypes : WhenProjectingFlatTypes + { + public WhenProjectingFlatTypes(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldErrorProjectingStructCtorParameters() + => RunShouldErrorProjectingStructCtorParameters(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/WhenProjectingToComplexTypeMembers.cs b/AgileMapper.UnitTests.Orms.Ef6/WhenProjectingToComplexTypeMembers.cs new file mode 100644 index 000000000..e5a53e5c3 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/WhenProjectingToComplexTypeMembers.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6 +{ + using Infrastructure; + using Orms; + + public class WhenProjectingToComplexTypeMembers : WhenProjectingToComplexTypeMembers + { + public WhenProjectingToComplexTypeMembers(InMemoryEf6TestContext context) + : base(context) + { + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/WhenProjectingToEnumerableMembers.cs b/AgileMapper.UnitTests.Orms.Ef6/WhenProjectingToEnumerableMembers.cs new file mode 100644 index 000000000..7486662d0 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/WhenProjectingToEnumerableMembers.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6 +{ + using System.Threading.Tasks; + using Infrastructure; + using Xunit; + + public class WhenProjectingToEnumerableMembers : WhenProjectingToEnumerableMembers + { + public WhenProjectingToEnumerableMembers(InMemoryEf6TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectToAComplexTypeCollectionMember() + => RunShouldProjectToAComplexTypeCollectionMember(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.Ef6/WhenProjectingToFlatTypes.cs b/AgileMapper.UnitTests.Orms.Ef6/WhenProjectingToFlatTypes.cs new file mode 100644 index 000000000..bf0a20123 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/WhenProjectingToFlatTypes.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6 +{ + using Infrastructure; + + public class WhenProjectingToFlatTypes : WhenProjectingToFlatTypes + { + public WhenProjectingToFlatTypes(InMemoryEf6TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef6/WhenValidatingProjections.cs b/AgileMapper.UnitTests.Orms.Ef6/WhenValidatingProjections.cs new file mode 100644 index 000000000..34fef99c7 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/WhenValidatingProjections.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6 +{ + using Infrastructure; + + public class WhenValidatingProjections : WhenValidatingProjections + { + public WhenValidatingProjections(InMemoryEf6TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef6/WhenViewingProjectionPlans.cs b/AgileMapper.UnitTests.Orms.Ef6/WhenViewingProjectionPlans.cs new file mode 100644 index 000000000..aea83afa9 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/WhenViewingProjectionPlans.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Ef6 +{ + using Infrastructure; + + public class WhenViewingProjectionPlans : WhenViewingProjectionPlans + { + public WhenViewingProjectionPlans(InMemoryEf6TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.Ef6/packages.config b/AgileMapper.UnitTests.Orms.Ef6/packages.config new file mode 100644 index 000000000..c02338986 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.Ef6/packages.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/AgileMapper.UnitTests.Orms.EfCore1.csproj b/AgileMapper.UnitTests.Orms.EfCore1/AgileMapper.UnitTests.Orms.EfCore1.csproj new file mode 100644 index 000000000..fd1c25c47 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/AgileMapper.UnitTests.Orms.EfCore1.csproj @@ -0,0 +1,258 @@ + + + + + + + Debug + AnyCPU + {FDEC5C57-9B2B-4599-80A6-DF0791BC7E1B} + Library + Properties + AgileObjects.AgileMapper.UnitTests.Orms.EfCore1 + AgileObjects.AgileMapper.UnitTests.Orms.EfCore1 + v4.6.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + + + ..\AgileMapper.snk + + + + ..\packages\Microsoft.EntityFrameworkCore.1.1.5\lib\net451\Microsoft.EntityFrameworkCore.dll + + + ..\packages\Microsoft.EntityFrameworkCore.InMemory.1.1.3\lib\net451\Microsoft.EntityFrameworkCore.InMemory.dll + + + ..\packages\Microsoft.Extensions.Caching.Abstractions.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Caching.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Caching.Memory.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Caching.Memory.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.1.1.1\lib\netstandard1.1\Microsoft.Extensions.DependencyInjection.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.2.0.0\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Logging.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Logging.dll + + + ..\packages\Microsoft.Extensions.Logging.Abstractions.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Options.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Options.dll + + + ..\packages\Microsoft.Extensions.Primitives.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll + + + ..\packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll + True + + + ..\packages\Remotion.Linq.2.1.2\lib\net45\Remotion.Linq.dll + + + + ..\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.ComponentModel.Annotations.4.4.0\lib\net461\System.ComponentModel.Annotations.dll + + + + + ..\packages\System.Console.4.3.0\lib\net46\System.Console.dll + + + ..\packages\System.Diagnostics.DiagnosticSource.4.4.1\lib\net46\System.Diagnostics.DiagnosticSource.dll + + + ..\packages\System.Diagnostics.Tracing.4.3.0\lib\net462\System.Diagnostics.Tracing.dll + + + ..\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.IO.4.3.0\lib\net462\System.IO.dll + + + ..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll + True + + + + ..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll + + + ..\packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll + + + ..\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 + True + + + ..\packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll + + + + ..\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.CompilerServices.Unsafe.4.4.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Runtime.Extensions.4.3.0\lib\net462\System.Runtime.Extensions.dll + + + ..\packages\System.Runtime.InteropServices.4.3.0\lib\net462\System.Runtime.InteropServices.dll + + + ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + True + + + ..\packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net461\System.Security.Cryptography.Algorithms.dll + True + + + ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll + + + ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll + + + ..\packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net461\System.Security.Cryptography.X509Certificates.dll + True + + + + + ..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll + + + ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll + + + ..\packages\xunit.assert.2.3.1\lib\netstandard1.1\xunit.assert.dll + + + ..\packages\xunit.extensibility.core.2.3.1\lib\netstandard1.1\xunit.core.dll + + + ..\packages\xunit.extensibility.execution.2.3.1\lib\net452\xunit.execution.desktop.dll + + + + + Designer + + + Designer + + + + + {66522d44-19f5-4af5-9d43-483a3cd6f958} + AgileMapper.UnitTests.Orms + + + {46d95c53-b4cb-4ee7-9573-5d3ef96099c0} + AgileMapper + + + + + Properties\CommonAssemblyInfo.cs + + + Properties\VersionInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs new file mode 100644 index 000000000..47d3aeb1a --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs @@ -0,0 +1,20 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Configuration.Inline +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration.Inline; + using Xunit; + + public class WhenConfiguringConstructorDataSourcesInline + : WhenConfiguringConstructorDataSourcesInline + { + public WhenConfiguringConstructorDataSourcesInline(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldApplyAConfiguredConstantByParameterTypeInline() + => RunShouldApplyAConfiguredConstantByParameterTypeInline(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Configuration/Inline/WhenConfiguringDataSourcesInline.cs b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/Inline/WhenConfiguringDataSourcesInline.cs new file mode 100644 index 000000000..9c0beb6db --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/Inline/WhenConfiguringDataSourcesInline.cs @@ -0,0 +1,14 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Configuration.Inline +{ + using Infrastructure; + using Orms.Configuration.Inline; + + public class WhenConfiguringDataSourcesInline + : WhenConfiguringDataSourcesInline + { + public WhenConfiguringDataSourcesInline(InMemoryEfCore1TestContext context) + : base(context) + { + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringConstructorDataSources.cs b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringConstructorDataSources.cs new file mode 100644 index 000000000..44d2106ae --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringConstructorDataSources.cs @@ -0,0 +1,23 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringConstructorDataSources : WhenConfiguringConstructorDataSources + { + public WhenConfiguringConstructorDataSources(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldApplyAConfiguredConstantByParameterType() + => RunShouldApplyAConfiguredConstantByParameterType(); + + [Fact] + public Task ShouldApplyAConfiguredExpressionByParameterName() + => RunShouldApplyAConfiguredExpressionByParameterName(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringDataSources.cs b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringDataSources.cs new file mode 100644 index 000000000..2226f765b --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringDataSources.cs @@ -0,0 +1,21 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringDataSources : WhenConfiguringDataSources + { + public WhenConfiguringDataSources(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldApplyAConfiguredMember() => DoShouldApplyAConfiguredMember(); + + [Fact] + public Task ShouldApplyMultipleConfiguredMembers() => DoShouldApplyMultipleConfiguredMembers(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringDerivedTypes.cs b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringDerivedTypes.cs new file mode 100644 index 000000000..dc4377ca9 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringDerivedTypes.cs @@ -0,0 +1,24 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringDerivedTypes : WhenConfiguringDerivedTypes + { + public WhenConfiguringDerivedTypes(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectToConfiguredDerivedTypes() => RunShouldProjectToConfiguredDerivedTypes(); + + [Fact] + public Task ShouldProjectToAFallbackDerivedType() => RunShouldProjectToAFallbackDerivedType(); + + [Fact] + public Task ShouldNotAttemptToApplyDerivedSourceTypePairing() => RunShouldNotAttemptToApplyDerivedSourceTypePairing(); + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringEnumMapping.cs b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringEnumMapping.cs new file mode 100644 index 000000000..a3af883c8 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringEnumMapping.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Configuration +{ + using Infrastructure; + using Orms.Configuration; + + public class WhenConfiguringEnumMapping : WhenConfiguringEnumMapping + { + public WhenConfiguringEnumMapping(InMemoryEfCore1TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringObjectCreation.cs b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringObjectCreation.cs new file mode 100644 index 000000000..dac02809c --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringObjectCreation.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringObjectCreation : WhenConfiguringObjectCreation + { + public WhenConfiguringObjectCreation(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldUseAConditionalObjectFactory() => RunShouldUseAConditionalObjectFactory(); + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringStringFormatting.cs b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringStringFormatting.cs new file mode 100644 index 000000000..e5e7bf19b --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenConfiguringStringFormatting.cs @@ -0,0 +1,24 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringStringFormatting : WhenConfiguringStringFormatting + { + public WhenConfiguringStringFormatting(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldFormatDateTimes() => DoShouldFormatDateTimes(); + + [Fact] + public Task ShouldFormatDecimals() => DoShouldFormatDecimals(); + + [Fact] + public Task ShouldFormatDoubles() => DoShouldFormatDoubles(); + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenIgnoringMembers.cs b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenIgnoringMembers.cs new file mode 100644 index 000000000..7a9aeedb0 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenIgnoringMembers.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Configuration +{ + using Infrastructure; + using Orms.Configuration; + + public class WhenIgnoringMembers : WhenIgnoringMembers + { + public WhenIgnoringMembers(InMemoryEfCore1TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenMappingToNull.cs b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenMappingToNull.cs new file mode 100644 index 000000000..4809eb3d6 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Configuration/WhenMappingToNull.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenMappingToNull : WhenMappingToNull + { + public WhenMappingToNull(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldApplyAUserConfiguration() => RunShouldApplyAUserConfiguration(); + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Infrastructure/EfCore1DbSetWrapper.cs b/AgileMapper.UnitTests.Orms.EfCore1/Infrastructure/EfCore1DbSetWrapper.cs new file mode 100644 index 000000000..408512241 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Infrastructure/EfCore1DbSetWrapper.cs @@ -0,0 +1,29 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Infrastructure +{ + using System; + using System.Linq.Expressions; + using System.Threading.Tasks; + using Microsoft.EntityFrameworkCore; + using Orms.Infrastructure; + + public class EfCore1DbSetWrapper : DbSetWrapperBase + where TEntity : class + { + private readonly DbSet _dbSet; + + public EfCore1DbSetWrapper(DbContext context) + : base(context.Set()) + { + _dbSet = context.Set(); + } + + public override void Include(Expression> navigationPropertyPath) + => _dbSet.Include(navigationPropertyPath); + + public override Task Add(TEntity itemToAdd) => _dbSet.AddAsync(itemToAdd); + + public override Task AddRange(TEntity[] itemsToAdd) => _dbSet.AddRangeAsync(itemsToAdd); + + public override void Clear() => _dbSet.RemoveRange(_dbSet); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Infrastructure/EfCore1TestDbContext.cs b/AgileMapper.UnitTests.Orms.EfCore1/Infrastructure/EfCore1TestDbContext.cs new file mode 100644 index 000000000..8ef8822b8 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Infrastructure/EfCore1TestDbContext.cs @@ -0,0 +1,146 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Infrastructure +{ + using System.Threading.Tasks; + using Microsoft.EntityFrameworkCore; + using Orms.Infrastructure; + using TestClasses; + + public class EfCore1TestDbContext : DbContext, ITestDbContext + { + private static readonly DbContextOptions _inMemoryOptions = + new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "EfCore1TestDb") + .Options; + + public EfCore1TestDbContext() + : base(_inMemoryOptions) + { + } + + public DbSet Animals { get; set; } + + public DbSet Shapes { get; set; } + + public DbSet Companies { get; set; } + + public DbSet Employees { get; set; } + + public DbSet Categories { get; set; } + + public DbSet Products { get; set; } + + public DbSet Accounts { get; set; } + + public DbSet Persons { get; set; } + + public DbSet
Addresses { get; set; } + + public DbSet Rotas { get; set; } + + public DbSet RotaEntries { get; set; } + + public DbSet Orders { get; set; } + + public DbSet OrderItems { get; set; } + + public DbSet BoolItems { get; set; } + + public DbSet ByteItems { get; set; } + + public DbSet ShortItems { get; set; } + + public DbSet IntItems { get; set; } + + public DbSet LongItems { get; set; } + + public DbSet DecimalItems { get; set; } + + public DbSet DoubleItems { get; set; } + + public DbSet DateTimeItems { get; set; } + + public DbSet StringItems { get; set; } + + public DbSet TitleItems { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasOne(c => c.Ceo) + .WithOne(e => e.Company) + .HasForeignKey(e => e.CompanyId); + + modelBuilder + .Entity() + .HasOne(c => c.ParentCategory) + .WithMany(c => c.SubCategories) + .HasForeignKey(c => c.ParentCategoryId); + + modelBuilder.Entity(); + modelBuilder.Entity(); + + modelBuilder.Entity() + .HasKey(aa => new { aa.AccountId, aa.AddressId }); + + modelBuilder.Entity() + .HasOne(aa => aa.Account) + .WithMany(a => a.DeliveryAddresses) + .HasForeignKey(aa => aa.AccountId); + + base.OnModelCreating(modelBuilder); + } + + #region ITestDbContext Members + + IDbSetWrapper ITestDbContext.Animals => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Shapes => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Companies => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Employees => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Categories => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Products => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Accounts => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Persons => new EfCore1DbSetWrapper(this); + + IDbSetWrapper
ITestDbContext.Addresses => new EfCore1DbSetWrapper
(this); + + IDbSetWrapper ITestDbContext.Rotas => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.RotaEntries => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Orders => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.OrderItems => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.BoolItems => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.ByteItems => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.ShortItems => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.IntItems => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.LongItems => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.DecimalItems => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.DoubleItems => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.DateTimeItems => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.StringItems => new EfCore1DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.TitleItems => new EfCore1DbSetWrapper(this); + + Task ITestDbContext.SaveChanges() => SaveChangesAsync(); + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Infrastructure/InMemoryEfCore1TestContext.cs b/AgileMapper.UnitTests.Orms.EfCore1/Infrastructure/InMemoryEfCore1TestContext.cs new file mode 100644 index 000000000..e7b66ca98 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Infrastructure/InMemoryEfCore1TestContext.cs @@ -0,0 +1,8 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Infrastructure +{ + using Orms.Infrastructure; + + public class InMemoryEfCore1TestContext : InMemoryOrmTestContext + { + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Infrastructure/InMemoryEfCore1TestDefinition.cs b/AgileMapper.UnitTests.Orms.EfCore1/Infrastructure/InMemoryEfCore1TestDefinition.cs new file mode 100644 index 000000000..d2ba98aad --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Infrastructure/InMemoryEfCore1TestDefinition.cs @@ -0,0 +1,10 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Infrastructure +{ + using Orms; + using Xunit; + + [CollectionDefinition(TestConstants.OrmCollectionName)] + public class InMemoryEfCore1TestDefinition : ICollectionFixture + { + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Properties/AssemblyInfo.cs b/AgileMapper.UnitTests.Orms.EfCore1/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..a4f5d441d --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AgileMapper.UnitTests.Orms.EfCore1")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9abbd6a9-5f95-442a-88db-3fc1ebf374a7")] \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/Recursion/WhenProjectingCircularReferences.cs b/AgileMapper.UnitTests.Orms.EfCore1/Recursion/WhenProjectingCircularReferences.cs new file mode 100644 index 000000000..74c9ab960 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/Recursion/WhenProjectingCircularReferences.cs @@ -0,0 +1,29 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.Recursion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Recursion; + using Xunit; + + public class WhenProjectingCircularReferences : + WhenProjectingCircularReferences, + IOneToManyRecursionProjectorTest + { + public WhenProjectingCircularReferences(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAOneToManyRelationshipToDefaultRecursionDepth() + => DoShouldProjectAOneToManyRelationshipToDefaultRecursionDepth(); + + [Fact] + public Task ShouldProjectAOneToManyRelationshipToFirstRecursionDepth() + => DoShouldProjectAOneToManyRelationshipToFirstRecursionDepth(); + + [Fact] + public Task ShouldProjectAOneToManyRelationshipToSecondRecursionDepth() + => DoShouldProjectAOneToManyRelationshipToSecondRecursionDepth(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToBools.cs b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToBools.cs new file mode 100644 index 000000000..1ab6d378d --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToBools.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.SimpleTypeConversion +{ + using Infrastructure; + using Orms.SimpleTypeConversion; + + public class WhenConvertingToBools : WhenConvertingToBools + { + public WhenConvertingToBools(InMemoryEfCore1TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToDateTimes.cs b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToDateTimes.cs new file mode 100644 index 000000000..af642eb6b --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToDateTimes.cs @@ -0,0 +1,30 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToDateTimes : + WhenConvertingToDateTimes, + IStringConverterTest, + IStringConversionValidationFailureTest + { + public WhenConvertingToDateTimes(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAParseableString() + => RunShouldProjectAParseableStringToADateTime(); + + [Fact] + public Task ShouldProjectANullString() + => RunShouldProjectANullStringToADateTime(); + + [Fact] + public Task ShouldErrorProjectingAnUnparseableString() + => RunShouldErrorProjectingAnUnparseableStringToADateTime(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToDoubles.cs b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToDoubles.cs new file mode 100644 index 000000000..3233f892b --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToDoubles.cs @@ -0,0 +1,30 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToDoubles : + WhenConvertingToDoubles, + IStringConverterTest, + IStringConversionValidationFailureTest + { + public WhenConvertingToDoubles(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAParseableString() + => RunShouldProjectAParseableStringToADouble(); + + [Fact] + public Task ShouldProjectANullString() + => RunShouldProjectANullStringToADouble(); + + [Fact] + public Task ShouldErrorProjectingAnUnparseableString() + => RunShouldErrorProjectingAnUnparseableStringToADouble(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToEnums.cs b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToEnums.cs new file mode 100644 index 000000000..3101249bd --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToEnums.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.SimpleTypeConversion +{ + using Infrastructure; + using Orms.SimpleTypeConversion; + + public class WhenConvertingToEnums : WhenConvertingToEnums + { + public WhenConvertingToEnums(InMemoryEfCore1TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToGuids.cs b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToGuids.cs new file mode 100644 index 000000000..af8ba9704 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToGuids.cs @@ -0,0 +1,25 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToGuids : + WhenConvertingToGuids, + IStringConverterTest + { + public WhenConvertingToGuids(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAParseableString() + => RunShouldProjectAParseableStringToAGuid(); + + [Fact] + public Task ShouldProjectANullString() + => RunShouldProjectANullStringToAGuid(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToInts.cs b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToInts.cs new file mode 100644 index 000000000..0b6553e3c --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToInts.cs @@ -0,0 +1,30 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToInts : + WhenConvertingToInts, + IStringConverterTest, + IStringConversionValidationFailureTest + { + public WhenConvertingToInts(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAParseableString() + => RunShouldProjectAParseableStringToAnInt(); + + [Fact] + public Task ShouldProjectANullString() + => RunShouldProjectANullStringToAnInt(); + + [Fact] + public Task ShouldErrorProjectingAnUnparseableString() + => RunShouldErrorProjectingAnUnparseableStringToAnInt(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToStrings.cs b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToStrings.cs new file mode 100644 index 000000000..342a72ecc --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/SimpleTypeConversion/WhenConvertingToStrings.cs @@ -0,0 +1,24 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToStrings : WhenConvertingToStrings + { + public WhenConvertingToStrings(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectADecimalToAString() => DoShouldProjectADecimalToAString(); + + [Fact] + public Task ShouldProjectADoubleToAString() => DoShouldProjectADoubleToAString(); + + [Fact] + public Task ShouldProjectADateTimeToAString() => DoShouldProjectADateTimeToAString(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/WhenProjectingFlatTypes.cs b/AgileMapper.UnitTests.Orms.EfCore1/WhenProjectingFlatTypes.cs new file mode 100644 index 000000000..a7f9381ae --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/WhenProjectingFlatTypes.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1 +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms; + using Xunit; + + public class WhenProjectingFlatTypes : WhenProjectingFlatTypes + { + public WhenProjectingFlatTypes(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectStructCtorParameters() => RunShouldProjectStructCtorParameters(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/WhenProjectingToComplexTypeMembers.cs b/AgileMapper.UnitTests.Orms.EfCore1/WhenProjectingToComplexTypeMembers.cs new file mode 100644 index 000000000..901e0ebb5 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/WhenProjectingToComplexTypeMembers.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1 +{ + using Infrastructure; + using Orms; + + public class WhenProjectingToComplexTypeMembers : WhenProjectingToComplexTypeMembers + { + public WhenProjectingToComplexTypeMembers(InMemoryEfCore1TestContext context) + : base(context) + { + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/WhenProjectingToEnumerableMembers.cs b/AgileMapper.UnitTests.Orms.EfCore1/WhenProjectingToEnumerableMembers.cs new file mode 100644 index 000000000..19a1c8b5c --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/WhenProjectingToEnumerableMembers.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1 +{ + using System.Threading.Tasks; + using Infrastructure; + using Xunit; + + public class WhenProjectingToEnumerableMembers : WhenProjectingToEnumerableMembers + { + public WhenProjectingToEnumerableMembers(InMemoryEfCore1TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectToAComplexTypeCollectionMember() + => RunShouldProjectToAComplexTypeCollectionMember(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/WhenProjectingToFlatTypes.cs b/AgileMapper.UnitTests.Orms.EfCore1/WhenProjectingToFlatTypes.cs new file mode 100644 index 000000000..9961a87a2 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/WhenProjectingToFlatTypes.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1 +{ + using Infrastructure; + + public class WhenProjectingToFlatTypes : WhenProjectingToFlatTypes + { + public WhenProjectingToFlatTypes(InMemoryEfCore1TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore1/WhenValidatingProjections.cs b/AgileMapper.UnitTests.Orms.EfCore1/WhenValidatingProjections.cs new file mode 100644 index 000000000..dbeb19c87 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/WhenValidatingProjections.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1 +{ + using Infrastructure; + + public class WhenValidatingProjections : WhenValidatingProjections + { + public WhenValidatingProjections(InMemoryEfCore1TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore1/WhenViewingProjectionPlans.cs b/AgileMapper.UnitTests.Orms.EfCore1/WhenViewingProjectionPlans.cs new file mode 100644 index 000000000..a7b60bd6f --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/WhenViewingProjectionPlans.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore1 +{ + using Infrastructure; + + public class WhenViewingProjectionPlans : WhenViewingProjectionPlans + { + public WhenViewingProjectionPlans(InMemoryEfCore1TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore1/app.config b/AgileMapper.UnitTests.Orms.EfCore1/app.config new file mode 100644 index 000000000..70b68dd93 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/app.config @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore1/packages.config b/AgileMapper.UnitTests.Orms.EfCore1/packages.config new file mode 100644 index 000000000..0b32f134e --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore1/packages.config @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/AgileMapper.UnitTests.Orms.EfCore2.csproj b/AgileMapper.UnitTests.Orms.EfCore2/AgileMapper.UnitTests.Orms.EfCore2.csproj new file mode 100644 index 000000000..e7c54f90a --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/AgileMapper.UnitTests.Orms.EfCore2.csproj @@ -0,0 +1,254 @@ + + + + + + + Debug + AnyCPU + {2E3DF5C2-8A38-4A03-86D7-8D463C917E47} + Library + Properties + AgileObjects.AgileMapper.UnitTests.Orms.EfCore2 + AgileObjects.AgileMapper.UnitTests.Orms.EfCore2 + v4.6.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + + + ..\AgileMapper.snk + + + + + ..\packages\Microsoft.EntityFrameworkCore.2.0.1\lib\netstandard2.0\Microsoft.EntityFrameworkCore.dll + + + ..\packages\Microsoft.EntityFrameworkCore.InMemory.2.0.0\lib\netstandard2.0\Microsoft.EntityFrameworkCore.InMemory.dll + + + ..\packages\Microsoft.Extensions.Caching.Abstractions.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Caching.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Caching.Memory.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Caching.Memory.dll + + + ..\packages\Microsoft.Extensions.Configuration.Abstractions.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Configuration.Abstractions.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.2.0.0\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.dll + + + ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.2.0.0\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Logging.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Logging.dll + + + ..\packages\Microsoft.Extensions.Logging.Abstractions.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll + + + ..\packages\Microsoft.Extensions.Logging.Debug.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Logging.Debug.dll + + + ..\packages\Microsoft.Extensions.Options.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Options.dll + + + ..\packages\Microsoft.Extensions.Primitives.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll + + + ..\packages\Remotion.Linq.2.1.2\lib\net45\Remotion.Linq.dll + + + ..\packages\Shouldly.2.8.3\lib\net451\Shouldly.dll + + + + ..\packages\System.Collections.Immutable.1.4.0\lib\netstandard2.0\System.Collections.Immutable.dll + + + ..\packages\System.ComponentModel.Annotations.4.4.0\lib\net461\System.ComponentModel.Annotations.dll + + + + + ..\packages\System.Diagnostics.DiagnosticSource.4.4.1\lib\net46\System.Diagnostics.DiagnosticSource.dll + + + ..\packages\System.Interactive.Async.3.1.1\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 + True + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.4.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Runtime.Extensions.4.3.0\lib\net462\System.Runtime.Extensions.dll + True + + + ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll + True + + + ..\packages\xunit.assert.2.3.1\lib\netstandard1.1\xunit.assert.dll + + + ..\packages\xunit.extensibility.core.2.3.1\lib\netstandard1.1\xunit.core.dll + + + ..\packages\xunit.extensibility.execution.2.3.1\lib\net452\xunit.execution.desktop.dll + + + + + + + + + + + + + Properties\CommonAssemblyInfo.cs + + + Properties\VersionInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + {049e1ee5-48ce-441a-b166-3cf6bec17957} + AgileMapper.UnitTests.MoreTestClasses + + + {66522d44-19f5-4af5-9d43-483a3cd6f958} + AgileMapper.UnitTests.Orms + + + {a3f2d405-8c0b-4033-9ec5-1b64007593fb} + AgileMapper.UnitTests + + + {46d95c53-b4cb-4ee7-9573-5d3ef96099c0} + AgileMapper + + + + + + + False + + + False + + + False + + + False + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs new file mode 100644 index 000000000..688265a84 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs @@ -0,0 +1,20 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration.Inline +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration.Inline; + using Xunit; + + public class WhenConfiguringConstructorDataSourcesInline + : WhenConfiguringConstructorDataSourcesInline + { + public WhenConfiguringConstructorDataSourcesInline(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldApplyAConfiguredConstantByParameterTypeInline() + => RunShouldApplyAConfiguredConstantByParameterTypeInline(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringDataSourcesInline.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringDataSourcesInline.cs new file mode 100644 index 000000000..e6e05c2d4 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringDataSourcesInline.cs @@ -0,0 +1,14 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration.Inline +{ + using Infrastructure; + using Orms.Configuration.Inline; + + public class WhenConfiguringDataSourcesInline + : WhenConfiguringDataSourcesInline + { + public WhenConfiguringDataSourcesInline(InMemoryEfCore2TestContext context) + : base(context) + { + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringDerivedTypesInline.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringDerivedTypesInline.cs new file mode 100644 index 000000000..0cd609617 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringDerivedTypesInline.cs @@ -0,0 +1,53 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration.Inline +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using Microsoft.EntityFrameworkCore; + using Orms.Infrastructure; + using TestClasses; + using Xunit; + using static TestClasses.Animal.AnimalType; + + public class WhenConfiguringDerivedTypesInline : OrmTestClassBase + { + public WhenConfiguringDerivedTypesInline(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectToConfiguredDerivedTypesInline() + { + return RunTest(async context => + { + var fido = new Animal { Type = Dog, Name = "Fido" }; + var nelly = new Animal { Type = Elephant, Name = "Nelly" }; + + await context.Animals.AddRangeAsync(fido, nelly); + await context.SaveChangesAsync(); + + var animalDtos = await context + .Animals + .Project().To(cfg => cfg + .If(a => a.Type == Dog) + .MapTo() + .And + .If(a => a.Type == Elephant) + .MapTo()) + .OrderBy(a => a.Id) + .ToArrayAsync(); + + animalDtos.First().ShouldBeOfType(); + animalDtos.First().Id.ShouldBe(fido.Id); + animalDtos.First().Name.ShouldBe("Fido"); + animalDtos.First().Sound.ShouldBe("Woof"); + + animalDtos.Second().ShouldBeOfType(); + animalDtos.Second().Id.ShouldBe(nelly.Id); + animalDtos.Second().Name.ShouldBe("Nelly"); + animalDtos.Second().Sound.ShouldBe("Trumpet"); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringEnumMappingInline.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringEnumMappingInline.cs new file mode 100644 index 000000000..effcb7689 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringEnumMappingInline.cs @@ -0,0 +1,67 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration.Inline +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using Microsoft.EntityFrameworkCore; + using Orms.Infrastructure; + using TestClasses; + using Xunit; + using PaymentTypeUk = UnitTests.TestClasses.PaymentTypeUk; + using PaymentTypeUs = UnitTests.TestClasses.PaymentTypeUs; + + public class WhenConfiguringEnumMappingInline : OrmTestClassBase + { + public WhenConfiguringEnumMappingInline(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldPairEnumMembersInline() + { + return RunTest(async (context, mapper) => + { + var order1 = new OrderUk + { + DatePlaced = DateTime.Now, + PaymentType = PaymentTypeUk.Cheque + }; + + var order2 = new OrderUk + { + DatePlaced = DateTime.Now.AddMinutes(1), + PaymentType = PaymentTypeUk.Cash + }; + + var order3 = new OrderUk + { + DatePlaced = DateTime.Now.AddMinutes(2), + PaymentType = PaymentTypeUk.Card + }; + + await context.Orders.AddRangeAsync(order1, order2, order3); + await context.SaveChangesAsync(); + + var orderVms = await context + .Orders + .ProjectUsing(mapper).To(cfg => cfg + .PairEnum(PaymentTypeUk.Cheque).With(PaymentTypeUs.Check)) + .OrderByDescending(o => o.DatePlaced) + .ToArrayAsync(); + + orderVms.Length.ShouldBe(3); + + orderVms.First().DatePlaced.ShouldBe(order3.DatePlaced); + orderVms.First().PaymentType.ShouldBe(PaymentTypeUs.Card); + + orderVms.Second().DatePlaced.ShouldBe(order2.DatePlaced); + orderVms.Second().PaymentType.ShouldBe(PaymentTypeUs.Cash); + + orderVms.Third().DatePlaced.ShouldBe(order1.DatePlaced); + orderVms.Third().PaymentType.ShouldBe(PaymentTypeUs.Check); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringNameMatchingInline.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringNameMatchingInline.cs new file mode 100644 index 000000000..9fdacf15c --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringNameMatchingInline.cs @@ -0,0 +1,167 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration.Inline +{ + using System.Threading.Tasks; + using Infrastructure; + using Microsoft.EntityFrameworkCore; + using Orms.Infrastructure; + using TestClasses; + using Xunit; + + public class WhenConfiguringNameMatchingInline : OrmTestClassBase + { + public WhenConfiguringNameMatchingInline(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldUseACustomPrefixInline() + { + return RunTest(async (context, mapper) => + { + var names = new PublicStringNames + { + _Value = "123", + _Value_ = "456", + Value_ = "789" + }; + + await context.StringNameItems.AddAsync(names); + await context.SaveChangesAsync(); + + var stringDto = await context + .StringNameItems + .ProjectUsing(mapper) + .To(cfg => cfg.UseNamePrefix("_")) + .FirstAsync(); + + stringDto.Value.ShouldBe("123"); + }); + } + + [Fact] + public Task ShouldUseMultipleCustomPrefixesInline() + { + return RunTest(async (context, mapper) => + { + var names = new PublicStringNames + { + _Value = "aaa", + _Value_ = "bbb", + Value_ = "ccc" + }; + + await context.StringNameItems.AddAsync(names); + await context.SaveChangesAsync(); + + var stringDto = await context + .StringNameItems + .ProjectUsing(mapper) + .To(cfg => cfg.UseNamePrefixes("str", "_")) + .FirstAsync(); + + stringDto.Value.ShouldBe("aaa"); + }); + } + + [Fact] + public Task ShouldUseACustomSuffixInline() + { + return RunTest(async (context, mapper) => + { + var names = new PublicStringNames + { + _Value = "123", + _Value_ = "456", + Value_ = "789" + }; + + await context.StringNameItems.AddAsync(names); + await context.SaveChangesAsync(); + + var stringDto = await context + .StringNameItems + .ProjectUsing(mapper) + .To(cfg => cfg.UseNameSuffix("_")) + .FirstAsync(); + + stringDto.Value.ShouldBe("789"); + }); + } + + [Fact] + public Task ShouldUseMultipleCustomSuffixesInline() + { + return RunTest(async (context, mapper) => + { + var names = new PublicStringNames + { + _Value = "aaa", + _Value_ = "bbb", + Value_ = "ccc" + }; + + await context.StringNameItems.AddAsync(names); + await context.SaveChangesAsync(); + + var stringDto = await context + .StringNameItems + .ProjectUsing(mapper) + .To(cfg => cfg.UseNameSuffixes("Val", "_")) + .FirstAsync(); + + stringDto.Value.ShouldBe("ccc"); + }); + } + + [Fact] + public Task ShouldUseACustomNamingPatternInline() + { + return RunTest(async (context, mapper) => + { + var names = new PublicStringNames + { + _Value = "123", + _Value_ = "456", + Value_ = "789" + }; + + await context.StringNameItems.AddAsync(names); + await context.SaveChangesAsync(); + + var stringDto = await context + .StringNameItems + .ProjectUsing(mapper) + .To(cfg => cfg.UseNamePattern("^_(.+)_$")) + .FirstAsync(); + + stringDto.Value.ShouldBe("456"); + }); + } + + [Fact] + public Task ShouldUseMultipleCustomNamingPatternsInline() + { + return RunTest(async (context, mapper) => + { + var names = new PublicStringNames + { + _Value = "aaa", + _Value_ = "bbb", + Value_ = "ccc" + }; + + await context.StringNameItems.AddAsync(names); + await context.SaveChangesAsync(); + + var stringDto = await context + .StringNameItems + .ProjectUsing(mapper) + .To(cfg => cfg.UseNamePatterns("^str(.+)Val$", "^_(.+)_$")) + .FirstAsync(); + + stringDto.Value.ShouldBe("bbb"); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringObjectCreationInline.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringObjectCreationInline.cs new file mode 100644 index 000000000..8db1959b2 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringObjectCreationInline.cs @@ -0,0 +1,44 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration.Inline +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using Orms.Infrastructure; + using TestClasses; + using Xunit; + + public class WhenConfiguringObjectCreationInline : OrmTestClassBase + { + public WhenConfiguringObjectCreationInline(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldUseAConditionalObjectFactoryInline() + { + return RunTest(async context => + { + await context.IntItems.AddRangeAsync( + new PublicInt { Value = 1 }, + new PublicInt { Value = 2 }, + new PublicInt { Value = 3 }); + + await context.SaveChangesAsync(); + + var stringDtos = context + .IntItems + .OrderBy(p => p.Id) + .Project() + .To(cfg => cfg + .If(p => p.Value % 2 == 0) + .CreateInstancesUsing(p => new PublicStringCtorDto((p.Value * 2).ToString()))) + .ToArray(); + + stringDtos.First().Value.ShouldBe("1"); + stringDtos.Second().Value.ShouldBe("4"); + stringDtos.Third().Value.ShouldBe("3"); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringStringFormattingInline.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringStringFormattingInline.cs new file mode 100644 index 000000000..a1213cd3c --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenConfiguringStringFormattingInline.cs @@ -0,0 +1,44 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration.Inline +{ + using System.Threading.Tasks; + using Infrastructure; + using Microsoft.EntityFrameworkCore; + using Orms.Infrastructure; + using TestClasses; + using Xunit; + + public class WhenConfiguringStringFormattingInline : OrmTestClassBase + { + public WhenConfiguringStringFormattingInline(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldFormatDecimalsInline() + { + return RunTest(async (context, mapper) => + { + var source = new PublicDecimal { Value = 123.00m }; + + await context.DecimalItems.AddAsync(source); + await context.SaveChangesAsync(); + + var stringDto = await context + .DecimalItems + .ProjectUsing(mapper) + .To(cfg => cfg + .WhenMapping + .StringsFrom(c => c.FormatUsing("C")) + .AndWhenMapping + .From() + .ProjectedTo() + .Map(d => d.Value * 2) + .To(dto => dto.Value)) + .SingleAsync(); + + stringDto.Value.ShouldBe(246.00m.ToString("C")); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenIgnoringMembersInline.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenIgnoringMembersInline.cs new file mode 100644 index 000000000..f22d8d7f1 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenIgnoringMembersInline.cs @@ -0,0 +1,43 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration.Inline +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using Orms.Infrastructure; + using TestClasses; + using Xunit; + + public class WhenIgnoringMembersInline : OrmTestClassBase + { + public WhenIgnoringMembersInline(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldIgnoreAConfiguredMemberConditionallyInline() + { + return RunTest(async (context, mapper) => + { + var product1 = new Product { Name = "1" }; + var product2 = new Product { Name = "P.2" }; + + await context.Products.AddRangeAsync(product1, product2); + await context.SaveChangesAsync(); + + var productDtos = context + .Products + .ProjectUsing(mapper).To(cfg => cfg + .If(p => p.Name.Length < 2) + .Ignore(p => p.Name)) + .OrderBy(p => p.ProductId) + .ToArray(); + + productDtos.First().ProductId.ShouldBe(product1.ProductId); + productDtos.First().Name.ShouldBeNull(); + productDtos.Second().ProductId.ShouldBe(product2.ProductId); + productDtos.Second().Name.ShouldBe("P.2"); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenMappingToNullInline.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenMappingToNullInline.cs new file mode 100644 index 000000000..c946474f3 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenMappingToNullInline.cs @@ -0,0 +1,60 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration.Inline +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using Orms.Infrastructure; + using TestClasses; + using Xunit; + + public class WhenMappingToNullInline : OrmTestClassBase + { + public WhenMappingToNullInline(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldApplyAUserConfigurationInline() + { + return RunTest(async (context, mapper) => + { + var person1 = new Person + { + Name = "Frank", + Address = new Address { Line1 = "1" } + }; + + var person2 = new Person + { + Name = "Dee", + Address = new Address { Line1 = "Paddie's Pub" } + }; + + await context.Persons.AddRangeAsync(person1, person2); + await context.SaveChangesAsync(); + + var personDtos = context + .Persons + .ProjectUsing(mapper) + .To(cfg => cfg + .WhenMapping + .From
() + .ProjectedTo() + .If(a => a.Line1.Length != 1) + .MapToNull()) + .OrderBy(p => p.Id) + .ToArray(); + + personDtos.Length.ShouldBe(2); + + personDtos.First().Name.ShouldBe("Frank"); + personDtos.First().Address.ShouldNotBeNull(); + personDtos.First().Address.Line1.ShouldBe("1"); + + personDtos.Second().Name.ShouldBe("Dee"); + personDtos.Second().Address.ShouldBeNull(); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenValidatingProjectionsInline.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenValidatingProjectionsInline.cs new file mode 100644 index 000000000..7fa69dd0d --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/Inline/WhenValidatingProjectionsInline.cs @@ -0,0 +1,63 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration.Inline +{ + using System.Threading.Tasks; + using Infrastructure; + using Microsoft.EntityFrameworkCore; + using Orms.Infrastructure; + using TestClasses; + using Validation; + using Xunit; + + public class WhenValidatingProjectionsInline : OrmTestClassBase + { + public WhenValidatingProjectionsInline(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldSupportCachedProjectionValidationInline() + { + return RunTest(async context => + { + var address = new Address { Line1 = "1" }; + + await context.Addresses.AddAsync(address); + await context.SaveChangesAsync(); + + Should.NotThrow(async () => + { + var addressDto = await context + .Addresses + .Project().To(cfg => cfg + .ThrowNowIfMappingPlanIsIncomplete()) + .FirstAsync(); + + addressDto.Line1.ShouldBe("1"); + }); + }); + } + + [Fact] + public Task ShouldErrorIfCachedProjectionTargetTypeIsUnconstructableInline() + { + return RunTest(async (context, mapper) => + { + var validationEx = await Should.ThrowAsync(async () => + { + await context + .Addresses + .ProjectUsing(mapper) + .To(cfg => cfg + .ThrowNowIfMappingPlanIsIncomplete()) + .FirstOrDefaultAsync(); + }); + + validationEx.Message.ShouldContain("IQueryable
-> IQueryable"); + validationEx.Message.ShouldContain("Rule set: Project"); + validationEx.Message.ShouldContain("Unconstructable target Types"); + validationEx.Message.ShouldContain("Address -> ProductStruct"); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringCallbacks.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringCallbacks.cs new file mode 100644 index 000000000..e13c6d480 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringCallbacks.cs @@ -0,0 +1,250 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration +{ + using System; + using System.Threading.Tasks; + using Infrastructure; + using Orms.Infrastructure; + using TestClasses; + using Xunit; + + public class WhenConfiguringCallbacks : OrmTestClassBase + { + public WhenConfiguringCallbacks(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldNotAttemptToCallAPreMappingCallback() + { + return RunTest(async (context, mapper) => + { + var callbackCalled = false; + + mapper.WhenMapping + .To() + .Before + .MappingBegins + .Call(ctx => callbackCalled = true); + + var square = new Square { SideLength = 12 }; + + await context.Shapes.AddAsync(square); + await context.SaveChangesAsync(); + + var squareVm = context + .Shapes + .ProjectUsing(mapper) + .To() + .ShouldHaveSingleItem(); + + callbackCalled.ShouldBeFalse(); + squareVm.SideLength.ShouldBe(12); + + mapper.Map(square).OnTo(squareVm); + + callbackCalled.ShouldBeTrue(); + }); + } + + [Fact] + public Task ShouldNotAttemptToCallAPostMappingCallback() + { + return RunTest(async (context, mapper) => + { + var callbackCalled = false; + + mapper.WhenMapping + .From() + .To() + .After + .MappingEnds + .Call((s, c) => callbackCalled = true); + + var circle = new Circle { Diameter = 1 }; + + await context.Shapes.AddAsync(circle); + await context.SaveChangesAsync(); + + var circleVm = context + .Shapes + .ProjectUsing(mapper) + .To() + .ShouldHaveSingleItem(); + + callbackCalled.ShouldBeFalse(); + circleVm.Diameter.ShouldBe(1); + + mapper.Map(circle).Over(circleVm); + + callbackCalled.ShouldBeTrue(); + }); + } + + [Fact] + public Task ShouldNotAttemptToCallAPreObjectCreationCallback() + { + return RunTest(async (context, mapper) => + { + var callbackCalled = false; + + mapper.WhenMapping + .To() + .Before + .CreatingInstancesOf() + .Call(ctx => callbackCalled = true); + + var person = new Person { Name = "Bjorn", Address = new Address { Line1 = "Sweden" } }; + + await context.Persons.AddAsync(person); + await context.SaveChangesAsync(); + + var personDto = context + .Persons + .ProjectUsing(mapper) + .To() + .ShouldHaveSingleItem(); + + callbackCalled.ShouldBeFalse(); + personDto.Name.ShouldBe("Bjorn"); + personDto.Address.ShouldNotBeNull(); + personDto.Address.Line1.ShouldBe("Sweden"); + + mapper.Map(person).Over(personDto); + + callbackCalled.ShouldBeTrue(); + }); + } + + [Fact] + public Task ShouldNotAttemptToCallAPostObjectCreationCallback() + { + return RunTest(async (context, mapper) => + { + var callbackCalled = false; + + mapper.WhenMapping + .To() + .After + .CreatingInstances + .Call(ctx => callbackCalled = true); + + var person = new Person { Name = "Benny", Address = new Address { Line1 = "Sweden" } }; + + await context.Persons.AddAsync(person); + await context.SaveChangesAsync(); + + var personDto = context + .Persons + .ProjectUsing(mapper) + .To() + .ShouldHaveSingleItem(); + + callbackCalled.ShouldBeFalse(); + personDto.Name.ShouldBe("Benny"); + personDto.Address.ShouldNotBeNull(); + personDto.Address.Line1.ShouldBe("Sweden"); + + mapper.Map(person).ToANew(); + + callbackCalled.ShouldBeTrue(); + }); + } + + [Fact] + public Task ShouldNotAttemptToCallAPreMemberMappingCallback() + { + return RunTest(async (context, mapper) => + { + var callbackCalled = false; + + mapper.WhenMapping + .To() + .Before + .Mapping(s => s.SideLength) + .Call(ctx => callbackCalled = true); + + var square = new Square { SideLength = 1 }; + + await context.Shapes.AddAsync(square); + await context.SaveChangesAsync(); + + var squareVm = context + .Shapes + .ProjectUsing(mapper) + .To() + .ShouldHaveSingleItem(); + + callbackCalled.ShouldBeFalse(); + squareVm.SideLength.ShouldBe(1); + + mapper.Map(square).Over(squareVm); + + callbackCalled.ShouldBeTrue(); + }); + } + + [Fact] + public Task ShouldNotAttemptToCallAPostMemberMappingCallback() + { + return RunTest(async (context, mapper) => + { + var callbackCalled = false; + + mapper.WhenMapping + .To() + .After + .Mapping(c => c.Diameter) + .Call(ctx => callbackCalled = true); + + var circle = new Circle { Diameter = 11 }; + + await context.Shapes.AddAsync(circle); + await context.SaveChangesAsync(); + + var circleVm = context + .Shapes + .ProjectUsing(mapper) + .To() + .ShouldHaveSingleItem(); + + callbackCalled.ShouldBeFalse(); + circleVm.Diameter.ShouldBe(11); + + mapper.Map(circle).Over(circleVm); + + callbackCalled.ShouldBeTrue(); + }); + } + + [Fact] + public Task ShouldBeTolerantOfGlobalExceptionSwallowing() + { + return RunTest(async (context, mapper) => + { + mapper.WhenMapping + .SwallowAllExceptions() + .AndWhenMapping + .To() + .After + .CreatingInstances + .Call(ctx => throw new InvalidOperationException("BOOM")); + + var circle = new Circle { Diameter = 10 }; + + await context.Shapes.AddAsync(circle); + await context.SaveChangesAsync(); + + var circleVm = context + .Shapes + .ProjectUsing(mapper) + .To() + .ShouldHaveSingleItem(); + + circleVm.Diameter.ShouldBe(10); + + Should.NotThrow(() => mapper.Map(circle).ToANew()); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringConstructorDataSources.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringConstructorDataSources.cs new file mode 100644 index 000000000..d5715f78f --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringConstructorDataSources.cs @@ -0,0 +1,23 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringConstructorDataSources : WhenConfiguringConstructorDataSources + { + public WhenConfiguringConstructorDataSources(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldApplyAConfiguredConstantByParameterType() + => RunShouldApplyAConfiguredConstantByParameterType(); + + [Fact] + public Task ShouldApplyAConfiguredExpressionByParameterName() + => RunShouldApplyAConfiguredExpressionByParameterName(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringDataSources.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringDataSources.cs new file mode 100644 index 000000000..ce2369a9c --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringDataSources.cs @@ -0,0 +1,21 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringDataSources : WhenConfiguringDataSources + { + public WhenConfiguringDataSources(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldApplyAConfiguredMember() => DoShouldApplyAConfiguredMember(); + + [Fact] + public Task ShouldApplyMultipleConfiguredMembers() => DoShouldApplyMultipleConfiguredMembers(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringDerivedTypes.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringDerivedTypes.cs new file mode 100644 index 000000000..a6c32681a --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringDerivedTypes.cs @@ -0,0 +1,24 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringDerivedTypes : WhenConfiguringDerivedTypes + { + public WhenConfiguringDerivedTypes(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectToConfiguredDerivedTypes() => RunShouldProjectToConfiguredDerivedTypes(); + + [Fact] + public Task ShouldProjectToAFallbackDerivedType() => RunShouldProjectToAFallbackDerivedType(); + + [Fact] + public Task ShouldNotAttemptToApplyDerivedSourceTypePairing() => RunShouldNotAttemptToApplyDerivedSourceTypePairing(); + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringEnumMapping.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringEnumMapping.cs new file mode 100644 index 000000000..3e66e7c93 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringEnumMapping.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration +{ + using Infrastructure; + using Orms.Configuration; + + public class WhenConfiguringEnumMapping : WhenConfiguringEnumMapping + { + public WhenConfiguringEnumMapping(InMemoryEfCore2TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringNameMatching.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringNameMatching.cs new file mode 100644 index 000000000..2e76ebba9 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringNameMatching.cs @@ -0,0 +1,98 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Microsoft.EntityFrameworkCore; + using Orms.Infrastructure; + using TestClasses; + using Xunit; + + public class WhenConfiguringNameMatching : OrmTestClassBase + { + public WhenConfiguringNameMatching(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldUseACustomPrefix() + { + return RunTest(async (context, mapper) => + { + mapper.WhenMapping.UseNamePrefix("_"); + + var names = new PublicStringNames + { + _Value = "123", + _Value_ = "456", + Value_ = "789" + }; + + await context.StringNameItems.AddAsync(names); + await context.SaveChangesAsync(); + + var stringDto = await context + .StringNameItems + .ProjectUsing(mapper) + .To() + .FirstAsync(); + + stringDto.Value.ShouldBe("123"); + }); + } + + [Fact] + public Task ShouldUseACustomSuffix() + { + return RunTest(async (context, mapper) => + { + mapper.WhenMapping.UseNameSuffix("_"); + + var names = new PublicStringNames + { + _Value = "123", + _Value_ = "456", + Value_ = "789" + }; + + await context.StringNameItems.AddAsync(names); + await context.SaveChangesAsync(); + + var stringDto = await context + .StringNameItems + .ProjectUsing(mapper) + .To() + .FirstAsync(); + + stringDto.Value.ShouldBe("789"); + }); + } + + [Fact] + public Task ShouldUseACustomNamingPattern() + { + return RunTest(async (context, mapper) => + { + mapper.WhenMapping.UseNamePattern("^_(.+)_$"); + + var names = new PublicStringNames + { + _Value = "123", + _Value_ = "456", + Value_ = "789" + }; + + await context.StringNameItems.AddAsync(names); + await context.SaveChangesAsync(); + + var stringDto = await context + .StringNameItems + .ProjectUsing(mapper) + .To() + .FirstAsync(); + + stringDto.Value.ShouldBe("456"); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringObjectCreation.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringObjectCreation.cs new file mode 100644 index 000000000..f37d166c1 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringObjectCreation.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringObjectCreation : WhenConfiguringObjectCreation + { + public WhenConfiguringObjectCreation(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldUseAConditionalObjectFactory() => RunShouldUseAConditionalObjectFactory(); + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringObjectTracking.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringObjectTracking.cs new file mode 100644 index 000000000..26744e5b9 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringObjectTracking.cs @@ -0,0 +1,49 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using Microsoft.EntityFrameworkCore; + using Orms.Infrastructure; + using TestClasses; + using Xunit; + + public class WhenConfiguringObjectTracking : OrmTestClassBase + { + public WhenConfiguringObjectTracking(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldIgnoreObjectTracking() + { + return RunTest(async (context, mapper) => + { + var circle1 = new Circle { Diameter = 1 }; + var circle2 = new Circle { Diameter = 2 }; + var circle3 = new Circle { Diameter = 3 }; + + await context.Shapes.AddRangeAsync(circle1, circle2, circle3); + await context.SaveChangesAsync(); + + mapper.WhenMapping + .To() + .MaintainIdentityIntegrity(); + + var circleVms = await context + .Shapes + .ProjectUsing(mapper) + .To() + .OrderBy(c => c.Diameter) + .ToArrayAsync(); + + circleVms.Length.ShouldBe(3); + + circleVms.First().Diameter.ShouldBe(1); + circleVms.Second().Diameter.ShouldBe(2); + circleVms.Third().Diameter.ShouldBe(3); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringStringFormatting.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringStringFormatting.cs new file mode 100644 index 000000000..b21e60195 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenConfiguringStringFormatting.cs @@ -0,0 +1,24 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenConfiguringStringFormatting : WhenConfiguringStringFormatting + { + public WhenConfiguringStringFormatting(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldFormatDateTimes() => DoShouldFormatDateTimes(); + + [Fact] + public Task ShouldFormatDecimals() => DoShouldFormatDecimals(); + + [Fact] + public Task ShouldFormatDoubles() => DoShouldFormatDoubles(); + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenIgnoringMembers.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenIgnoringMembers.cs new file mode 100644 index 000000000..a4fd70d27 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenIgnoringMembers.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration +{ + using Infrastructure; + using Orms.Configuration; + + public class WhenIgnoringMembers : WhenIgnoringMembers + { + public WhenIgnoringMembers(InMemoryEfCore2TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenMappingToNull.cs b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenMappingToNull.cs new file mode 100644 index 000000000..ada6d48eb --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Configuration/WhenMappingToNull.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Configuration; + using Xunit; + + public class WhenMappingToNull : WhenMappingToNull + { + public WhenMappingToNull(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldApplyAUserConfiguration() => RunShouldApplyAUserConfiguration(); + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Infrastructure/EfCore2DbSetWrapper.cs b/AgileMapper.UnitTests.Orms.EfCore2/Infrastructure/EfCore2DbSetWrapper.cs new file mode 100644 index 000000000..cfc6da522 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Infrastructure/EfCore2DbSetWrapper.cs @@ -0,0 +1,29 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Infrastructure +{ + using System; + using System.Linq.Expressions; + using System.Threading.Tasks; + using Microsoft.EntityFrameworkCore; + using Orms.Infrastructure; + + public class EfCore2DbSetWrapper : DbSetWrapperBase + where TEntity : class + { + private readonly DbSet _dbSet; + + public EfCore2DbSetWrapper(DbContext context) + : base(context.Set()) + { + _dbSet = context.Set(); + } + + public override void Include(Expression> navigationPropertyPath) + => _dbSet.Include(navigationPropertyPath); + + public override Task Add(TEntity itemToAdd) => _dbSet.AddAsync(itemToAdd); + + public override Task AddRange(TEntity[] itemsToAdd) => _dbSet.AddRangeAsync(itemsToAdd); + + public override void Clear() => _dbSet.RemoveRange(_dbSet); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Infrastructure/EfCore2TestDbContext.cs b/AgileMapper.UnitTests.Orms.EfCore2/Infrastructure/EfCore2TestDbContext.cs new file mode 100644 index 000000000..ee1e20445 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Infrastructure/EfCore2TestDbContext.cs @@ -0,0 +1,161 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Infrastructure +{ + using System.Threading.Tasks; + using Microsoft.EntityFrameworkCore; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Debug; + using Orms.Infrastructure; + using TestClasses; + + public class EfCore2TestDbContext : DbContext, ITestDbContext + { + private static readonly DbContextOptions _inMemoryOptions = + new DbContextOptionsBuilder() + .UseLoggerFactory(new LoggerFactory(new[] { new DebugLoggerProvider() })) + .UseInMemoryDatabase(databaseName: "EfCore2TestDb") + .Options; + + public EfCore2TestDbContext() + : this(_inMemoryOptions) + { + } + + protected EfCore2TestDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Animals { get; set; } + + public DbSet Shapes { get; set; } + + public DbSet Companies { get; set; } + + public DbSet Employees { get; set; } + + public DbSet Categories { get; set; } + + public DbSet Products { get; set; } + + public DbSet Accounts { get; set; } + + public DbSet Persons { get; set; } + + public DbSet
Addresses { get; set; } + + public DbSet Rotas { get; set; } + + public DbSet RotaEntries { get; set; } + + public DbSet Orders { get; set; } + + public DbSet OrderItems { get; set; } + + public DbSet BoolItems { get; set; } + + public DbSet ByteItems { get; set; } + + public DbSet ShortItems { get; set; } + + public DbSet IntItems { get; set; } + + public DbSet LongItems { get; set; } + + public DbSet DecimalItems { get; set; } + + public DbSet DoubleItems { get; set; } + + public DbSet DateTimeItems { get; set; } + + public DbSet StringItems { get; set; } + + public DbSet StringNameItems { get; set; } + + public DbSet TitleItems { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasOne(c => c.Ceo) + .WithOne(e => e.Company) + .HasForeignKey(e => e.CompanyId); + + modelBuilder + .Entity() + .HasOne(c => c.ParentCategory) + .WithMany(c => c.SubCategories) + .HasForeignKey(c => c.ParentCategoryId); + + modelBuilder.Entity(); + modelBuilder.Entity(); + + modelBuilder.Entity() + .HasKey(aa => new { aa.AccountId, aa.AddressId }); + + modelBuilder.Entity() + .HasOne(aa => aa.Account) + .WithMany(a => a.DeliveryAddresses) + .HasForeignKey(aa => aa.AccountId); + + base.OnModelCreating(modelBuilder); + } + + #region ITestDbContext Members + + IDbSetWrapper ITestDbContext.Animals => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Shapes => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Companies => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Employees => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Categories => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Products => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Accounts => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Persons => new EfCore2DbSetWrapper(this); + + IDbSetWrapper
ITestDbContext.Addresses => new EfCore2DbSetWrapper
(this); + + IDbSetWrapper ITestDbContext.Rotas => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.RotaEntries => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.Orders => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.OrderItems => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.BoolItems => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.ByteItems => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.ShortItems => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.IntItems => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.LongItems => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.DecimalItems => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.DoubleItems => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.DateTimeItems => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.StringItems => new EfCore2DbSetWrapper(this); + + IDbSetWrapper ITestDbContext.TitleItems => new EfCore2DbSetWrapper(this); + + Task ITestDbContext.SaveChanges() + { + StringNameItems.RemoveRange(StringNameItems); + + return SaveChangesAsync(); + } + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Infrastructure/InMemoryEfCore2TestContext.cs b/AgileMapper.UnitTests.Orms.EfCore2/Infrastructure/InMemoryEfCore2TestContext.cs new file mode 100644 index 000000000..4e2ce39c7 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Infrastructure/InMemoryEfCore2TestContext.cs @@ -0,0 +1,8 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Infrastructure +{ + using Orms.Infrastructure; + + public class InMemoryEfCore2TestContext : InMemoryOrmTestContext + { + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Infrastructure/InMemoryEfCore2TestDefinition.cs b/AgileMapper.UnitTests.Orms.EfCore2/Infrastructure/InMemoryEfCore2TestDefinition.cs new file mode 100644 index 000000000..5c2125fce --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Infrastructure/InMemoryEfCore2TestDefinition.cs @@ -0,0 +1,10 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Infrastructure +{ + using Orms; + using Xunit; + + [CollectionDefinition(TestConstants.OrmCollectionName)] + public class InMemoryEfCore2TestDefinition : ICollectionFixture + { + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Properties/AssemblyInfo.cs b/AgileMapper.UnitTests.Orms.EfCore2/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..eb6d0e1de --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AgileMapper.UnitTests.Orms.EfCore2")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9abbd6a9-5f95-442a-88db-3fc1ebf374a7")] \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/Recursion/WhenProjectingCircularReferences.cs b/AgileMapper.UnitTests.Orms.EfCore2/Recursion/WhenProjectingCircularReferences.cs new file mode 100644 index 000000000..1770d8411 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/Recursion/WhenProjectingCircularReferences.cs @@ -0,0 +1,29 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.Recursion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.Recursion; + using Xunit; + + public class WhenProjectingCircularReferences : + WhenProjectingCircularReferences, + IOneToManyRecursionProjectorTest + { + public WhenProjectingCircularReferences(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAOneToManyRelationshipToDefaultRecursionDepth() + => DoShouldProjectAOneToManyRelationshipToDefaultRecursionDepth(); + + [Fact] + public Task ShouldProjectAOneToManyRelationshipToFirstRecursionDepth() + => DoShouldProjectAOneToManyRelationshipToFirstRecursionDepth(); + + [Fact] + public Task ShouldProjectAOneToManyRelationshipToSecondRecursionDepth() + => DoShouldProjectAOneToManyRelationshipToSecondRecursionDepth(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToBools.cs b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToBools.cs new file mode 100644 index 000000000..16dd78e09 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToBools.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.SimpleTypeConversion +{ + using Infrastructure; + using Orms.SimpleTypeConversion; + + public class WhenConvertingToBools : WhenConvertingToBools + { + public WhenConvertingToBools(InMemoryEfCore2TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToDateTimes.cs b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToDateTimes.cs new file mode 100644 index 000000000..a4ed23a25 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToDateTimes.cs @@ -0,0 +1,30 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToDateTimes : + WhenConvertingToDateTimes, + IStringConverterTest, + IStringConversionValidationFailureTest + { + public WhenConvertingToDateTimes(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAParseableString() + => RunShouldProjectAParseableStringToADateTime(); + + [Fact] + public Task ShouldProjectANullString() + => RunShouldProjectANullStringToADateTime(); + + [Fact] + public Task ShouldErrorProjectingAnUnparseableString() + => RunShouldErrorProjectingAnUnparseableStringToADateTime(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToDoubles.cs b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToDoubles.cs new file mode 100644 index 000000000..0a8b0a831 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToDoubles.cs @@ -0,0 +1,30 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToDoubles : + WhenConvertingToDoubles, + IStringConverterTest, + IStringConversionValidationFailureTest + { + public WhenConvertingToDoubles(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAParseableString() + => RunShouldProjectAParseableStringToADouble(); + + [Fact] + public Task ShouldProjectANullString() + => RunShouldProjectANullStringToADouble(); + + [Fact] + public Task ShouldErrorProjectingAnUnparseableString() + => RunShouldErrorProjectingAnUnparseableStringToADouble(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToEnums.cs b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToEnums.cs new file mode 100644 index 000000000..405f6b8e2 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToEnums.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.SimpleTypeConversion +{ + using Infrastructure; + using Orms.SimpleTypeConversion; + + public class WhenConvertingToEnums : WhenConvertingToEnums + { + public WhenConvertingToEnums(InMemoryEfCore2TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToGuids.cs b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToGuids.cs new file mode 100644 index 000000000..9c8f4aeab --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToGuids.cs @@ -0,0 +1,25 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToGuids : + WhenConvertingToGuids, + IStringConverterTest + { + public WhenConvertingToGuids(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAParseableString() + => RunShouldProjectAParseableStringToAGuid(); + + [Fact] + public Task ShouldProjectANullString() + => RunShouldProjectANullStringToAGuid(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToInts.cs b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToInts.cs new file mode 100644 index 000000000..755e7f71d --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToInts.cs @@ -0,0 +1,28 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToInts : + WhenConvertingToInts, + IStringConverterTest, + IStringConversionValidationFailureTest + { + public WhenConvertingToInts(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAParseableString() => RunShouldProjectAParseableStringToAnInt(); + + [Fact] + public Task ShouldProjectANullString() => RunShouldProjectANullStringToAnInt(); + + [Fact] + public Task ShouldErrorProjectingAnUnparseableString() + => RunShouldErrorProjectingAnUnparseableStringToAnInt(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToStrings.cs b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToStrings.cs new file mode 100644 index 000000000..a7be8a6be --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/SimpleTypeConversion/WhenConvertingToStrings.cs @@ -0,0 +1,24 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms.SimpleTypeConversion; + using Xunit; + + public class WhenConvertingToStrings : WhenConvertingToStrings + { + public WhenConvertingToStrings(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectADecimalToAString() => DoShouldProjectADecimalToAString(); + + [Fact] + public Task ShouldProjectADoubleToAString() => DoShouldProjectADoubleToAString(); + + [Fact] + public Task ShouldProjectADateTimeToAString() => DoShouldProjectADateTimeToAString(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs b/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs new file mode 100644 index 000000000..067d5da28 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/WhenCreatingProjections.cs @@ -0,0 +1,52 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2 +{ + using System.Threading.Tasks; + using Infrastructure; + using Microsoft.EntityFrameworkCore; + using MoreTestClasses; + using Orms.Infrastructure; + using TestClasses; + using Xunit; + + public class WhenCreatingProjections : OrmTestClassBase + { + public WhenCreatingProjections(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldReuseACachedProjectionMapper() + { + return RunTest(async mapper => + { + using (var context1 = new EfCore2TestDbContext()) + { + var stringDtos = await context1 + .StringItems + .ProjectUsing(mapper).To() + .ToListAsync(); + + stringDtos.ShouldBeEmpty(); + } + + mapper.RootMapperCountShouldBeOne(); + + using (var context2 = new EfCore2TestDbContext()) + { + context2.StringItems.Add(new PublicString { Id = 1, Value = "New!" }); + await context2.SaveChangesAsync(); + + var moreStringDtos = await context2 + .StringItems + .ProjectUsing(mapper).To() + .ToArrayAsync(); + + moreStringDtos.ShouldHaveSingleItem(); + } + + mapper.RootMapperCountShouldBeOne(); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/WhenProjectingFlatTypes.cs b/AgileMapper.UnitTests.Orms.EfCore2/WhenProjectingFlatTypes.cs new file mode 100644 index 000000000..b7aa3bae2 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/WhenProjectingFlatTypes.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2 +{ + using System.Threading.Tasks; + using Infrastructure; + using Orms; + using Xunit; + + public class WhenProjectingFlatTypes : WhenProjectingFlatTypes + { + public WhenProjectingFlatTypes(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectStructCtorParameters() => RunShouldProjectStructCtorParameters(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/WhenProjectingToComplexTypeMembers.cs b/AgileMapper.UnitTests.Orms.EfCore2/WhenProjectingToComplexTypeMembers.cs new file mode 100644 index 000000000..ed2a99e3e --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/WhenProjectingToComplexTypeMembers.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2 +{ + using Infrastructure; + using Orms; + + public class WhenProjectingToComplexTypeMembers : WhenProjectingToComplexTypeMembers + { + public WhenProjectingToComplexTypeMembers(InMemoryEfCore2TestContext context) + : base(context) + { + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/WhenProjectingToEnumerableMembers.cs b/AgileMapper.UnitTests.Orms.EfCore2/WhenProjectingToEnumerableMembers.cs new file mode 100644 index 000000000..8e956dbea --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/WhenProjectingToEnumerableMembers.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2 +{ + using System.Threading.Tasks; + using Infrastructure; + using Xunit; + + public class WhenProjectingToEnumerableMembers : WhenProjectingToEnumerableMembers + { + public WhenProjectingToEnumerableMembers(InMemoryEfCore2TestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectToAComplexTypeCollectionMember() + => RunShouldProjectToAComplexTypeCollectionMember(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/WhenProjectingToFlatTypes.cs b/AgileMapper.UnitTests.Orms.EfCore2/WhenProjectingToFlatTypes.cs new file mode 100644 index 000000000..e32bcc557 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/WhenProjectingToFlatTypes.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2 +{ + using Infrastructure; + + public class WhenProjectingToFlatTypes : WhenProjectingToFlatTypes + { + public WhenProjectingToFlatTypes(InMemoryEfCore2TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/WhenValidatingProjections.cs b/AgileMapper.UnitTests.Orms.EfCore2/WhenValidatingProjections.cs new file mode 100644 index 000000000..9f2909606 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/WhenValidatingProjections.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2 +{ + using Infrastructure; + + public class WhenValidatingProjections : WhenValidatingProjections + { + public WhenValidatingProjections(InMemoryEfCore2TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/WhenViewingProjectionPlans.cs b/AgileMapper.UnitTests.Orms.EfCore2/WhenViewingProjectionPlans.cs new file mode 100644 index 000000000..494dbab5b --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/WhenViewingProjectionPlans.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.EfCore2 +{ + using Infrastructure; + + public class WhenViewingProjectionPlans : WhenViewingProjectionPlans + { + public WhenViewingProjectionPlans(InMemoryEfCore2TestContext context) + : base(context) + { + } + } +} diff --git a/AgileMapper.UnitTests.Orms.EfCore2/app.config b/AgileMapper.UnitTests.Orms.EfCore2/app.config new file mode 100644 index 000000000..f278537bb --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/app.config @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms.EfCore2/packages.config b/AgileMapper.UnitTests.Orms.EfCore2/packages.config new file mode 100644 index 000000000..de00ae8a4 --- /dev/null +++ b/AgileMapper.UnitTests.Orms.EfCore2/packages.config @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/AgileMapper.UnitTests.Orms.csproj b/AgileMapper.UnitTests.Orms/AgileMapper.UnitTests.Orms.csproj new file mode 100644 index 000000000..6e3918f1c --- /dev/null +++ b/AgileMapper.UnitTests.Orms/AgileMapper.UnitTests.Orms.csproj @@ -0,0 +1,203 @@ + + + + + + Debug + AnyCPU + {66522D44-19F5-4AF5-9D43-483A3CD6F958} + Library + Properties + AgileObjects.AgileMapper.UnitTests.Orms + AgileObjects.AgileMapper.UnitTests.Orms + v4.6.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + False + UnitTest + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + true + + + ..\AgileMapper.snk + + + + + + + + + + + + ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll + + + ..\packages\xunit.assert.2.3.1\lib\netstandard1.1\xunit.assert.dll + + + ..\packages\xunit.extensibility.core.2.3.1\lib\netstandard1.1\xunit.core.dll + + + ..\packages\xunit.extensibility.execution.2.3.1\lib\net452\xunit.execution.desktop.dll + + + + + Properties\CommonAssemblyInfo.cs + + + Properties\VersionInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {049E1EE5-48CE-441A-B166-3CF6BEC17957} + AgileMapper.UnitTests.MoreTestClasses + + + {a3f2d405-8c0b-4033-9ec5-1b64007593fb} + AgileMapper.UnitTests + + + {46d95c53-b4cb-4ee7-9573-5d3ef96099c0} + AgileMapper + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs b/AgileMapper.UnitTests.Orms/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs new file mode 100644 index 000000000..15f222dd8 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Configuration/Inline/WhenConfiguringConstructorDataSourcesInline.cs @@ -0,0 +1,43 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Configuration.Inline +{ + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + + public abstract class WhenConfiguringConstructorDataSourcesInline : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConfiguringConstructorDataSourcesInline(ITestContext context) + : base(context) + { + } + + #region Project -> Ctor Parameter by Type Inline + + protected Task RunShouldApplyAConfiguredConstantByParameterTypeInline() + => RunTest(DoShouldApplyAConfiguredConstantByParameterType); + + protected Task RunShouldErrorApplyingAConfiguredConstantByParameterTypeInline() + => RunTestAndExpectThrow(DoShouldApplyAConfiguredConstantByParameterType); + + private static async Task DoShouldApplyAConfiguredConstantByParameterType(TOrmContext context) + { + var product = new Product { Name = "Prod.1" }; + + await context.Products.Add(product); + await context.SaveChanges(); + + var productDto = context + .Products + .Project().To(cfg => cfg + .Map("GRAPES!") + .ToCtor()) + .ShouldHaveSingleItem(); + + productDto.ProductId.ShouldBe(product.ProductId); + productDto.Name.ShouldBe("GRAPES!"); + } + + #endregion + } +} diff --git a/AgileMapper.UnitTests.Orms/Configuration/Inline/WhenConfiguringDataSourcesInline.cs b/AgileMapper.UnitTests.Orms/Configuration/Inline/WhenConfiguringDataSourcesInline.cs new file mode 100644 index 000000000..e9de614f5 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Configuration/Inline/WhenConfiguringDataSourcesInline.cs @@ -0,0 +1,70 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Configuration.Inline +{ + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + + public abstract class WhenConfiguringDataSourcesInline : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConfiguringDataSourcesInline(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldApplyAConfiguredConstantInline() + { + return RunTest(async context => + { + var product = new Product { Name = "P1" }; + + await context.Products.Add(product); + await context.SaveChanges(); + + var productDto = context + .Products + .Project().To(cfg => cfg + .Map("PROD!!") + .To(dto => dto.Name)) + .ShouldHaveSingleItem(); + + productDto.ProductId.ShouldBe(product.ProductId); + productDto.Name.ShouldBe("PROD!!"); + }); + } + + [Fact] + public Task ShouldApplyAConfiguredConstantToANestedMemberInline() + { + return RunTest(async context => + { + var person = new Person + { + Name = "Person One", + Address = new Address { Line1 = "Line One", Postcode = "Postcode" } + }; + + await context.Persons.Add(person); + await context.SaveChanges(); + + var personDto = context + .Persons + .Project().To(cfg => cfg + .WhenMapping + .ProjectionsTo() + .Map("LINE TWO?!") + .To(a => a.Line2)) + .ShouldHaveSingleItem(); + + personDto.Id.ShouldBe(person.PersonId); + personDto.Name.ShouldBe("Person One"); + personDto.Address.ShouldNotBeNull(); + personDto.Address.Line1.ShouldBe("Line One"); + personDto.Address.Line2.ShouldBe("LINE TWO?!"); + personDto.Address.Postcode.ShouldBe("Postcode"); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringConstructorDataSources.cs b/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringConstructorDataSources.cs new file mode 100644 index 000000000..4c56e1eb1 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringConstructorDataSources.cs @@ -0,0 +1,78 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Configuration +{ + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + + public abstract class WhenConfiguringConstructorDataSources : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConfiguringConstructorDataSources(ITestContext context) + : base(context) + { + } + + #region Project -> Ctor Parameter by Type + + protected Task RunShouldApplyAConfiguredConstantByParameterType() + => RunTest(DoShouldApplyAConfiguredConstantByParameterType); + + protected Task RunShouldErrorApplyingAConfiguredConstantByParameterType() + => RunTestAndExpectThrow(DoShouldApplyAConfiguredConstantByParameterType); + + private static async Task DoShouldApplyAConfiguredConstantByParameterType(TOrmContext context, IMapper mapper) + { + var product = new Product { Name = "Prod.One" }; + + await context.Products.Add(product); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .Map("Bananas!") + .ToCtor(); + + var productDto = context + .Products + .ProjectUsing(mapper).To() + .ShouldHaveSingleItem(); + + productDto.ProductId.ShouldBe(product.ProductId); + productDto.Name.ShouldBe("Bananas!"); + } + + #endregion + + #region Project -> Ctor Parameter by Name + protected Task RunShouldApplyAConfiguredExpressionByParameterName() + => RunTest(DoShouldApplyAConfiguredExpressionByParameterName); + + protected Task RunShouldErrorApplyingAConfiguredExpressionByParameterName() + => RunTestAndExpectThrow(DoShouldApplyAConfiguredExpressionByParameterName); + + private static async Task DoShouldApplyAConfiguredExpressionByParameterName(TOrmContext context, IMapper mapper) + { + var product = new Product { Name = "Prod.One" }; + + await context.Products.Add(product); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .Map(p => "2 * 3 = " + (2 * 3)) + .ToCtor("name"); + + var productDto = context + .Products + .ProjectUsing(mapper).To() + .ShouldHaveSingleItem(); + + productDto.ProductId.ShouldBe(product.ProductId); + productDto.Name.ShouldBe("2 * 3 = 6"); + } + + #endregion + } +} diff --git a/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringDataSources.cs b/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringDataSources.cs new file mode 100644 index 000000000..adefa11d8 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringDataSources.cs @@ -0,0 +1,321 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Configuration +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + + public abstract class WhenConfiguringDataSources : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConfiguringDataSources(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldApplyAConfiguredConstant() + { + return RunTest(async (context, mapper) => + { + var product = new Product { Name = "P1" }; + + await context.Products.Add(product); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .Map("PRODUCT") + .To(dto => dto.Name); + + var productDto = context + .Products + .ProjectUsing(mapper).To() + .ShouldHaveSingleItem(); + + productDto.ProductId.ShouldBe(product.ProductId); + productDto.Name.ShouldBe("PRODUCT"); + }); + } + + [Fact] + public Task ShouldConditionallyApplyAConfiguredConstant() + { + return RunTest(async (context, mapper) => + { + var product1 = new Product { Name = "P1" }; + var product2 = new Product { Name = "P2" }; + + await context.Products.AddRange(product1, product2); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .If(p => p.Name == "P2") + .Map("PRODUCT!?") + .To(dto => dto.Name); + + var productDtos = context + .Products + .ProjectUsing(mapper).To() + .OrderBy(p => p.ProductId) + .ToArray(); + + productDtos.Length.ShouldBe(2); + + productDtos.First().ProductId.ShouldBe(product1.ProductId); + productDtos.First().Name.ShouldBe("P1"); + + productDtos.Second().ProductId.ShouldBe(product2.ProductId); + productDtos.Second().Name.ShouldBe("PRODUCT!?"); + }); + } + + [Fact] + public Task ShouldApplyAConfiguredConstantToANestedMember() + { + return RunTest(async (context, mapper) => + { + var person = new Person + { + Name = "Person 1", + Address = new Address { Line1 = "Line 1", Postcode = "Postcode" } + }; + + await context.Persons.Add(person); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .Map("LINE ONE!?") + .To(dto => dto.Address.Line1); + + var personDto = context + .Persons + .ProjectUsing(mapper).To() + .ShouldHaveSingleItem(); + + personDto.Id.ShouldBe(person.PersonId); + personDto.Name.ShouldBe("Person 1"); + personDto.Address.ShouldNotBeNull(); + personDto.Address.Line1.ShouldBe("LINE ONE!?"); + personDto.Address.Postcode.ShouldBe("Postcode"); + }); + } + + protected Task DoShouldApplyAConfiguredMember() + { + return RunTest(async (context, mapper) => + { + var product = new Product { Name = "P1" }; + + await context.Products.Add(product); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .Map(p => p.ProductId) + .To(dto => dto.Name); + + var personDto = context + .Products + .ProjectUsing(mapper).To() + .ShouldHaveSingleItem(); + + personDto.Id.ShouldBe(product.ProductId); + personDto.Name.ShouldBe(product.ProductId); + }); + } + + protected Task DoShouldApplyMultipleConfiguredMembers() + { + return RunTest(async (context, mapper) => + { + var product = new Product { Name = "Product1" }; + + await context.Products.Add(product); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .Map(p => p.ProductId) + .To(dto => dto.Name) + .And + .Map(p => p.Name) + .To(p => p.Address.Line1); + + var personDto = context + .Products + .ProjectUsing(mapper).To() + .ShouldHaveSingleItem(); + + personDto.Id.ShouldBe(product.ProductId); + personDto.Name.ShouldBe(product.ProductId); + personDto.Address.ShouldNotBeNull(); + personDto.Address.Line1.ShouldBe("Product1"); + }); + } + + [Fact] + public Task ShouldConditionallyApplyAConfiguredMember() + { + return RunTest(async (context, mapper) => + { + var product1 = new Product { Name = "P.1" }; + var product2 = new Product { Name = "P.2" }; + + await context.Products.AddRange(product1, product2); + await context.SaveChanges(); + + var product1Id = product1.ProductId; + + mapper.WhenMapping + .From() + .ProjectedTo() + .If(p => p.ProductId > product1Id) + .Map(p => p.Name + "?!") + .To(dto => dto.Name); + + var productDtos = context + .Products + .ProjectUsing(mapper).To() + .OrderBy(p => p.ProductId) + .ToArray(); + + productDtos.Length.ShouldBe(2); + + productDtos.First().ProductId.ShouldBe(product1.ProductId); + productDtos.First().Name.ShouldBe("P.1"); + + productDtos.Second().ProductId.ShouldBe(product2.ProductId); + productDtos.Second().Name.ShouldBe("P.2?!"); + }); + } + + [Fact] + public Task ShouldApplyConditionalAndUnconditionalDataSourcesInOrder() + { + return RunTest(async (context, mapper) => + { + var product1 = new Product { Name = "P.1" }; + var product2 = new Product { Name = "P.2" }; + + await context.Products.AddRange(product1, product2); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .Map(p => p.Name + '!') + .To(dto => dto.Name) + .But + .If(p => p.Name.Contains("1")) + .Map(p => p.Name + '?') + .To(dto => dto.Name); + + var productDtos = context + .Products + .ProjectUsing(mapper).To() + .OrderBy(p => p.ProductId) + .ToArray(); + + productDtos.Length.ShouldBe(2); + + productDtos.First().ProductId.ShouldBe(product1.ProductId); + productDtos.First().Name.ShouldBe("P.1?"); + + productDtos.Second().ProductId.ShouldBe(product2.ProductId); + productDtos.Second().Name.ShouldBe("P.2!"); + }); + } + + [Fact] + public Task ShouldHandleANullMemberInACondition() + { + return RunTest(async (context, mapper) => + { + var person1 = new Person { Name = "Frank", Address = new Address { Line1 = "Philly" } }; + var person2 = new Person { Name = "Dee" }; + + await context.Persons.AddRange(person1, person2); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .If(p => p.Address.Line2 == null) + .Map("None") + .To(dto => dto.AddressLine2); + + var personDtos = context + .Persons + .ProjectUsing(mapper).To() + .OrderBy(p => p.Id) + .ToArray(); + + personDtos.Length.ShouldBe(2); + + personDtos.First().Id.ShouldBe(person1.PersonId); + personDtos.First().Name.ShouldBe("Frank"); + personDtos.First().AddressId.ShouldBe(person1.Address.AddressId); + personDtos.First().AddressLine1.ShouldBe("Philly"); + personDtos.First().AddressLine2.ShouldBe("None"); + personDtos.First().AddressPostcode.ShouldBeNull(); + + personDtos.Second().Id.ShouldBe(person2.PersonId); + personDtos.Second().Name.ShouldBe("Dee"); + personDtos.Second().AddressId.ShouldBeDefault(); + personDtos.Second().AddressLine1.ShouldBeNull(); + personDtos.Second().AddressLine2.ShouldBe("None"); + personDtos.Second().AddressPostcode.ShouldBeNull(); + }); + } + + [Fact] + public Task ShouldSupportMultipleDivergedMappers() + { + return RunTest(async (context, mapper1) => + { + var address = new Address { Line1 = "Philly", Postcode = "PH1 1LY" }; + + await context.Addresses.Add(address); + await context.SaveChanges(); + + using (var mapper2 = Mapper.CreateNew()) + { + mapper2.WhenMapping + .From
() + .ProjectedTo() + .Map(p => p.Line1) + .To(dto => dto.Line2); + + var addressDto1 = context + .Addresses + .ProjectUsing(mapper1).To() + .ShouldHaveSingleItem(); + + addressDto1.Id.ShouldBe(address.AddressId); + addressDto1.Line1.ShouldBe("Philly"); + addressDto1.Line2.ShouldBeNull(); + addressDto1.Postcode.ShouldBe("PH1 1LY"); + + var addressDto2 = context + .Addresses + .ProjectUsing(mapper2).To() + .First(); + + addressDto2.Id.ShouldBe(address.AddressId); + addressDto2.Line1.ShouldBe("Philly"); + addressDto2.Line2.ShouldBe("Philly"); + addressDto2.Postcode.ShouldBe("PH1 1LY"); + } + }); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringDerivedTypes.cs b/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringDerivedTypes.cs new file mode 100644 index 000000000..8cf8f2e60 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringDerivedTypes.cs @@ -0,0 +1,192 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Configuration +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using static TestClasses.Animal.AnimalType; + + public abstract class WhenConfiguringDerivedTypes : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConfiguringDerivedTypes(ITestContext context) + : base(context) + { + } + + #region Project -> Derived Types + + protected Task RunShouldProjectToConfiguredDerivedTypes() + => RunTest(DoShouldProjectToConfiguredDerivedTypes); + + protected Task RunShouldErrorProjectingToConfiguredDerivedTypes() + => RunTestAndExpectThrow(DoShouldProjectToConfiguredDerivedTypes); + + private static async Task DoShouldProjectToConfiguredDerivedTypes(TOrmContext context, IMapper mapper) + { + var fido = new Animal { Type = Dog, Name = "Fido", Sound = "Bark" }; + var nelly = new Animal { Type = Elephant, Name = "Nelly" }; + var kaa = new Animal { Type = Snake, Name = "Kaa" }; + var sparkles = new Animal { Name = "Sparkles", Sound = "Wheeeee!" }; + + await context.Animals.AddRange(fido, nelly, kaa, sparkles); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .If(a => a.Type == Dog) + .MapTo() + .And + .If(a => a.Type == Elephant) + .MapTo() + .And + .If(a => a.Type == Snake) + .MapTo(); + + var animalDtos = context + .Animals + .ProjectUsing(mapper).To() + .OrderBy(a => a != null ? a.Id : 1000) + .ToArray(); + + animalDtos.First().ShouldBeOfType(); + animalDtos.First().Id.ShouldBe(fido.Id); + animalDtos.First().Name.ShouldBe("Fido"); + animalDtos.First().Sound.ShouldBe("Woof"); + + animalDtos.Second().ShouldBeOfType(); + animalDtos.Second().Id.ShouldBe(nelly.Id); + animalDtos.Second().Name.ShouldBe("Nelly"); + animalDtos.Second().Sound.ShouldBe("Trumpet"); + + animalDtos.Third().ShouldBeOfType(); + animalDtos.Third().Id.ShouldBe(kaa.Id); + animalDtos.Third().Name.ShouldBe("Kaa"); + animalDtos.Third().Sound.ShouldBe("Hiss"); + + animalDtos.Fourth().ShouldBeNull(); + } + + #endregion + + #region Project -> Fallback Derived Type + + protected Task RunShouldProjectToAFallbackDerivedType() + => RunTest(DoShouldProjectToAFallbackDerivedType); + + protected Task RunShouldErrorProjectingToAFallbackDerivedType() + => RunTestAndExpectThrow(DoShouldProjectToAFallbackDerivedType); + + private static async Task DoShouldProjectToAFallbackDerivedType(TOrmContext context, IMapper mapper) + { + var fido = new Animal { Type = Dog, Name = "Fido", Sound = "Bark" }; + var nelly = new Animal { Type = Elephant, Name = "Nelly", Sound = "HrrrRRRRRRR" }; + var sparkles = new Animal { Name = "Sparkles", Sound = "Wheeeee!" }; + + await context.Animals.AddRange(fido, nelly, sparkles); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .If(a => a.Type == Dog) + .MapTo() + .And + .If(a => a.Type != Dog) + .MapTo(); + + var animalDtos = context + .Animals + .ProjectUsing(mapper).To() + .OrderBy(a => a.Id) + .ToArray(); + + animalDtos.First().ShouldBeOfType(); + animalDtos.First().Id.ShouldBe(fido.Id); + animalDtos.First().Name.ShouldBe("Fido"); + animalDtos.First().Sound.ShouldBe("Woof"); + + animalDtos.Second().ShouldBeOfType(); + animalDtos.Second().Id.ShouldBe(nelly.Id); + animalDtos.Second().Name.ShouldBe("Nelly"); + animalDtos.Second().Sound.ShouldBe("HrrrRRRRRRR"); + + animalDtos.Third().ShouldBeOfType(); + animalDtos.Third().Id.ShouldBe(sparkles.Id); + animalDtos.Third().Name.ShouldBe("Sparkles"); + animalDtos.Third().Sound.ShouldBe("Wheeeee!"); + } + + #endregion + + #region Project -> No Derived Source Types + + protected Task RunShouldNotAttemptToApplyDerivedSourceTypePairing() + => RunTest(DoShouldNotAttemptToApplyDerivedSourceTypePairing); + + protected Task RunShouldErrorAttemptingToNotApplyDerivedSourceTypePairing() + => RunTestAndExpectThrow(DoShouldNotAttemptToApplyDerivedSourceTypePairing); + + private static async Task DoShouldNotAttemptToApplyDerivedSourceTypePairing(TOrmContext context, IMapper mapper) + { + var square = new Square { Name = "Square", NumberOfSides = 4, SideLength = 10 }; + var circle = new Circle { Name = "Circle", Diameter = 5 }; + + var shapes = new Shape[] { square, circle }; + + var mappedShapeVms = mapper.Map(shapes).ToANew(); + + mappedShapeVms.Length.ShouldBe(2); + + mappedShapeVms.First().ShouldBeOfType(); + mappedShapeVms.Second().ShouldBeOfType(); + + await context.Shapes.AddRange(square, circle); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .If(s => s.Name == "Square") + .MapTo() + .But + .If(s => s.Name == "Circle") + .MapTo(); + + var shapeVms = context + .Shapes + .ProjectUsing(mapper).To() + .OrderBy(a => a.Id) + .ToArray(); + + shapeVms.Length.ShouldBe(2); + + var squareVm = shapeVms.First() as SquareViewModel; + squareVm.ShouldNotBeNull(); + + // ReSharper disable once PossibleNullReferenceException + squareVm.Id.ShouldBe(square.Id); + squareVm.Name.ShouldBe(square.Name); + squareVm.NumberOfSides.ShouldBe(square.NumberOfSides); + squareVm.SideLength.ShouldBe(square.SideLength); + + var circleVm = shapeVms.Second() as CircleViewModel; + circleVm.ShouldNotBeNull(); + + // ReSharper disable once PossibleNullReferenceException + circleVm.Id.ShouldBe(circle.Id); + circleVm.Name.ShouldBe(circle.Name); + circleVm.Diameter.ShouldBe(circle.Diameter); + + var mappedSquareVm = mapper.Map(square).ToANew(); + + mappedSquareVm.Id.ShouldBe(square.Id); + mappedSquareVm.Name.ShouldBe(square.Name); + mappedSquareVm.NumberOfSides.ShouldBe(square.NumberOfSides); + mappedSquareVm.SideLength.ShouldBe(square.SideLength); + } + + #endregion + } +} diff --git a/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringEnumMapping.cs b/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringEnumMapping.cs new file mode 100644 index 000000000..840326804 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringEnumMapping.cs @@ -0,0 +1,71 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Configuration +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + using PaymentTypeUk = UnitTests.TestClasses.PaymentTypeUk; + using PaymentTypeUs = UnitTests.TestClasses.PaymentTypeUs; + + public abstract class WhenConfiguringEnumMapping : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConfiguringEnumMapping(ITestContext context) : base(context) + { + } + + [Fact] + public Task ShouldPairEnumMembers() + { + return RunTest(async (context, mapper) => + { + var order1 = new OrderUk + { + DatePlaced = DateTime.Now, + PaymentType = PaymentTypeUk.Cheque + }; + + var order2 = new OrderUk + { + DatePlaced = DateTime.Now.AddMinutes(1), + PaymentType = PaymentTypeUk.Cash + }; + + var order3 = new OrderUk + { + DatePlaced = DateTime.Now.AddMinutes(2), + PaymentType = PaymentTypeUk.Card + }; + + await context.Orders.AddRange(order1, order2, order3); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .PairEnum(PaymentTypeUk.Cheque).With(PaymentTypeUs.Check) + .And + .PairEnum(PaymentTypeUk.Card).With(PaymentTypeUs.Check); + + var orderVms = context + .Orders + .ProjectUsing(mapper).To() + .OrderBy(o => o.DatePlaced) + .ToArray(); + + orderVms.Length.ShouldBe(3); + + orderVms.First().DatePlaced.ShouldBe(order1.DatePlaced); + orderVms.First().PaymentType.ShouldBe(PaymentTypeUs.Check); + + orderVms.Second().DatePlaced.ShouldBe(order2.DatePlaced); + orderVms.Second().PaymentType.ShouldBe(PaymentTypeUs.Cash); + + orderVms.Third().DatePlaced.ShouldBe(order3.DatePlaced); + orderVms.Third().PaymentType.ShouldBe(PaymentTypeUs.Check); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringObjectCreation.cs b/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringObjectCreation.cs new file mode 100644 index 000000000..6b490b7ed --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringObjectCreation.cs @@ -0,0 +1,113 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Configuration +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + + public abstract class WhenConfiguringObjectCreation : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConfiguringObjectCreation(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldUseACustomObjectFactory() + { + return RunTest(async (context, mapper) => + { + await context.StringItems.Add(new PublicString { Value = "Ctor!" }); + await context.SaveChanges(); + + mapper.WhenMapping + .ProjectionsTo() + .CreateInstancesUsing(o => new PublicStringDto { Value = "PANTS" }); + + var ctorDto = context + .StringItems + .ProjectUsing(mapper) + .To() + .ShouldHaveSingleItem(); + + ctorDto.Value.ShouldBe("PANTS"); + }); + } + + [Fact] + public Task ShouldUseACustomObjectFactoryForASpecifiedType() + { + return RunTest(async (context, mapper) => + { + var person = new Person + { + Name = "Fatima", + Address = new Address { Line1 = "1", Line2 = "2" } + }; + + await context.Persons.Add(person); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .CreateInstancesOf().Using(p => new AddressDto + { + Line1 = p.Address.Line1 + "!", + Line2 = p.Address.Line2 + "?", + Postcode = "BA7 8RD" + }); + + var personDto = context + .Persons + .ProjectUsing(mapper) + .To() + .ShouldHaveSingleItem(); + + personDto.Address.ShouldNotBeNull(); + personDto.Address.Line1.ShouldBe("1!"); + personDto.Address.Line2.ShouldBe("2?"); + personDto.Address.Postcode.ShouldBe("BA7 8RD"); + }); + } + + #region Project -> Conditional Factory + + protected Task RunShouldUseAConditionalObjectFactory() + => RunTest(DoShouldUseAConditionalObjectFactory); + + protected Task RunShouldErrorUsingAConditionalObjectFactory() + => RunTestAndExpectThrow(DoShouldUseAConditionalObjectFactory); + + private static async Task DoShouldUseAConditionalObjectFactory(TOrmContext context, IMapper mapper) + { + await context.IntItems.AddRange( + new PublicInt { Value = 1 }, + new PublicInt { Value = 2 }, + new PublicInt { Value = 3 }); + + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .If(p => p.Value % 2 == 0) + .CreateInstancesUsing(p => new PublicStringCtorDto((p.Value * 2).ToString())); + + var stringDtos = context + .IntItems + .OrderBy(p => p.Id) + .ProjectUsing(mapper) + .To() + .ToArray(); + + stringDtos.First().Value.ShouldBe("1"); + stringDtos.Second().Value.ShouldBe("4"); + stringDtos.Third().Value.ShouldBe("3"); + } + + #endregion + } +} diff --git a/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringStringFormatting.cs b/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringStringFormatting.cs new file mode 100644 index 000000000..7384cf2a5 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Configuration/WhenConfiguringStringFormatting.cs @@ -0,0 +1,106 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Configuration +{ + using System; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + + public abstract class WhenConfiguringStringFormatting : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConfiguringStringFormatting(ITestContext context) + : base(context) + { + } + + protected Task DoShouldFormatDateTimes(Func expectedDateStringFactory = null) + { + if (expectedDateStringFactory == null) + { + expectedDateStringFactory = d => d.ToString("o"); + } + + return RunTest(async (context, mapper) => + { + mapper.WhenMapping + .StringsFrom(c => c.FormatUsing("o")); + + var source = new PublicDateTime { Value = DateTime.Now }; + var result = mapper.Map(source).ToANew(); + + result.Value.ShouldBe(source.Value.ToString("o")); + + await context.DateTimeItems.Add(source); + await context.SaveChanges(); + + var stringDto = context + .DateTimeItems + .ProjectUsing(mapper) + .To() + .ShouldHaveSingleItem(); + + stringDto.Value.ShouldBe(expectedDateStringFactory.Invoke(source.Value)); + }); + } + + protected Task DoShouldFormatDecimals(Func expectedDecimalStringFactory = null) + { + if (expectedDecimalStringFactory == null) + { + expectedDecimalStringFactory = d => d.ToString("C"); + } + + return RunTest(async (context, mapper) => + { + mapper.WhenMapping + .StringsFrom(c => c.FormatUsing("C")); + + var source = new PublicDecimal { Value = 674378.52m }; + var result = mapper.Map(source).ToANew(); + + result.Value.ShouldBe(source.Value.ToString("C")); + + await context.DecimalItems.Add(source); + await context.SaveChanges(); + + var stringDto = context + .DecimalItems + .ProjectUsing(mapper) + .To() + .ShouldHaveSingleItem(); + + stringDto.Value.ShouldBe(expectedDecimalStringFactory.Invoke(source.Value)); + }); + } + + protected Task DoShouldFormatDoubles(Func expectedDoubleStringFactory = null) + { + if (expectedDoubleStringFactory == null) + { + expectedDoubleStringFactory = d => d.ToString("0.000"); + } + + return RunTest(async (context, mapper) => + { + mapper.WhenMapping + .StringsFrom(c => c.FormatUsing("0.000")); + + var source = new PublicDouble { Value = 6778.52423 }; + var result = mapper.Map(source).ToANew(); + + result.Value.ShouldBe(source.Value.ToString("0.000")); + + await context.DoubleItems.Add(source); + await context.SaveChanges(); + + var stringDto = context + .DoubleItems + .ProjectUsing(mapper) + .To() + .ShouldHaveSingleItem(); + + stringDto.Value.ShouldBe(expectedDoubleStringFactory.Invoke(source.Value)); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms/Configuration/WhenIgnoringMembers.cs b/AgileMapper.UnitTests.Orms/Configuration/WhenIgnoringMembers.cs new file mode 100644 index 000000000..611c8114c --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Configuration/WhenIgnoringMembers.cs @@ -0,0 +1,146 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Configuration +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + + public abstract class WhenIgnoringMembers : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenIgnoringMembers(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldIgnoreAConfiguredMember() + { + return RunTest(async (context, mapper) => + { + var product = new Product { Name = "P1" }; + + await context.Products.Add(product); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .Ignore(p => p.Name); + + var productDto = context + .Products + .ProjectUsing(mapper).To() + .ShouldHaveSingleItem(); + + productDto.ProductId.ShouldBe(product.ProductId); + productDto.Name.ShouldBeNull(); + }); + } + + [Fact] + public Task ShouldIgnoreAConfiguredMemberConditionally() + { + return RunTest(async (context, mapper) => + { + var product1 = new Product { Name = "P.1" }; + var product2 = new Product { Name = "P.2" }; + + await context.Products.AddRange(product1, product2); + await context.SaveChanges(); + + mapper.WhenMapping + .From() + .ProjectedTo() + .If(p => p.Name.EndsWith(2 + "")) + .Ignore(p => p.Name); + + var productDtos = context + .Products + .ProjectUsing(mapper).To() + .OrderBy(p => p.ProductId) + .ToArray(); + + productDtos.First().ProductId.ShouldBe(product1.ProductId); + productDtos.First().Name.ShouldBe("P.1"); + productDtos.Second().ProductId.ShouldBe(product2.ProductId); + productDtos.Second().Name.ShouldBeNull(); + }); + } + + [Fact] + public Task ShouldIgnoreMembersByTypeAndTargetType() + { + return RunTest(async (context, mapper) => + { + var person = new Person + { + Name = "Mac", + Address = new Address { Line1 = "1", Line2 = "2", Postcode = "3" } + }; + + await context.Persons.Add(person); + await context.SaveChanges(); + + mapper.WhenMapping + .From
() + .ProjectedTo() + .IgnoreTargetMembersOfType(); + + var personDto = context + .Persons + .ProjectUsing(mapper).To() + .ShouldHaveSingleItem(); + + personDto.Id.ShouldBe(person.PersonId); + personDto.Name.ShouldBe("Mac"); + personDto.Address.ShouldNotBeNull(); + personDto.Address.Id.ShouldNotBe(person.Address.AddressId); + personDto.Address.Id.ShouldBeDefault(); + personDto.Address.Line1.ShouldBe("1"); + personDto.Address.Line2.ShouldBe("2"); + personDto.Address.Postcode.ShouldBe("3"); + }); + } + + [Fact] + public Task ShouldIgnorePropertiesByPropertyInfoMatcher() + { + return RunTest(async (context, mapper) => + { + var person = new Person + { + Name = "Frankie", + Address = new Address { Line1 = "1", Line2 = "2", Postcode = "3" } + }; + + await context.Persons.Add(person); + await context.SaveChanges(); + + mapper.WhenMapping + .To() + .IgnoreTargetMembersWhere(member => + member.IsPropertyMatching(p => p.GetGetMethod().Name.StartsWith("get_Line2"))); + + mapper.WhenMapping + .From
() + .ProjectedTo() + .IgnoreTargetMembersWhere(member => + member.IsPropertyMatching(p => p.GetSetMethod().Name.EndsWith("Line1"))); + + var personDto = context + .Persons + .ProjectUsing(mapper).To() + .ShouldHaveSingleItem(); + + personDto.Id.ShouldBe(person.PersonId); + personDto.Name.ShouldBe("Frankie"); + personDto.Address.ShouldNotBeNull(); + personDto.Address.Line1.ShouldBeNull(); + personDto.Address.Line2.ShouldBeNull(); + personDto.Address.Postcode.ShouldBe("3"); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms/Configuration/WhenMappingToNull.cs b/AgileMapper.UnitTests.Orms/Configuration/WhenMappingToNull.cs new file mode 100644 index 000000000..93766e5ae --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Configuration/WhenMappingToNull.cs @@ -0,0 +1,67 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Configuration +{ + using System.Linq; + using System.Threading.Tasks; + using AgileMapper.Extensions.Internal; + using Infrastructure; + using TestClasses; + + public abstract class WhenMappingToNull : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenMappingToNull(ITestContext context) + : base(context) + { + } + + #region Project -> Configured Null + + protected Task RunShouldApplyAUserConfiguration() + => RunTest(DoShouldApplyAUserConfiguration); + + protected Task RunShouldErrorApplyingAUserConfiguration() + => RunTestAndExpectThrow(DoShouldApplyAUserConfiguration); + + private static async Task DoShouldApplyAUserConfiguration(TOrmContext context, IMapper mapper) + { + var person1 = new Person + { + Name = "Frank", + Address = new Address { Line1 = "1" } + }; + + var person2 = new Person + { + Name = "Dee", + Address = new Address { Line1 = "Paddie's Pub" } + }; + + await context.Persons.AddRange(person1, person2); + await context.SaveChanges(); + + mapper.WhenMapping + .From
() + .ProjectedTo() + .If(a => a.Line1.Length == 1) + .MapToNull(); + + var personDtos = context + .Persons + .ProjectUsing(mapper) + .To() + .OrderBy(p => p.Id) + .ToArray(); + + personDtos.Length.ShouldBe(2); + + personDtos.First().Name.ShouldBe("Frank"); + personDtos.First().Address.ShouldBeNull(); + + personDtos.Second().Name.ShouldBe("Dee"); + personDtos.Second().Address.ShouldNotBeNull(); + personDtos.Second().Address.Line1.ShouldBe("Paddie's Pub"); + } + + #endregion + } +} diff --git a/AgileMapper.UnitTests.Orms/Infrastructure/DbSetWrapperBase.cs b/AgileMapper.UnitTests.Orms/Infrastructure/DbSetWrapperBase.cs new file mode 100644 index 000000000..1ce20572f --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Infrastructure/DbSetWrapperBase.cs @@ -0,0 +1,37 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Infrastructure +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Threading.Tasks; + + public abstract class DbSetWrapperBase : IDbSetWrapper + { + private readonly IQueryable _dbSet; + + protected DbSetWrapperBase(IQueryable dbSet) + { + _dbSet = dbSet; + } + + public IEnumerator GetEnumerator() => _dbSet.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public Expression Expression => _dbSet.Expression; + + public Type ElementType => _dbSet.ElementType; + + public IQueryProvider Provider => _dbSet.Provider; + + public abstract void Include(Expression> navigationPropertyPath); + + public abstract Task Add(TEntity itemToAdd); + + public abstract Task AddRange(TEntity[] itemsToAdd); + + public abstract void Clear(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/Infrastructure/IDbSetWrapper.cs b/AgileMapper.UnitTests.Orms/Infrastructure/IDbSetWrapper.cs new file mode 100644 index 000000000..23a278c5d --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Infrastructure/IDbSetWrapper.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Infrastructure +{ + using System; + using System.Linq; + using System.Linq.Expressions; + using System.Threading.Tasks; + + public interface IDbSetWrapper : IQueryable + { + void Include(Expression> navigationPropertyPath); + + Task Add(TEntity itemToAdd); + + Task AddRange(params TEntity[] itemsToAdd); + + void Clear(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/Infrastructure/ITestContext.cs b/AgileMapper.UnitTests.Orms/Infrastructure/ITestContext.cs new file mode 100644 index 000000000..81d20d626 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Infrastructure/ITestContext.cs @@ -0,0 +1,10 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Infrastructure +{ + using System; + + public interface ITestContext : IDisposable + where TOrmContext : ITestDbContext, new() + { + TOrmContext DbContext { get; } + } +} diff --git a/AgileMapper.UnitTests.Orms/Infrastructure/ITestDbContext.cs b/AgileMapper.UnitTests.Orms/Infrastructure/ITestDbContext.cs new file mode 100644 index 000000000..70e29f56f --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Infrastructure/ITestDbContext.cs @@ -0,0 +1,57 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Infrastructure +{ + using System; + using System.Threading.Tasks; + using TestClasses; + + public interface ITestDbContext : IDisposable + { + IDbSetWrapper Animals { get; } + + IDbSetWrapper Shapes { get; } + + IDbSetWrapper Companies { get; } + + IDbSetWrapper Employees { get; } + + IDbSetWrapper Categories { get; } + + IDbSetWrapper Products { get; } + + IDbSetWrapper Accounts { get; } + + IDbSetWrapper Persons { get; } + + IDbSetWrapper
Addresses { get; } + + IDbSetWrapper Rotas { get; } + + IDbSetWrapper RotaEntries { get; } + + IDbSetWrapper Orders { get; } + + IDbSetWrapper OrderItems { get; } + + IDbSetWrapper BoolItems { get; } + + IDbSetWrapper ByteItems { get; } + + IDbSetWrapper ShortItems { get; } + + IDbSetWrapper IntItems { get; } + + IDbSetWrapper LongItems { get; } + + IDbSetWrapper DecimalItems { get; } + + IDbSetWrapper DoubleItems { get; } + + IDbSetWrapper DateTimeItems { get; } + + IDbSetWrapper StringItems { get; } + + IDbSetWrapper TitleItems { get; } + + Task SaveChanges(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/Infrastructure/ITestLocalDbContext.cs b/AgileMapper.UnitTests.Orms/Infrastructure/ITestLocalDbContext.cs new file mode 100644 index 000000000..129f8b294 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Infrastructure/ITestLocalDbContext.cs @@ -0,0 +1,9 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Infrastructure +{ + public interface ITestLocalDbContext : ITestDbContext + { + void CreateDatabase(); + + void DeleteDatabase(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/Infrastructure/InMemoryOrmTestContext.cs b/AgileMapper.UnitTests.Orms/Infrastructure/InMemoryOrmTestContext.cs new file mode 100644 index 000000000..f17e70563 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Infrastructure/InMemoryOrmTestContext.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Infrastructure +{ + public class InMemoryOrmTestContext : ITestContext + where TOrmContext : ITestDbContext, new() + { + public InMemoryOrmTestContext() + { + DbContext = new TOrmContext(); + } + + public TOrmContext DbContext { get; } + + public void Dispose() + { + DbContext?.Dispose(); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/Infrastructure/LocalDbTestContext.cs b/AgileMapper.UnitTests.Orms/Infrastructure/LocalDbTestContext.cs new file mode 100644 index 000000000..026edba0c --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Infrastructure/LocalDbTestContext.cs @@ -0,0 +1,20 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Infrastructure +{ + public class LocalDbTestContext : ITestContext + where TOrmContext : ITestLocalDbContext, new() + { + public LocalDbTestContext() + { + DbContext = new TOrmContext(); + DbContext.CreateDatabase(); + } + + public TOrmContext DbContext { get; } + + public void Dispose() + { + DbContext.DeleteDatabase(); + DbContext.Dispose(); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/Infrastructure/OrmTestClassBase.cs b/AgileMapper.UnitTests.Orms/Infrastructure/OrmTestClassBase.cs new file mode 100644 index 000000000..62cc692ed --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Infrastructure/OrmTestClassBase.cs @@ -0,0 +1,83 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Infrastructure +{ + using System; + using System.Threading.Tasks; + using Xunit; + + [Collection(TestConstants.OrmCollectionName)] + public abstract class OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected OrmTestClassBase(ITestContext context) + { + Context = context.DbContext; + } + + protected TOrmContext Context { get; } + + protected Task RunTestAndExpectThrow(Func test) + => Should.ThrowAsync(() => RunTest(test)); + + protected Task RunTestAndExpectThrow(Func test) + => Should.ThrowAsync(() => RunTest(test)); + + protected Task RunTest(Func test) + => RunTest(mapper => test.Invoke(Context, mapper)); + + protected async Task RunTest(Func test) + { + try + { + await test.Invoke(Context); + } + finally + { + await EmptyDbContext(); + } + } + + protected async Task RunTest(Func test) + { + try + { + using (var mapper = Mapper.CreateNew()) + { + await test.Invoke(mapper); + } + } + finally + { + await EmptyDbContext(); + } + } + + private async Task EmptyDbContext() + { + Context.Animals.Clear(); + Context.Shapes.Clear(); + Context.Companies.Clear(); + Context.Employees.Clear(); + Context.Categories.Clear(); + Context.Products.Clear(); + Context.Addresses.Clear(); + Context.Accounts.Clear(); + Context.Persons.Clear(); + Context.OrderItems.Clear(); + Context.Orders.Clear(); + Context.Rotas.Clear(); + Context.RotaEntries.Clear(); + Context.BoolItems.Clear(); + Context.ByteItems.Clear(); + Context.ShortItems.Clear(); + Context.IntItems.Clear(); + Context.LongItems.Clear(); + Context.DecimalItems.Clear(); + Context.DoubleItems.Clear(); + Context.DateTimeItems.Clear(); + Context.StringItems.Clear(); + Context.TitleItems.Clear(); + + await Context.SaveChanges(); + } + } +} diff --git a/AgileMapper.UnitTests.Orms/Properties/AssemblyInfo.cs b/AgileMapper.UnitTests.Orms/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..5c07fa563 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Properties/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using Xunit; + +[assembly: AssemblyTitle("AgileObjects.AgileMapper.UnitTests.Orms")] +[assembly: AssemblyDescription("AgileObjects.AgileMapper.UnitTests.Orms")] + +[assembly: ComVisible(false)] + +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/Recursion/IOneToManyRecursionProjectionFailureTest.cs b/AgileMapper.UnitTests.Orms/Recursion/IOneToManyRecursionProjectionFailureTest.cs new file mode 100644 index 000000000..95a152e9c --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Recursion/IOneToManyRecursionProjectionFailureTest.cs @@ -0,0 +1,9 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Recursion +{ + using System.Threading.Tasks; + + public interface IOneToManyRecursionProjectionFailureTest + { + Task ShouldErrorProjectingAOneToManyRelationshipToZeroethRecursionDepth(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/Recursion/IOneToManyRecursionProjectorTest.cs b/AgileMapper.UnitTests.Orms/Recursion/IOneToManyRecursionProjectorTest.cs new file mode 100644 index 000000000..0cfdea28c --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Recursion/IOneToManyRecursionProjectorTest.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Recursion +{ + using System.Threading.Tasks; + + public interface IOneToManyRecursionProjectorTest + { + Task ShouldProjectAOneToManyRelationshipToDefaultRecursionDepth(); + + Task ShouldProjectAOneToManyRelationshipToFirstRecursionDepth(); + + Task ShouldProjectAOneToManyRelationshipToSecondRecursionDepth(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/Recursion/WhenProjectingCircularReferences.cs b/AgileMapper.UnitTests.Orms/Recursion/WhenProjectingCircularReferences.cs new file mode 100644 index 000000000..6ebf1ce2e --- /dev/null +++ b/AgileMapper.UnitTests.Orms/Recursion/WhenProjectingCircularReferences.cs @@ -0,0 +1,209 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.Recursion +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + + public abstract class WhenProjectingCircularReferences : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenProjectingCircularReferences(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAOneToOneRelationship() + { + return RunTest(async context => + { + var company = new Company + { + Name = "Acme", + HeadOffice = new Address { Line1 = "Acme Park", Postcode = "AC3 3ME" } + }; + + await context.Companies.Add(company); + await context.SaveChanges(); + + var ceo = new Employee + { + Name = "Mr Ceo", + DateOfBirth = DateTime.Today.AddYears(-21), + Address = new Address { Line1 = "Ceo Towers", Postcode = "CE0 0EC" }, + CompanyId = company.Id, + Company = company + }; + + await context.Employees.Add(ceo); + await context.SaveChanges(); + + company.CeoId = ceo.Id; + company.Ceo = ceo; + company.HeadOfficeId = company.HeadOffice.AddressId; + + await context.SaveChanges(); + + var companyDto = context.Companies.Project().To().ShouldHaveSingleItem(); + + companyDto.Id.ShouldBe(company.Id); + companyDto.Name.ShouldBe(company.Name); + + companyDto.HeadOffice.ShouldNotBeNull(); + companyDto.HeadOffice.Line1.ShouldBe("Acme Park"); + companyDto.HeadOffice.Postcode.ShouldBe("AC3 3ME"); + + companyDto.Ceo.ShouldNotBeNull(); + companyDto.Ceo.Name.ShouldBe("Mr Ceo"); + companyDto.Ceo.DateOfBirth.ShouldBe(ceo.DateOfBirth); + companyDto.Ceo.Company.ShouldBeNull(); + + companyDto.Ceo.Address.ShouldNotBeNull(); + companyDto.Ceo.Address.Line1.ShouldBe("Ceo Towers"); + companyDto.Ceo.Address.Postcode.ShouldBe("CE0 0EC"); + }); + } + + #region Project One-to-Many + + protected Task DoShouldErrorProjectingAOneToManyRelationshipToZeroethRecursionDepth() + => RunTestAndExpectThrow(context => ProjectAOneToManyRelationshipToRecursionDepth(0, context)); + + protected Task DoShouldProjectAOneToManyRelationshipToFirstRecursionDepth() + => RunTest(context => ProjectAOneToManyRelationshipToRecursionDepth(1, context)); + + protected Task DoShouldProjectAOneToManyRelationshipToSecondRecursionDepth() + => RunTest(context => ProjectAOneToManyRelationshipToRecursionDepth(2, context)); + + protected Task DoShouldProjectAOneToManyRelationshipToDefaultRecursionDepth() + => RunTest(context => ProjectAOneToManyRelationshipToRecursionDepth(null, context)); + + protected async Task ProjectAOneToManyRelationshipToRecursionDepth( + int? depth, + TOrmContext context) + { + var topLevel = new Category { Name = "Top Level" }; + + topLevel.AddSubCategories( + new Category { Name = "Top > One" }, + new Category { Name = "Top > Two" }, + new Category { Name = "Top > Three" }); + + await context.Categories.Add(topLevel); + + if (depth > 0) + { + topLevel.SubCategories.First().AddSubCategories( + new Category { Name = "Top > One > One" }, + new Category { Name = "Top > One > Two" }); + + topLevel.SubCategories.Second().AddSubCategories( + new Category { Name = "Top > Two > One" }, + new Category { Name = "Top > Two > Two" }, + new Category { Name = "Top > Two > Three" }); + + topLevel.SubCategories.Third().AddSubCategories( + new Category { Name = "Top > Three > One" }); + + if (depth > 1) + { + topLevel.SubCategories.Second().SubCategories.Second().AddSubCategories( + new Category { Name = "Top > Two > Two > One" }, + new Category { Name = "Top > Two > Two > Two" }); + } + } + + await context.SaveChanges(); + + var query = depth.HasValue + ? context.Categories + .Project().To(cfg => cfg.RecurseToDepth(depth.Value)) + : context.Categories + .Project().To(); + + var topLevelDto = query + .OrderBy(c => c.Id) + .First(c => c.Name == "Top Level"); + + topLevelDto.Id.ShouldBe(topLevel.Id); + topLevelDto.ParentCategoryId.ShouldBeNull(); + topLevelDto.ParentCategory.ShouldBeNull(); + + var depth1Dtos = GetOrderedSubCategories(topLevelDto); + + depth1Dtos.Length.ShouldBe(3); + + var child1 = topLevel.SubCategories.First(); + var child2 = topLevel.SubCategories.Second(); + var child3 = topLevel.SubCategories.Third(); + + Verify(depth1Dtos.First(), child1); + Verify(depth1Dtos.Second(), child2); + Verify(depth1Dtos.Third(), child3); + + if (!(depth > 0)) + { + depth1Dtos.First().SubCategories.ShouldBeEmpty(); + depth1Dtos.Second().SubCategories.ShouldBeEmpty(); + depth1Dtos.Third().SubCategories.ShouldBeEmpty(); + return; + } + + var depth11Dtos = GetOrderedSubCategories(depth1Dtos.First()); + + depth11Dtos.Length.ShouldBe(2); + + Verify(depth11Dtos.First(), child1.SubCategories.First()); + Verify(depth11Dtos.Second(), child1.SubCategories.Second()); + + var depth12Dtos = GetOrderedSubCategories(depth1Dtos.Second()); + + depth12Dtos.Length.ShouldBe(3); + + Verify(depth12Dtos.First(), child2.SubCategories.First()); + Verify(depth12Dtos.Second(), child2.SubCategories.Second()); + + var depth13Dtos = GetOrderedSubCategories(depth1Dtos.Third()); + + depth13Dtos.ShouldHaveSingleItem(); + + Verify(depth13Dtos.First(), child3.SubCategories.First()); + + depth11Dtos.First().SubCategories.ShouldBeEmpty(); + depth11Dtos.Second().SubCategories.ShouldBeEmpty(); + + depth12Dtos.First().SubCategories.ShouldBeEmpty(); + depth12Dtos.Third().SubCategories.ShouldBeEmpty(); + + depth13Dtos.First().SubCategories.ShouldBeEmpty(); + + if (!(depth > 1)) + { + depth12Dtos.Second().SubCategories.ShouldBeEmpty(); + return; + } + + var depth122Dtos = GetOrderedSubCategories(depth12Dtos.Second()); + + depth122Dtos.Length.ShouldBe(2); + + Verify(depth122Dtos.First(), child2.SubCategories.Second().SubCategories.First()); + Verify(depth122Dtos.Second(), child2.SubCategories.Second().SubCategories.Second()); + } + + private static CategoryDto[] GetOrderedSubCategories(CategoryDto parentDto) + => parentDto.SubCategories.OrderBy(sc => sc.Id).ToArray(); + + private static void Verify(CategoryDto result, Category source) + { + result.Id.ShouldBe(source.Id); + result.Name.ShouldBe(source.Name); + result.ParentCategoryId.ShouldBe(source.ParentCategoryId); + } + + #endregion + } +} diff --git a/AgileMapper.UnitTests.Orms/SimpleTypeConversion/IStringConversionFailureTest.cs b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/IStringConversionFailureTest.cs new file mode 100644 index 000000000..4eaa046a4 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/IStringConversionFailureTest.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.SimpleTypeConversion +{ + using System.Threading.Tasks; + + public interface IStringConversionFailureTest + { + Task ShouldErrorProjectingAParseableString(); + + Task ShouldErrorProjectingANullString(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/SimpleTypeConversion/IStringConversionValidationFailureTest.cs b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/IStringConversionValidationFailureTest.cs new file mode 100644 index 000000000..8eea0c7cb --- /dev/null +++ b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/IStringConversionValidationFailureTest.cs @@ -0,0 +1,9 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.SimpleTypeConversion +{ + using System.Threading.Tasks; + + public interface IStringConversionValidationFailureTest + { + Task ShouldErrorProjectingAnUnparseableString(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/SimpleTypeConversion/IStringConversionValidatorTest.cs b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/IStringConversionValidatorTest.cs new file mode 100644 index 000000000..26a9c9ad5 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/IStringConversionValidatorTest.cs @@ -0,0 +1,9 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.SimpleTypeConversion +{ + using System.Threading.Tasks; + + public interface IStringConversionValidatorTest + { + Task ShouldProjectAnUnparseableString(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/SimpleTypeConversion/IStringConverterTest.cs b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/IStringConverterTest.cs new file mode 100644 index 000000000..e820de867 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/IStringConverterTest.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.SimpleTypeConversion +{ + using System.Threading.Tasks; + + public interface IStringConverterTest + { + Task ShouldProjectAParseableString(); + + Task ShouldProjectANullString(); + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToBools.cs b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToBools.cs new file mode 100644 index 000000000..cad2db1c5 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToBools.cs @@ -0,0 +1,143 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.SimpleTypeConversion +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + + public abstract class WhenConvertingToBools : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConvertingToBools(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAnIntOneToTrue() + { + return RunTest(async context => + { + await context.IntItems.Add(new PublicInt { Value = 1 }); + await context.SaveChanges(); + + var boolItem = context.IntItems.Project().To().First(); + + boolItem.Value.ShouldBeTrue(); + }); + } + + [Fact] + public Task ShouldProjectAnIntZeroToFalse() + { + return RunTest(async context => + { + await context.IntItems.Add(new PublicInt { Value = 0 }); + await context.SaveChanges(); + + var boolItem = context.IntItems.Project().To().First(); + + boolItem.Value.ShouldBeFalse(); + }); + } + + [Fact] + public Task ShouldProjectAStringTrueToTrue() + { + return RunTest(async context => + { + await context.StringItems.Add(new PublicString { Value = "true" }); + await context.SaveChanges(); + + var boolItem = context.StringItems.Project().To().First(); + + boolItem.Value.ShouldBeTrue(); + }); + } + + [Fact] + public Task ShouldProjectAStringTrueToTrueIgnoringCase() + { + return RunTest(async context => + { + await context.StringItems.Add(new PublicString { Value = "tRuE" }); + await context.SaveChanges(); + + var boolItem = context.StringItems.Project().To().First(); + + boolItem.Value.ShouldBeTrue(); + }); + } + + [Fact] + public Task ShouldProjectAStringOneToTrue() + { + return RunTest(async context => + { + await context.StringItems.Add(new PublicString { Value = "1" }); + await context.SaveChanges(); + + var boolItem = context.StringItems.Project().To().First(); + + boolItem.Value.ShouldBeTrue(); + }); + } + + [Fact] + public Task ShouldProjectAStringFalseToFalse() + { + return RunTest(async context => + { + await context.StringItems.Add(new PublicString { Value = "false" }); + await context.SaveChanges(); + + var boolItem = context.StringItems.Project().To().First(); + + boolItem.Value.ShouldBeFalse(); + }); + } + + [Fact] + public Task ShouldProjectAStringZeroToFalse() + { + return RunTest(async context => + { + await context.StringItems.Add(new PublicString { Value = "0" }); + await context.SaveChanges(); + + var boolItem = context.StringItems.Project().To().First(); + + boolItem.Value.ShouldBeFalse(); + }); + } + + [Fact] + public Task ShouldProjectAStringNonBooleanValueToFalse() + { + return RunTest(async context => + { + await context.StringItems.Add(new PublicString { Value = "uokyujhygt" }); + await context.SaveChanges(); + + var boolItem = context.StringItems.Project().To().First(); + + boolItem.Value.ShouldBeFalse(); + }); + } + + [Fact] + public Task ShouldProjectAStringNullToFalse() + { + return RunTest(async context => + { + await context.StringItems.Add(new PublicString { Value = null }); + await context.SaveChanges(); + + var boolItem = context.StringItems.Project().To().First(); + + boolItem.Value.ShouldBeFalse(); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToDateTimes.cs b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToDateTimes.cs new file mode 100644 index 000000000..ef6c76e0c --- /dev/null +++ b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToDateTimes.cs @@ -0,0 +1,79 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.SimpleTypeConversion +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + + public abstract class WhenConvertingToDateTimes : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConvertingToDateTimes(ITestContext context) + : base(context) + { + } + + #region Parseable String -> DateTime + + protected Task RunShouldProjectAParseableStringToADateTime() + => RunTest(ProjectParseableStringToADateTime); + + protected Task RunShouldErrorProjectingAParseableStringToADateTime() + => RunTestAndExpectThrow(ProjectParseableStringToADateTime); + + private static async Task ProjectParseableStringToADateTime(TOrmContext context) + { + var now = DateTime.Now; + + await context.StringItems.Add(new PublicString { Value = now.ToString("s") }); + await context.SaveChanges(); + + var dateTimeItem = context.StringItems.Project().To().First(); + + dateTimeItem.Value.ShouldBe(now, TimeSpan.FromSeconds(1)); + } + + #endregion + + #region Null String -> DateTime + + protected Task RunShouldProjectANullStringToADateTime() + => RunTest(ProjectANullStringToADateTime); + + protected Task RunShouldErrorProjectingANullStringToADateTime() + => RunTestAndExpectThrow(ProjectANullStringToADateTime); + + private static async Task ProjectANullStringToADateTime(TOrmContext context) + { + await context.StringItems.Add(new PublicString { Value = default(string) }); + await context.SaveChanges(); + + var dateTimeItem = context.StringItems.Project().To().First(); + + dateTimeItem.Value.ShouldBe(default(DateTime)); + } + + #endregion + + #region Unparseable String -> DateTime + + protected Task RunShouldProjectAnUnparseableStringToADateTime() + => RunTest(ProjectAnUnparseableStringToADateTime); + + protected Task RunShouldErrorProjectingAnUnparseableStringToADateTime() + => RunTestAndExpectThrow(ProjectAnUnparseableStringToADateTime); + + private static async Task ProjectAnUnparseableStringToADateTime(TOrmContext context) + { + await context.StringItems.Add(new PublicString { Value = "htgijfoekld" }); + await context.SaveChanges(); + + var dateTimeItem = context.StringItems.Project().To().First(); + + dateTimeItem.Value.ShouldBe(default(DateTime)); + } + + #endregion + } +} diff --git a/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToDoubles.cs b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToDoubles.cs new file mode 100644 index 000000000..66d099507 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToDoubles.cs @@ -0,0 +1,104 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.SimpleTypeConversion +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + + public abstract class WhenConvertingToDoubles : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConvertingToDoubles(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAShortToADouble() + { + return RunTest(async context => + { + await context.ShortItems.Add(new PublicShort { Value = 123 }); + await context.SaveChanges(); + + var doubleItem = context.ShortItems.Project().To().First(); + + doubleItem.Value.ShouldBe(123d); + }); + } + + [Fact] + public Task ShouldProjectALongToADouble() + { + return RunTest(async context => + { + await context.LongItems.Add(new PublicLong { Value = 12345L }); + await context.SaveChanges(); + + var doubleItem = context.LongItems.Project().To().First(); + + doubleItem.Value.ShouldBe(12345d); + }); + } + + #region Parseable String -> Double + + protected Task RunShouldProjectAParseableStringToADouble() + => RunTest(ProjectParseableStringToDouble); + + protected Task RunShouldErrorProjectingAParseableStringToADouble() + => RunTestAndExpectThrow(ProjectParseableStringToDouble); + + private static async Task ProjectParseableStringToDouble(TOrmContext context) + { + await context.StringItems.Add(new PublicString { Value = "738.01" }); + await context.SaveChanges(); + + var doubleItem = context.StringItems.Project().To().First(); + + doubleItem.Value.ShouldBe(738.01); + } + + #endregion + + #region Null String -> Double + + protected Task RunShouldProjectANullStringToADouble() + => RunTest(ProjectNullStringToDouble); + + protected Task RunShouldErrorProjectingANullStringToADouble() + => RunTestAndExpectThrow(ProjectNullStringToDouble); + + private static async Task ProjectNullStringToDouble(TOrmContext context) + { + await context.StringItems.Add(new PublicString { Value = default(string) }); + await context.SaveChanges(); + + var doubleItem = context.StringItems.Project().To().First(); + + doubleItem.Value.ShouldBe(default(double)); + } + + #endregion + + #region Unparseable String -> Double + + protected Task RunShouldProjectAnUnparseableStringToADouble() + => RunTest(ProjectUnparseableStringToDouble); + + protected Task RunShouldErrorProjectingAnUnparseableStringToADouble() + => RunTestAndExpectThrow(ProjectUnparseableStringToDouble); + + private static async Task ProjectUnparseableStringToDouble(TOrmContext context) + { + await context.StringItems.Add(new PublicString { Value = "poioiujygy" }); + await context.SaveChanges(); + + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed + context.StringItems.Project().To().First(); + } + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToEnums.cs b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToEnums.cs new file mode 100644 index 000000000..e31333e32 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToEnums.cs @@ -0,0 +1,116 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.SimpleTypeConversion +{ + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using UnitTests.TestClasses; + using Xunit; + using static UnitTests.TestClasses.Title; + + public abstract class WhenConvertingToEnums : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConvertingToEnums(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAByteToAnEnum() + { + return RunTest(async context => + { + await context.ByteItems.Add(new PublicByte { Value = (byte)Dr }); + await context.SaveChanges(); + + var enumItem = context.ByteItems.Project().To().ShouldHaveSingleItem(); + + enumItem.Value.ShouldBe(Dr); + }); + } + + [Fact] + public Task ShouldProjectAShortToAnEnum() + { + return RunTest(async context => + { + await context.ShortItems.Add(new PublicShort { Value = (short)Count }); + await context.SaveChanges(); + + var enumItem = context.ShortItems.Project().To().ShouldHaveSingleItem(); + + enumItem.Value.ShouldBe(Count); + }); + } + + [Fact] + public Task ShouldProjectAnIntToAnEnum() + { + return RunTest(async context => + { + await context.IntItems.Add(new PublicInt { Value = (int)Duke }); + await context.SaveChanges(); + + var enumItem = context.IntItems.Project().To().ShouldHaveSingleItem(); + + enumItem.Value.ShouldBe(Duke); + }); + } + + [Fact] + public Task ShouldProjectALongToAnEnum() + { + return RunTest(async context => + { + await context.LongItems.Add(new PublicLong { Value = (long)Ms }); + await context.SaveChanges(); + + var enumItem = context.LongItems.Project().To().ShouldHaveSingleItem(); + + enumItem.Value.ShouldBe(Ms); + }); + } + + [Fact] + public Task ShouldProjectAMatchingStringToAnEnum() + { + return RunTest(async context => + { + await context.StringItems.Add(new PublicString { Value = Mr.ToString() }); + await context.SaveChanges(); + + var enumItem = context.StringItems.Project().To().ShouldHaveSingleItem(); + + enumItem.Value.ShouldBe(Mr); + }); + } + + [Fact] + public Task ShouldProjectAMatchingNumericStringToAnEnum() + { + return RunTest(async context => + { + await context.StringItems.Add(new PublicString { Value = ((int)Mrs).ToString() }); + await context.SaveChanges(); + + var enumItem = context.StringItems.Project().To().ShouldHaveSingleItem(); + + enumItem.Value.ShouldBe(Mrs); + }); + } + + [Fact] + public Task ShouldProjectANonMatchingStringToAnEnum() + { + return RunTest(async context => + { + await context.StringItems.Add(new PublicString { Value = "Horse Pills" }); + await context.SaveChanges(); + + var enumItem = context.StringItems.Project().To().ShouldHaveSingleItem(); + + enumItem.Value.ShouldBe(default(Title)); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToGuids.cs b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToGuids.cs new file mode 100644 index 000000000..3ea93c54f --- /dev/null +++ b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToGuids.cs @@ -0,0 +1,59 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.SimpleTypeConversion +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + + public abstract class WhenConvertingToGuids : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConvertingToGuids(ITestContext context) + : base(context) + { + } + + #region Parseable String -> Guid + + protected Task RunShouldProjectAParseableStringToAGuid() + => RunTest(ProjectAParseableStringToAGuid); + + protected Task RunShouldErrorProjectingAParseableStringToAGuid() + => RunTestAndExpectThrow(ProjectAParseableStringToAGuid); + + private static async Task ProjectAParseableStringToAGuid(TOrmContext context) + { + var guid = Guid.NewGuid(); + + await context.StringItems.Add(new PublicString { Value = guid.ToString() }); + await context.SaveChanges(); + + var guidItem = context.StringItems.Project().To().First(); + + guidItem.Value.ShouldBe(guid); + } + + #endregion + + #region Null String -> Guid + + protected Task RunShouldProjectANullStringToAGuid() + => RunTest(ProjectANullStringToAGuid); + + protected Task RunShouldErrorProjectingANullStringToAGuid() + => RunTestAndExpectThrow(ProjectANullStringToAGuid); + + private static async Task ProjectANullStringToAGuid(TOrmContext context) + { + await context.StringItems.Add(new PublicString { Value = default(string) }); + await context.SaveChanges(); + + var guidItem = context.StringItems.Project().To().First(); + + guidItem.Value.ShouldBe(default(Guid)); + } + + #endregion + } +} diff --git a/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToInts.cs b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToInts.cs new file mode 100644 index 000000000..2588c98c9 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToInts.cs @@ -0,0 +1,244 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.SimpleTypeConversion +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + + public abstract class WhenConvertingToInts : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConvertingToInts(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAShortToAnInt() + { + return RunTest(async context => + { + await context.ShortItems.Add(new PublicShort { Value = 123 }); + await context.SaveChanges(); + + var intItem = context.ShortItems.Project().To().First(); + + intItem.Value.ShouldBe(123); + }); + } + + [Fact] + public Task ShouldProjectAnInRangeLongToAnInt() + { + return RunTest(async context => + { + await context.LongItems.Add(new PublicLong { Value = 12345L }); + await context.SaveChanges(); + + var intItem = context.LongItems.Project().To().First(); + + intItem.Value.ShouldBe(12345); + }); + } + + [Fact] + public Task ShouldProjectATooBigLongToAnInt() + { + return RunTest(async context => + { + await context.LongItems.Add(new PublicLong { Value = long.MaxValue }); + await context.SaveChanges(); + + var intItem = context.LongItems.Project().To().First(); + + intItem.Value.ShouldBeDefault(); + }); + } + + [Fact] + public Task ShouldProjectATooSmallLongToAnInt() + { + return RunTest(async context => + { + await context.LongItems.Add(new PublicLong { Value = int.MinValue - 1L }); + await context.SaveChanges(); + + var intItem = context.LongItems.Project().To().First(); + + intItem.Value.ShouldBeDefault(); + }); + } + + [Fact] + public Task ShouldProjectAnInRangeDecimalToAnInt() + { + return RunTest(async context => + { + await context.DecimalItems.Add(new PublicDecimal { Value = 73872 }); + await context.SaveChanges(); + + var intItem = context.DecimalItems.Project().To().First(); + + intItem.Value.ShouldBe(73872); + }); + } + + [Fact] + public Task ShouldProjectATooBigDecimalToAnInt() + { + return RunTest(async context => + { + await context.DecimalItems.Add(new PublicDecimal { Value = decimal.MaxValue }); + await context.SaveChanges(); + + var intItem = context.DecimalItems.Project().To().First(); + + intItem.Value.ShouldBeDefault(); + }); + } + + [Fact] + public Task ShouldProjectATooSmallDecimalToAnInt() + { + return RunTest(async context => + { + await context.DecimalItems.Add(new PublicDecimal { Value = int.MinValue - 1.0m }); + await context.SaveChanges(); + + var intItem = context.DecimalItems.Project().To().First(); + + intItem.Value.ShouldBeDefault(); + }); + } + + [Fact] + public Task ShouldProjectANonWholeNumberDecimalToAnInt() + { + return RunTest(async context => + { + await context.DecimalItems.Add(new PublicDecimal { Value = 829.26m }); + await context.SaveChanges(); + + var intItem = context.DecimalItems.Project().To().First(); + + intItem.Value.ShouldBeDefault(); + }); + } + + [Fact] + public Task ShouldProjectAnInRangeDoubleToAnInt() + { + return RunTest(async context => + { + await context.DoubleItems.Add(new PublicDouble { Value = 7382.00 }); + await context.SaveChanges(); + + var intItem = context.DoubleItems.Project().To().First(); + + intItem.Value.ShouldBe(7382); + }); + } + + [Fact] + public Task ShouldProjectATooBigDoubleToAnInt() + { + return RunTest(async context => + { + await context.DoubleItems.Add(new PublicDouble { Value = double.MaxValue }); + await context.SaveChanges(); + + var intItem = context.DoubleItems.Project().To().First(); + + intItem.Value.ShouldBeDefault(); + }); + } + + [Fact] + public Task ShouldProjectATooSmallDoubleToAnInt() + { + return RunTest(async context => + { + await context.DoubleItems.Add(new PublicDouble { Value = int.MinValue - 1.00 }); + await context.SaveChanges(); + + var intItem = context.DoubleItems.Project().To().First(); + + intItem.Value.ShouldBeDefault(); + }); + } + + [Fact] + public Task ShouldProjectANonWholeNumberDoubleToAnInt() + { + return RunTest(async context => + { + await context.DoubleItems.Add(new PublicDouble { Value = 82.271 }); + await context.SaveChanges(); + + var intItem = context.DoubleItems.Project().To().First(); + + intItem.Value.ShouldBeDefault(); + }); + } + + #region Parseable String -> Int + + protected Task RunShouldProjectAParseableStringToAnInt() + => RunTest(ProjectParseableStringToInt); + + protected Task RunShouldErrorProjectingAParseableStringToAnInt() + => RunTestAndExpectThrow(ProjectParseableStringToInt); + + private static async Task ProjectParseableStringToInt(TOrmContext context) + { + await context.StringItems.Add(new PublicString { Value = "738" }); + await context.SaveChanges(); + + var intItem = context.StringItems.Project().To().First(); + + intItem.Value.ShouldBe(738); + } + + #endregion + + #region Null String -> Int + + protected Task RunShouldProjectANullStringToAnInt() + => RunTest(ProjectNullStringToInt); + + protected Task RunShouldErrorProjectingANullStringToAnInt() + => RunTestAndExpectThrow(ProjectNullStringToInt); + + private static async Task ProjectNullStringToInt(TOrmContext context) + { + await context.StringItems.Add(new PublicString { Value = default(string) }); + await context.SaveChanges(); + + var intItem = context.StringItems.Project().To().First(); + + intItem.Value.ShouldBe(default(int)); + } + + #endregion + + #region Unparseable String -> Int + + protected Task RunShouldProjectAnUnparseableStringToAnInt() + => RunTest(ProjectUnparseableStringToInt); + + protected Task RunShouldErrorProjectingAnUnparseableStringToAnInt() + => RunTestAndExpectThrow(ProjectUnparseableStringToInt); + + private static async Task ProjectUnparseableStringToInt(TOrmContext context) + { + await context.StringItems.Add(new PublicString { Value = "hsejk" }); + await context.SaveChanges(); + + // ReSharper disable once ReturnValueOfPureMethodIsNotUsed + context.StringItems.Project().To().First(); + } + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToStrings.cs b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToStrings.cs new file mode 100644 index 000000000..55944cfa6 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/SimpleTypeConversion/WhenConvertingToStrings.cs @@ -0,0 +1,103 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.SimpleTypeConversion +{ + using System; + using System.Globalization; + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + + public abstract class WhenConvertingToStrings : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenConvertingToStrings(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectABoolToAString() + { + return RunTest(async context => + { + await context.BoolItems.Add(new PublicBool { Value = true }); + await context.SaveChanges(); + + var stringItem = context.BoolItems.Project().To().First(); + + stringItem.Value.ShouldBe("true"); + }); + } + + [Fact] + public Task ShouldProjectAnIntToAString() + { + return RunTest(async context => + { + await context.IntItems.Add(new PublicInt { Value = 763483 }); + await context.SaveChanges(); + + var stringItem = context.IntItems.Project().To().First(); + + stringItem.Value.ShouldBe("763483"); + }); + } + + protected Task DoShouldProjectADecimalToAString(Func expectedDecimalStringFactory = null) + { + if (expectedDecimalStringFactory == null) + { + expectedDecimalStringFactory = d => d + ""; + } + + return RunTest(async context => + { + await context.DecimalItems.Add(new PublicDecimal { Value = 728.261m }); + await context.SaveChanges(); + + var stringItem = context.DecimalItems.Project().To().First(); + + stringItem.Value.ShouldBe(expectedDecimalStringFactory.Invoke(728.261m)); + }); + } + + protected Task DoShouldProjectADoubleToAString(Func expectedDoubleStringFactory = null) + { + if (expectedDoubleStringFactory == null) + { + expectedDoubleStringFactory = d => d + ""; + } + + return RunTest(async context => + { + await context.DoubleItems.Add(new PublicDouble { Value = 7212.34 }); + await context.SaveChanges(); + + var stringItem = context.DoubleItems.Project().To().First(); + + stringItem.Value.ShouldBe(expectedDoubleStringFactory.Invoke(7212.34)); + }); + } + + protected Task DoShouldProjectADateTimeToAString(Func expectedDateStringFactory = null) + { + if (expectedDateStringFactory == null) + { + expectedDateStringFactory = d => d.ToString(CultureInfo.CurrentCulture.DateTimeFormat); + } + + return RunTest(async context => + { + var now = DateTime.Now; + + await context.DateTimeItems.Add(new PublicDateTime { Value = now }); + await context.SaveChanges(); + + var stringItem = context.DateTimeItems.Project().To().First(); + + stringItem.Value.ShouldBe(expectedDateStringFactory.Invoke(now)); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms/TestClasses/Account.cs b/AgileMapper.UnitTests.Orms/TestClasses/Account.cs new file mode 100644 index 000000000..7626ec3ae --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/Account.cs @@ -0,0 +1,33 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + + public class Account + { + public Account() + { + DeliveryAddresses = new List(); + } + + [Key] + public int Id { get; set; } + + public int UserId { get; set; } + + public Person User { get; set; } + + public Account AddDeliveryAddress(Address address) + { + DeliveryAddresses.Add(new AccountAddress + { + Account = this, + Address = address + }); + + return this; + } + + public ICollection DeliveryAddresses { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/AccountAddress.cs b/AgileMapper.UnitTests.Orms/TestClasses/AccountAddress.cs new file mode 100644 index 000000000..4d072cc40 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/AccountAddress.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class AccountAddress + { + public int AccountId { get; set; } + + public Account Account { get; set; } + + public int AddressId { get; set; } + + public Address Address { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/AccountDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/AccountDto.cs new file mode 100644 index 000000000..237f451ea --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/AccountDto.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.Collections.Generic; + + public class AccountDto + { + public int Id { get; set; } + + public PersonDto User { get; set; } + + public IEnumerable DeliveryAddresses { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/Address.cs b/AgileMapper.UnitTests.Orms/TestClasses/Address.cs new file mode 100644 index 000000000..b93b6a978 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/Address.cs @@ -0,0 +1,16 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class Address + { + [Key] + public int AddressId { get; set; } + + public string Line1 { get; set; } + + public string Line2 { get; set; } + + public string Postcode { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/AddressDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/AddressDto.cs new file mode 100644 index 000000000..32c8a7241 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/AddressDto.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class AddressDto + { + public int Id { get; set; } + + public string Line1 { get; set; } + + public string Line2 { get; set; } + + public string Postcode { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/Animal.cs b/AgileMapper.UnitTests.Orms/TestClasses/Animal.cs new file mode 100644 index 000000000..b4ef3d267 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/Animal.cs @@ -0,0 +1,23 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class Animal + { + [Key] + public int Id { get; set; } + + public AnimalType Type { get; set; } + + public string Name { get; set; } + + public string Sound { get; set; } + + public enum AnimalType + { + Dog = 1, + Elephant = 2, + Snake = 3 + } + } +} diff --git a/AgileMapper.UnitTests.Orms/TestClasses/AnimalDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/AnimalDto.cs new file mode 100644 index 000000000..e8fdbff61 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/AnimalDto.cs @@ -0,0 +1,7 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class AnimalDto : AnimalDtoBase + { + public override string Sound { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/AnimalDtoBase.cs b/AgileMapper.UnitTests.Orms/TestClasses/AnimalDtoBase.cs new file mode 100644 index 000000000..c6008ef98 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/AnimalDtoBase.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public abstract class AnimalDtoBase + { + public int Id { get; set; } + + public string Name { get; set; } + + public abstract string Sound { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/Category.cs b/AgileMapper.UnitTests.Orms/TestClasses/Category.cs new file mode 100644 index 000000000..d750e7661 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/Category.cs @@ -0,0 +1,47 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + + public class Category + { + private Category _parentCategory; + + public Category() + { + SubCategories = new List(); + } + + [Key] + public int Id { get; set; } + + public int? ParentCategoryId { get; set; } + + public Category ParentCategory + { + get => _parentCategory; + set + { + _parentCategory = value; + + if (value != null) + { + ParentCategoryId = _parentCategory.Id; + } + } + } + + public string Name { get; set; } + + public void AddSubCategories(params Category[] subCategories) + { + foreach (var subCategory in subCategories) + { + subCategory.ParentCategory = this; + SubCategories.Add(subCategory); + } + } + + public ICollection SubCategories { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/CategoryDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/CategoryDto.cs new file mode 100644 index 000000000..62dd1a50c --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/CategoryDto.cs @@ -0,0 +1,17 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.Collections.Generic; + + public class CategoryDto + { + public int Id { get; set; } + + public int? ParentCategoryId { get; set; } + + public CategoryDto ParentCategory { get; set; } + + public string Name { get; set; } + + public IEnumerable SubCategories { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/Circle.cs b/AgileMapper.UnitTests.Orms/TestClasses/Circle.cs new file mode 100644 index 000000000..48a218dbe --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/Circle.cs @@ -0,0 +1,6 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class Circle : Shape + { + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/CircleViewModel.cs b/AgileMapper.UnitTests.Orms/TestClasses/CircleViewModel.cs new file mode 100644 index 000000000..a148e29bf --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/CircleViewModel.cs @@ -0,0 +1,7 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class CircleViewModel : ShapeViewModel + { + public int Diameter { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/Company.cs b/AgileMapper.UnitTests.Orms/TestClasses/Company.cs new file mode 100644 index 000000000..09f40534b --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/Company.cs @@ -0,0 +1,20 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class Company + { + [Key] + public int Id { get; set; } + + public string Name { get; set; } + + public int HeadOfficeId { get; set; } + + public Address HeadOffice { get; set; } + + public int? CeoId { get; set; } + + public Employee Ceo { get; set; } + } +} diff --git a/AgileMapper.UnitTests.Orms/TestClasses/CompanyDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/CompanyDto.cs new file mode 100644 index 000000000..7a0980bf5 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/CompanyDto.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class CompanyDto + { + public int Id { get; set; } + + public string Name { get; set; } + + public AddressDto HeadOffice { get; set; } + + public EmployeeDto Ceo { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/DogDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/DogDto.cs new file mode 100644 index 000000000..1bb2c8722 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/DogDto.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class DogDto : AnimalDtoBase + { + public override string Sound + { + get => "Woof"; + set { } + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/ElephantDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/ElephantDto.cs new file mode 100644 index 000000000..aa6700c04 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/ElephantDto.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class ElephantDto : AnimalDtoBase + { + public override string Sound + { + get => "Trumpet"; + set { } + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/Employee.cs b/AgileMapper.UnitTests.Orms/TestClasses/Employee.cs new file mode 100644 index 000000000..0484a852d --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/Employee.cs @@ -0,0 +1,24 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System; + using System.ComponentModel.DataAnnotations; + + public class Employee + { + [Key] + public int Id { get; set; } + + public DateTime DateOfBirth { get; set; } + + public string Name { get; set; } + + public int CompanyId { get; set; } + + [Required] + public Company Company { get; set; } + + public int AddressId { get; set; } + + public Address Address { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/EmployeeDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/EmployeeDto.cs new file mode 100644 index 000000000..4f557258e --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/EmployeeDto.cs @@ -0,0 +1,17 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System; + + public class EmployeeDto + { + public int Id { get; set; } + + public DateTime DateOfBirth { get; set; } + + public string Name { get; set; } + + public CompanyDto Company { get; set; } + + public AddressDto Address { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/OrderDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/OrderDto.cs new file mode 100644 index 000000000..7e0a68faa --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/OrderDto.cs @@ -0,0 +1,14 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System; + using System.Collections.Generic; + + public class OrderDto + { + public int Id { get; set; } + + public DateTime DatePlaced { get; set; } + + public IEnumerable Items { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/OrderItem.cs b/AgileMapper.UnitTests.Orms/TestClasses/OrderItem.cs new file mode 100644 index 000000000..4f71a164d --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/OrderItem.cs @@ -0,0 +1,10 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class OrderItem + { + [Key] + public int Id { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/OrderItemDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/OrderItemDto.cs new file mode 100644 index 000000000..108f0f142 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/OrderItemDto.cs @@ -0,0 +1,7 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class OrderItemDto + { + public int Id { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/OrderUk.cs b/AgileMapper.UnitTests.Orms/TestClasses/OrderUk.cs new file mode 100644 index 000000000..d6bed8efd --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/OrderUk.cs @@ -0,0 +1,19 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using UnitTests.TestClasses; + + public class OrderUk + { + [Key] + public int Id { get; set; } + + public DateTime DatePlaced { get; set; } + + public PaymentTypeUk PaymentType { get; set; } + + public ICollection Items { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/OrderUsViewModel.cs b/AgileMapper.UnitTests.Orms/TestClasses/OrderUsViewModel.cs new file mode 100644 index 000000000..e3a845a71 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/OrderUsViewModel.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System; + using UnitTests.TestClasses; + + public class OrderUsViewModel + { + public DateTime DatePlaced { get; set; } + + public PaymentTypeUs PaymentType { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/Person.cs b/AgileMapper.UnitTests.Orms/TestClasses/Person.cs new file mode 100644 index 000000000..6cf030069 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/Person.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class Person + { + [Key] + public int PersonId { get; set; } + + public string GetTitle() => "Dr"; + + public string Name { get; set; } + + public int? AddressId { get; set; } + + public Address Address { get; set; } + } +} diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PersonDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/PersonDto.cs new file mode 100644 index 000000000..6a573624c --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PersonDto.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class PersonDto + { + public int Id { get; set; } + + public string Name { get; set; } + + public AddressDto Address { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PersonViewModel.cs b/AgileMapper.UnitTests.Orms/TestClasses/PersonViewModel.cs new file mode 100644 index 000000000..63078d785 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PersonViewModel.cs @@ -0,0 +1,25 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class PersonViewModel + { + public int Id { get; set; } + + public string Title { get; set; } + + public string Name { get; set; } + + public void SetName(string name) => Name = name; + + public int? AddressId { get; set; } + + public string AddressLine1 { get; set; } + + public string AddressLine2 { get; set; } + + public string AddressPostcode + { + get; + set; + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/Product.cs b/AgileMapper.UnitTests.Orms/TestClasses/Product.cs new file mode 100644 index 000000000..329c12de8 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/Product.cs @@ -0,0 +1,14 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class Product + { + [Key] + public int ProductId { get; set; } + + public string Name { get; set; } + + public double Price { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/ProductDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/ProductDto.cs new file mode 100644 index 000000000..613562db3 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/ProductDto.cs @@ -0,0 +1,9 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class ProductDto + { + public int ProductId { get; set; } + + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/ProductStruct.cs b/AgileMapper.UnitTests.Orms/TestClasses/ProductStruct.cs new file mode 100644 index 000000000..1f6b14e29 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/ProductStruct.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public struct ProductStruct + { + public ProductStruct(string name) + { + ProductId = default(int); + Name = name; + Price = default(double); + } + + public int ProductId { get; set; } + + public string Name { get; set; } + + public double Price { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicBool.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicBool.cs new file mode 100644 index 000000000..6b985a377 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicBool.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class PublicBool + { + [Key] + public int Id { get; set; } + + public bool Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicBoolDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicBoolDto.cs new file mode 100644 index 000000000..6b0d38076 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicBoolDto.cs @@ -0,0 +1,9 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class PublicBoolDto + { + public int Id { get; set; } + + public bool Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicByte.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicByte.cs new file mode 100644 index 000000000..c48ecdb7b --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicByte.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class PublicByte + { + [Key] + public int Id { get; set; } + + public byte Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicByteDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicByteDto.cs new file mode 100644 index 000000000..28a0af9c9 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicByteDto.cs @@ -0,0 +1,9 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class PublicByteDto + { + public int Id { get; set; } + + public byte Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicDateTime.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicDateTime.cs new file mode 100644 index 000000000..0ac60c7b2 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicDateTime.cs @@ -0,0 +1,14 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System; + using System.ComponentModel.DataAnnotations; + + public class PublicDateTime + { + [Key] + public int Id { get; set; } + + + public DateTime Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicDateTimeDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicDateTimeDto.cs new file mode 100644 index 000000000..9b020665b --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicDateTimeDto.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System; + + public class PublicDateTimeDto + { + public int Id { get; set; } + + + public DateTime Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicDecimal.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicDecimal.cs new file mode 100644 index 000000000..8fca6251d --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicDecimal.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class PublicDecimal + { + [Key] + public int Id { get; set; } + + + public decimal Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicDouble.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicDouble.cs new file mode 100644 index 000000000..3cf2cd01f --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicDouble.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class PublicDouble + { + [Key] + public int Id { get; set; } + + + public double Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicDoubleDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicDoubleDto.cs new file mode 100644 index 000000000..0880d3656 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicDoubleDto.cs @@ -0,0 +1,10 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class PublicDoubleDto + { + public int Id { get; set; } + + + public double Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicGuidDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicGuidDto.cs new file mode 100644 index 000000000..577436e0b --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicGuidDto.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System; + + public class PublicGuidDto + { + public int Id { get; set; } + + + public Guid Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicInt.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicInt.cs new file mode 100644 index 000000000..9c1777790 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicInt.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class PublicInt + { + [Key] + public int Id { get; set; } + + + public int Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicIntDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicIntDto.cs new file mode 100644 index 000000000..1155a98ff --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicIntDto.cs @@ -0,0 +1,10 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class PublicIntDto + { + public int Id { get; set; } + + + public int Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicLong.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicLong.cs new file mode 100644 index 000000000..ab36a4c9f --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicLong.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class PublicLong + { + [Key] + public int Id { get; set; } + + + public long Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicShort.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicShort.cs new file mode 100644 index 000000000..dba76641f --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicShort.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class PublicShort + { + [Key] + public int Id { get; set; } + + + public short Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicString.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicString.cs new file mode 100644 index 000000000..0da541930 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicString.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class PublicString + { + [Key] + public int Id { get; set; } + + + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicStringCtorDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicStringCtorDto.cs new file mode 100644 index 000000000..ecbae435b --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicStringCtorDto.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class PublicStringCtorDto + { + public PublicStringCtorDto(string value) + { + Value = value; + } + + [Key] + public int Id { get; set; } + + + public string Value { get; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicStringDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicStringDto.cs new file mode 100644 index 000000000..50a71a348 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicStringDto.cs @@ -0,0 +1,10 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class PublicStringDto + { + public int Id { get; set; } + + + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicStringNames.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicStringNames.cs new file mode 100644 index 000000000..355512c52 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicStringNames.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + // ReSharper disable InconsistentNaming + public class PublicStringNames + { + [Key] + public int Id { get; set; } + + public string _Value { get; set; } + + public string _Value_ { get; set; } + + public string Value_ { get; set; } + } + // ReSharper restore InconsistentNaming +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicTitle.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicTitle.cs new file mode 100644 index 000000000..b762e6a3d --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicTitle.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + using UnitTests.TestClasses; + + public class PublicTitle + { + [Key] + public int Id { get; set; } + + public Title Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/PublicTitleDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/PublicTitleDto.cs new file mode 100644 index 000000000..80f1aab3e --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/PublicTitleDto.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using UnitTests.TestClasses; + + public class PublicTitleDto + { + public int Id { get; set; } + + public Title Value { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/Rota.cs b/AgileMapper.UnitTests.Orms/TestClasses/Rota.cs new file mode 100644 index 000000000..c84c70cd9 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/Rota.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + + public class Rota + { + [Key] + public int Id { get; set; } + + public DateTime StartDate { get; set; } + + public DateTime EndDate { get; set; } + + public ICollection Entries { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/RotaDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/RotaDto.cs new file mode 100644 index 000000000..0597b1005 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/RotaDto.cs @@ -0,0 +1,16 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System; + using System.Collections.Generic; + + public class RotaDto + { + public int Id { get; set; } + + public DateTime StartDate { get; set; } + + public DateTime EndDate { get; set; } + + public ICollection Entries { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/RotaEntry.cs b/AgileMapper.UnitTests.Orms/TestClasses/RotaEntry.cs new file mode 100644 index 000000000..086daf1da --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/RotaEntry.cs @@ -0,0 +1,23 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System; + using System.ComponentModel.DataAnnotations; + + public class RotaEntry + { + [Key] + public int Id { get; set; } + + public int PersonId { get; set; } + + public DayOfWeek DayOfWeek { get; set; } + + public byte StartHour { get; set; } + + public byte StartMinute { get; set; } + + public byte EndHour { get; set; } + + public byte EndMinute { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/RotaEntryDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/RotaEntryDto.cs new file mode 100644 index 000000000..95fd32e27 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/RotaEntryDto.cs @@ -0,0 +1,25 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System; + + public class RotaEntryDto + { + public int Id { get; set; } + + public int PersonId { get; set; } + + public DayOfWeek DayOfWeek { get; set; } + + public int StartHour { get; set; } + + public int StartMinute { get; set; } + + public DateTime StartTime { get; set; } + + public int EndHour { get; set; } + + public int EndMinute { get; set; } + + public DateTime EndTime { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/Shape.cs b/AgileMapper.UnitTests.Orms/TestClasses/Shape.cs new file mode 100644 index 000000000..55d31a53e --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/Shape.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + using System.ComponentModel.DataAnnotations; + + public class Shape + { + [Key] + public int Id { get; set; } + + public string Name { get; set; } + + public int? NumberOfSides { get; set; } + + public int? SideLength { get; set; } + + public int? Diameter { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/ShapeViewModel.cs b/AgileMapper.UnitTests.Orms/TestClasses/ShapeViewModel.cs new file mode 100644 index 000000000..c14bf0a9b --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/ShapeViewModel.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class ShapeViewModel + { + public int Id { get; set; } + + public string Name { get; set; } + + public int? NumberOfSides { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/SnakeDto.cs b/AgileMapper.UnitTests.Orms/TestClasses/SnakeDto.cs new file mode 100644 index 000000000..db8bb6067 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/SnakeDto.cs @@ -0,0 +1,11 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class SnakeDto : AnimalDtoBase + { + public override string Sound + { + get => "Hiss"; + set { } + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/Square.cs b/AgileMapper.UnitTests.Orms/TestClasses/Square.cs new file mode 100644 index 000000000..9efdb97b4 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/Square.cs @@ -0,0 +1,6 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class Square : Shape + { + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestClasses/SquareViewModel.cs b/AgileMapper.UnitTests.Orms/TestClasses/SquareViewModel.cs new file mode 100644 index 000000000..6b50c5397 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestClasses/SquareViewModel.cs @@ -0,0 +1,7 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms.TestClasses +{ + public class SquareViewModel : ShapeViewModel + { + public int SideLength { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestConstants.cs b/AgileMapper.UnitTests.Orms/TestConstants.cs new file mode 100644 index 000000000..b36b0000c --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestConstants.cs @@ -0,0 +1,24 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms +{ + using System; + + public static class TestConstants + { + public const string OrmCollectionName = "ORM Collection"; + + public static string GetLocalDbConnectionString() + { + var dbName = typeof(TDbContext).Name; + + if (dbName.EndsWith("Context", StringComparison.Ordinal)) + { + dbName = dbName.Substring(0, dbName.Length - "Context".Length); + } + + return "Data Source=(local);" + + "Initial Catalog=" + dbName + ";" + + "Integrated Security=True;" + + "MultipleActiveResultSets=True"; + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/TestExtensions.cs b/AgileMapper.UnitTests.Orms/TestExtensions.cs new file mode 100644 index 000000000..ee7b280bf --- /dev/null +++ b/AgileMapper.UnitTests.Orms/TestExtensions.cs @@ -0,0 +1,20 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms +{ + using System.Collections.Generic; + using System.Linq; + using System.Text.RegularExpressions; + + public static class TestExtensions + { + private static readonly Regex _tableNameMatcher = new Regex(@"FROM\s+(?.+)\s+AS"); + + public static T Second(this IEnumerable items) => items.ElementAt(1); + + public static T Third(this IEnumerable items) => items.ElementAt(2); + + public static T Fourth(this IEnumerable items) => items.ElementAt(3); + + public static string GetTableName(this string traceString) + => _tableNameMatcher.Match(traceString).Groups["table"].Value; + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/WhenProjectingFlatTypes.cs b/AgileMapper.UnitTests.Orms/WhenProjectingFlatTypes.cs new file mode 100644 index 000000000..941b1dbd1 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/WhenProjectingFlatTypes.cs @@ -0,0 +1,88 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + + public abstract class WhenProjectingFlatTypes : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenProjectingFlatTypes(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectToAFlatTypeArray() + { + return RunTest(async context => + { + var product1 = new Product { Name = "Product One" }; + var product2 = new Product { Name = "Product Two" }; + + await context.Products.AddRange(product1, product2); + await context.SaveChanges(); + + var productDtos = context + .Products + .Project().To() + .OrderBy(p => p.ProductId) + .ToArray(); + + productDtos.Length.ShouldBe(2); + + productDtos[0].ProductId.ShouldBe(product1.ProductId); + productDtos[0].Name.ShouldBe("Product One"); + + productDtos[1].ProductId.ShouldBe(product2.ProductId); + productDtos[1].Name.ShouldBe("Product Two"); + }); + } + + #region Project -> Struct Ctor Parameters + + protected Task RunShouldProjectStructCtorParameters() + => RunTest(DoShouldProjectStructCtorParameters); + + protected Task RunShouldErrorProjectingStructCtorParameters() + => RunTestAndExpectThrow(DoShouldProjectStructCtorParameters); + + private static async Task DoShouldProjectStructCtorParameters(TOrmContext context) + { + var product = new Product { Name = "Product One" }; + + await context.Products.Add(product); + await context.SaveChanges(); + + var productDto = context + .Products + .Project().To() + .ShouldHaveSingleItem(); + + productDto.ProductId.ShouldBe(product.ProductId); + productDto.Name.ShouldBe("Product One"); + } + + #endregion + + [Fact] + public Task ShouldProjectToANonMatchingTypeList() + { + return RunTest(async context => + { + var product = new Product { Name = "Uno" }; + + await context.Products.Add(product); + await context.SaveChanges(); + + var productDtos = context.Products.Project().To().ToList(); + + productDtos.ShouldHaveSingleItem(); + productDtos[0].Id.ShouldBe(product.ProductId); + productDtos[0].Value.ShouldBeNull(); + }); + } + } +} \ No newline at end of file diff --git a/AgileMapper.UnitTests.Orms/WhenProjectingToComplexTypeMembers.cs b/AgileMapper.UnitTests.Orms/WhenProjectingToComplexTypeMembers.cs new file mode 100644 index 000000000..140207b03 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/WhenProjectingToComplexTypeMembers.cs @@ -0,0 +1,77 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + + public abstract class WhenProjectingToComplexTypeMembers : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenProjectingToComplexTypeMembers(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectToAComplexTypeMember() + { + return RunTest(async context => + { + var person = new Person + { + Name = "Test Db", + Address = new Address + { + Line1 = "Test Db Line 1", + Line2 = "Test Db Line 2" + } + }; + + await context.Persons.Add(person); + await context.SaveChanges(); + + var personDto = context.Persons.Project().To().First(); + + personDto.Id.ShouldBe(person.PersonId); + personDto.Name.ShouldBe("Test Db"); + personDto.Address.ShouldNotBeNull(); + personDto.Address.Id.ShouldBe(person.Address.AddressId); + personDto.Address.Line1.ShouldBe("Test Db Line 1"); + personDto.Address.Line2.ShouldBe("Test Db Line 2"); + }); + } + + [Fact] + public Task ShouldHandleANullComplexTypeMember() + { + return RunTest(async context => + { + var person = new Person { Name = "No Address!" }; + + await context.Persons.Add(person); + await context.SaveChanges(); + + var personDto = context.Persons.Project().To().First(); + + personDto.Id.ShouldBe(person.PersonId); + personDto.Name.ShouldBe("No Address!"); + + if (QueryProviderNonEntityNullConstants) + { + personDto.Address.ShouldBeNull(); + return; + } + + personDto.Address.ShouldNotBeNull(); + personDto.Address.Id.ShouldBeDefault(); + personDto.Address.Line1.ShouldBeNull(); + personDto.Address.Line2.ShouldBeNull(); + personDto.Address.Postcode.ShouldBeNull(); + }); + } + + public virtual bool QueryProviderNonEntityNullConstants => true; + } +} diff --git a/AgileMapper.UnitTests.Orms/WhenProjectingToEnumerableMembers.cs b/AgileMapper.UnitTests.Orms/WhenProjectingToEnumerableMembers.cs new file mode 100644 index 000000000..2e3d731e2 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/WhenProjectingToEnumerableMembers.cs @@ -0,0 +1,189 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + + public abstract class WhenProjectingToEnumerableMembers : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenProjectingToEnumerableMembers(ITestContext context) + : base(context) + { + } + + #region Project -> Collection + + protected Task RunShouldProjectToAComplexTypeCollectionMember() + => RunTest(ProjectToComplexTypeCollectionMember); + + protected Task RunShouldErrorProjectingToAComplexTypeCollectionMember() + => RunTestAndExpectThrow(ProjectToComplexTypeCollectionMember); + + private static async Task ProjectToComplexTypeCollectionMember(TOrmContext context) + { + var rotaEntry1 = new RotaEntry + { + DayOfWeek = DayOfWeek.Monday, + PersonId = 10, + StartHour = 8, + StartMinute = 45, + EndHour = 5, + EndMinute = 15 + }; + + var rotaEntry2 = new RotaEntry + { + DayOfWeek = DayOfWeek.Tuesday, + PersonId = 8, + StartHour = 9, + StartMinute = 00, + EndHour = 4, + EndMinute = 30 + }; + + var rotaEntry3 = new RotaEntry + { + DayOfWeek = DayOfWeek.Friday, + PersonId = 51, + StartHour = 10, + StartMinute = 30, + EndHour = 10, + EndMinute = 31 + }; + + var rota = new Rota + { + StartDate = DateTime.Today, + EndDate = DateTime.Today.AddDays(7), + Entries = new List { rotaEntry1, rotaEntry2, rotaEntry3 } + }; + + await context.Rotas.Add(rota); + await context.SaveChanges(); + + var rotaDto = context.Rotas.Where(r => r.Id == 1).Project().To().First(); + + rotaDto.Id.ShouldBe(1); + rotaDto.StartDate.ShouldBe(rota.StartDate); + rotaDto.EndDate.ShouldBe(rota.EndDate); + rotaDto.Entries.Count.ShouldBe(rota.Entries.Count); + + var i = 0; + var rotaEntryDtos = rotaDto.Entries.OrderBy(re => re.Id).ToArray(); + + foreach (var rotaEntry in rota.Entries.OrderBy(re => re.Id)) + { + var rotaEntryDto = rotaEntryDtos.ElementAt(i); + + rotaEntryDto.Id.ShouldBe(rotaEntry.Id); + rotaEntryDto.DayOfWeek.ShouldBe(rotaEntry.DayOfWeek); + rotaEntryDto.PersonId.ShouldBe(rotaEntry.PersonId); + rotaEntryDto.StartHour.ShouldBe(rotaEntry.StartHour); + rotaEntryDto.StartMinute.ShouldBe(rotaEntry.StartMinute); + rotaEntryDto.EndHour.ShouldBe(rotaEntry.EndHour); + rotaEntryDto.EndMinute.ShouldBe(rotaEntry.EndMinute); + + ++i; + } + } + + #endregion + + [Fact] + public Task ShouldProjectToComplexTypeEnumerableMember() + { + return RunTest(async context => + { + var item1 = new OrderItem(); + var item2 = new OrderItem(); + + var order = new OrderUk + { + DatePlaced = DateTime.Now, + Items = new List { item1, item2 } + }; + + await context.Orders.Add(order); + await context.SaveChanges(); + + var orderDto = context + .Orders + .Project() + .To() + .ShouldHaveSingleItem(); + + orderDto.Id.ShouldBe(order.Id); + orderDto.DatePlaced.ShouldBe(order.DatePlaced); + orderDto.Items.Count().ShouldBe(order.Items.Count); + + var i = 0; + + foreach (var orderItem in order.Items) + { + var orderItemDto = order.Items.ElementAt(i); + + orderItemDto.Id.ShouldBe(orderItem.Id); + + ++i; + } + }); + } + + [Fact] + public Task ShouldProjectViaLinkingType() + { + return RunTest(async context => + { + var account = new Account + { + User = new Person + { + Name = "Mario", + Address = new Address { Line1 = "Here", Postcode = "HS93HS" } + } + }; + + account.AddDeliveryAddress(new Address { Line1 = "There", Postcode = "JS95TH" }); + account.AddDeliveryAddress(new Address { Line1 = "Somewhere", Postcode = "KA02ID" }); + + await context.Accounts.Add(account); + await context.SaveChanges(); + + var accountDto = context + .Accounts + .Project() + .To(cfg => cfg + .Map(a => a.DeliveryAddresses.Select(da => da.Address)) + .To(dto => dto.DeliveryAddresses)) + .ShouldHaveSingleItem(); + + accountDto.Id.ShouldBe(account.Id); + + accountDto.User.Id.ShouldBe(account.User.PersonId); + accountDto.User.Name.ShouldBe(account.User.Name); + accountDto.User.Address.Id.ShouldBe(account.User.Address.AddressId); + accountDto.User.Address.Line1.ShouldBe(account.User.Address.Line1); + accountDto.User.Address.Postcode.ShouldBe(account.User.Address.Postcode); + + accountDto.DeliveryAddresses.Count().ShouldBe(2); + + var addressDto1 = accountDto.DeliveryAddresses.First(); + var address1 = account.DeliveryAddresses.First().Address; + addressDto1.Id.ShouldBe(address1.AddressId); + addressDto1.Line1.ShouldBe(address1.Line1); + addressDto1.Postcode.ShouldBe(address1.Postcode); + + var addressDto2 = accountDto.DeliveryAddresses.Second(); + var address2 = account.DeliveryAddresses.Second().Address; + addressDto2.Id.ShouldBe(address2.AddressId); + addressDto2.Line1.ShouldBe(address2.Line1); + addressDto2.Postcode.ShouldBe(address2.Postcode); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms/WhenProjectingToFlatTypes.cs b/AgileMapper.UnitTests.Orms/WhenProjectingToFlatTypes.cs new file mode 100644 index 000000000..f67acf78e --- /dev/null +++ b/AgileMapper.UnitTests.Orms/WhenProjectingToFlatTypes.cs @@ -0,0 +1,62 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Xunit; + + public abstract class WhenProjectingToFlatTypes : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenProjectingToFlatTypes(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldProjectAComplexTypeMemberToAFlatTypeList() + { + return RunTest(async context => + { + var person1 = new Person + { + Name = "Person One", + Address = new Address + { + Line1 = "Person One Address Line 1", + Line2 = "Person One Address Line 2", + Postcode = "Person One Address Postcode" + } + }; + + var person2 = new Person { Name = "Person Two" }; + + await context.Persons.AddRange(person1, person2); + await context.SaveChanges(); + + var personViewModels = context + .Persons + .Project().To() + .OrderBy(pvm => pvm.Id) + .ToList(); + + personViewModels.Count.ShouldBe(2); + + personViewModels[0].Id.ShouldBe(person1.PersonId); + personViewModels[0].Name.ShouldBe("Person One"); + personViewModels[0].AddressId.ShouldBe(person1.Address.AddressId); + personViewModels[0].AddressLine1.ShouldBe("Person One Address Line 1"); + personViewModels[0].AddressLine2.ShouldBe("Person One Address Line 2"); + personViewModels[0].AddressPostcode.ShouldBe("Person One Address Postcode"); + + personViewModels[1].Id.ShouldBe(person2.PersonId); + personViewModels[1].Name.ShouldBe("Person Two"); + personViewModels[1].AddressId.ShouldBeNull(); + personViewModels[1].AddressLine1.ShouldBeNull(); + personViewModels[1].AddressLine2.ShouldBeNull(); + personViewModels[1].AddressPostcode.ShouldBeNull(); + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms/WhenValidatingProjections.cs b/AgileMapper.UnitTests.Orms/WhenValidatingProjections.cs new file mode 100644 index 000000000..91a6b1f50 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/WhenValidatingProjections.cs @@ -0,0 +1,69 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms +{ + using System.Threading.Tasks; + using Infrastructure; + using TestClasses; + using Validation; + using Xunit; + + public abstract class WhenValidatingProjections : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenValidatingProjections(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldSupportCachedProjectionValidation() + { + return RunTest((context, mapper) => + { + mapper.GetPlanForProjecting(context.Addresses).To(); + + Should.NotThrow(mapper.ThrowNowIfAnyMappingPlanIsIncomplete); + + return Task.CompletedTask; + }); + } + + [Fact] + public Task ShouldErrorIfCachedProjectionMembersHaveNoDataSources() + { + return RunTest((context, mapper) => + { + mapper.GetPlanForProjecting(context.RotaEntries).To(); + + var validationEx = Should.Throw(() => + mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); + + validationEx.Message.ShouldContain("IQueryable -> IQueryable"); + validationEx.Message.ShouldContain("Rule set: Project"); + validationEx.Message.ShouldContain("Unmapped target members"); + validationEx.Message.ShouldContain("IQueryable[i].StartTime"); + validationEx.Message.ShouldContain("IQueryable[i].EndTime"); + + return Task.CompletedTask; + }); + } + + [Fact] + public Task ShouldErrorIfCachedProjectionTargetTypeIsUnconstructable() + { + return RunTest((context, mapper) => + { + mapper.GetPlanForProjecting(context.Addresses).To(); + + var validationEx = Should.Throw(() => + mapper.ThrowNowIfAnyMappingPlanIsIncomplete()); + + validationEx.Message.ShouldContain("IQueryable
-> IQueryable"); + validationEx.Message.ShouldContain("Rule set: Project"); + validationEx.Message.ShouldContain("Unconstructable target Types"); + validationEx.Message.ShouldContain("Address -> ProductStruct"); + + return Task.CompletedTask; + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms/WhenViewingProjectionPlans.cs b/AgileMapper.UnitTests.Orms/WhenViewingProjectionPlans.cs new file mode 100644 index 000000000..14bf7d69a --- /dev/null +++ b/AgileMapper.UnitTests.Orms/WhenViewingProjectionPlans.cs @@ -0,0 +1,67 @@ +namespace AgileObjects.AgileMapper.UnitTests.Orms +{ + using System.Linq; + using System.Threading.Tasks; + using Infrastructure; + using MoreTestClasses; + using ObjectPopulation; + using TestClasses; + using Xunit; + + public abstract class WhenViewingProjectionPlans : OrmTestClassBase + where TOrmContext : ITestDbContext, new() + { + protected WhenViewingProjectionPlans(ITestContext context) + : base(context) + { + } + + [Fact] + public Task ShouldCreateAQueryProjectionPlanForASpecificQueryProvider() + { + return RunTest(mapper => + { + string plan = mapper + .GetPlanForProjecting(Context.Products) + .To(); + + plan.ShouldContain("Rule Set: Project"); + plan.ShouldContain("Source.Select("); + plan.ShouldContain("new ProductDto"); + + var cachedMapper = (IObjectMapper)mapper.RootMapperCountShouldBeOne(); + + cachedMapper.MapperData.SourceType.ShouldBe(typeof(IQueryable)); + cachedMapper.MapperData.TargetType.ShouldBe(typeof(IQueryable)); + + // Trigger a mapping: + Context.Products.Project().To().ShouldBeEmpty(); + + var usedMapper = (IObjectMapper)mapper.RootMapperCountShouldBeOne(); + + usedMapper.ShouldBe(cachedMapper); + + return Task.CompletedTask; + }); + } + + [Fact] + public Task ShouldReturnCachedQueryProjectionPlansInAllCachedPlans() + { + return RunTest(mapper => + { + mapper.GetPlanForProjecting(Context.Products).To(); + mapper.GetPlanForProjecting(Context.StringItems).To(); + mapper.GetPlanForProjecting(Context.Persons).To(); + + var allPlans = mapper.GetPlansInCache(); + + allPlans.ShouldContain("IQueryable -> IQueryable"); + allPlans.ShouldContain("IQueryable -> IQueryable"); + allPlans.ShouldContain("IQueryable -> IQueryable"); + + return Task.CompletedTask; + }); + } + } +} diff --git a/AgileMapper.UnitTests.Orms/packages.config b/AgileMapper.UnitTests.Orms/packages.config new file mode 100644 index 000000000..470f8bf96 --- /dev/null +++ b/AgileMapper.UnitTests.Orms/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj index 921271798..90e76d865 100644 --- a/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj +++ b/AgileMapper.UnitTests/AgileMapper.UnitTests.csproj @@ -1,5 +1,5 @@  - + @@ -14,6 +14,9 @@ AgileObjects.AgileMapper.UnitTests v4.6.1 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + False + UnitTest @@ -145,7 +148,6 @@ - @@ -307,9 +309,6 @@ AgileMapper - - - diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs index 126810621..4e43840d0 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringDataSourcesIncorrectly.cs @@ -93,7 +93,7 @@ public void ShouldErrorIfDuplicateDataSourceIsConfigured() .To(x => x.Value); mapper.WhenMapping - .From() + .From() .To>() .Map((p, x) => p.Id) .To(x => x.Value); diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringEnumMapping.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringEnumMapping.cs index 7be584c77..32180e5e1 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringEnumMapping.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringEnumMapping.cs @@ -124,7 +124,7 @@ public void ShouldErrorIfNoSourceEnumMembersSpecified() } }); - enumMappingEx.Message.ShouldContain("Source enum members"); + enumMappingEx.Message.ShouldContain("Pairing enum members"); } [Fact] @@ -140,7 +140,7 @@ public void ShouldErrorIfNoTargetEnumMembersSpecified() } }); - enumMappingEx.Message.ShouldContain("Target enum members"); + enumMappingEx.Message.ShouldContain("Paired enum members"); } [Fact] diff --git a/AgileMapper.UnitTests/Configuration/WhenConfiguringNameMatching.cs b/AgileMapper.UnitTests/Configuration/WhenConfiguringNameMatching.cs index 5ed794ebf..9670c77db 100644 --- a/AgileMapper.UnitTests/Configuration/WhenConfiguringNameMatching.cs +++ b/AgileMapper.UnitTests/Configuration/WhenConfiguringNameMatching.cs @@ -10,13 +10,11 @@ public class WhenConfiguringNameMatching { [Fact] - public void ShouldHandleACustomPrefix() + public void ShouldUseACustomPrefix() { using (var mapper = Mapper.CreateNew()) { - mapper - .WhenMapping - .UseNamePrefix("_p"); + mapper.WhenMapping.UseNamePrefix("_p"); var source = new { _pValue = "Help!" }; var result = mapper.Map(source).ToANew>(); @@ -26,13 +24,11 @@ public void ShouldHandleACustomPrefix() } [Fact] - public void ShouldHandleMultipleCustomPrefixes() + public void ShouldUseMultipleCustomPrefixes() { using (var mapper = Mapper.CreateNew()) { - mapper - .WhenMapping - .UseNamePrefixes("_p", "_f"); + mapper.WhenMapping.UseNamePrefixes("_p", "_f"); var source = new { _fValue = "Oops!" }; var result = mapper.Map(source).ToANew>(); @@ -42,13 +38,11 @@ public void ShouldHandleMultipleCustomPrefixes() } [Fact] - public void ShouldHandleACustomSuffix() + public void ShouldUseACustomSuffix() { using (var mapper = Mapper.CreateNew()) { - mapper - .WhenMapping - .UseNameSuffix("Str"); + mapper.WhenMapping.UseNameSuffix("Str"); var source = new { ValueStr = "La la la!" }; var result = mapper.Map(source).ToANew>(); @@ -58,13 +52,11 @@ public void ShouldHandleACustomSuffix() } [Fact] - public void ShouldHandleMultipleCustomSuffixes() + public void ShouldUseMultipleCustomSuffixes() { using (var mapper = Mapper.CreateNew()) { - mapper - .WhenMapping - .UseNameSuffixes("Str", "Int"); + mapper.WhenMapping.UseNameSuffixes("Str", "Int"); var source = new { ValueInt = 12345 }; var result = mapper.Map(source).ToANew>(); @@ -74,7 +66,7 @@ public void ShouldHandleMultipleCustomSuffixes() } [Fact] - public void ShouldHandleACustomNamingPattern() + public void ShouldUseACustomNamingPattern() { using (var mapper = Mapper.CreateNew()) { @@ -109,13 +101,11 @@ public void ShouldUseACustomNamingPatternInIdentifierMatching() } [Fact] - public void ShouldHandleACustomNamingPrefixPattern() + public void ShouldUseACustomNamingPrefixPattern() { using (var mapper = Mapper.CreateNew()) { - mapper - .WhenMapping - .UseNamePattern("^__(.+)$"); + mapper.WhenMapping.UseNamePattern("^__(.+)$"); var source = new { __Value = 911 }; var result = mapper.Map(source).ToANew>(); @@ -125,13 +115,11 @@ public void ShouldHandleACustomNamingPrefixPattern() } [Fact] - public void ShouldHandleACustomNamingSuffixPattern() + public void ShouldUseACustomNamingSuffixPattern() { using (var mapper = Mapper.CreateNew()) { - mapper - .WhenMapping - .UseNamePattern("^(.+)__$"); + mapper.WhenMapping.UseNamePattern("^(.+)__$"); var source = new { Value__ = 878 }; var result = mapper.Map(source).ToANew>(); @@ -141,13 +129,11 @@ public void ShouldHandleACustomNamingSuffixPattern() } [Fact] - public void ShouldHandleCustomNamingPatterns() + public void ShouldUseCustomNamingPatterns() { using (var mapper = Mapper.CreateNew()) { - mapper - .WhenMapping - .UseNamePatterns("^_abc(.+)xyz_$", "^__(.+)__$"); + mapper.WhenMapping.UseNamePatterns("^_abc(.+)xyz_$", "^__(.+)__$"); var source = new { __Value__ = 456 }; var result = mapper.Map(source).ToANew>(); @@ -163,9 +149,7 @@ public void ShouldErrorIfInvalidNamePatternFormatSpecified() { using (var mapper = Mapper.CreateNew()) { - mapper - .WhenMapping - .UseNamePatterns("^_[Name]_$"); + mapper.WhenMapping.UseNamePatterns("^_[Name]_$"); } }); } @@ -193,8 +177,7 @@ public void ShouldErrorIfNamePatternIsNull() { using (var mapper = Mapper.CreateNew()) { - mapper.WhenMapping - .UseNamePattern(null); + mapper.WhenMapping.UseNamePattern(null); } }); } @@ -206,9 +189,7 @@ public void ShouldErrorIfNoPatternsSupplied() { using (var mapper = Mapper.CreateNew()) { - mapper - .WhenMapping - .UseNamePatterns(); + mapper.WhenMapping.UseNamePatterns(); } }); } @@ -220,9 +201,7 @@ public void ShouldErrorIfPatternHasNoPrefixOrSuffix() { using (var mapper = Mapper.CreateNew()) { - mapper - .WhenMapping - .UseNamePattern("(.+)"); + mapper.WhenMapping.UseNamePattern("(.+)"); } }); } diff --git a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembers.cs b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembers.cs index a3bab7639..9fd349a91 100644 --- a/AgileMapper.UnitTests/Configuration/WhenIgnoringMembers.cs +++ b/AgileMapper.UnitTests/Configuration/WhenIgnoringMembers.cs @@ -18,7 +18,7 @@ public void ShouldIgnoreAConfiguredMember() mapper.WhenMapping .From() .ToANew() - .Ignore(x => x.Name); + .Ignore(pvm => pvm.Name); var source = new PersonViewModel { Name = "Jon" }; var result = mapper.Map(source).ToANew(); diff --git a/AgileMapper.UnitTests/Extensions/Internal/WhenEquatingExpressions.cs b/AgileMapper.UnitTests/Extensions/Internal/WhenEquatingExpressions.cs index e14726bf3..183bd65b8 100644 --- a/AgileMapper.UnitTests/Extensions/Internal/WhenEquatingExpressions.cs +++ b/AgileMapper.UnitTests/Extensions/Internal/WhenEquatingExpressions.cs @@ -15,7 +15,7 @@ public void ShouldEquateCheckedAdditions() Expression> bindingsOne = (x, y) => checked(x + y); Expression> bindingsTwo = (x, y) => checked(x + y); - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -24,7 +24,7 @@ public void ShouldEquateCheckedSubtractions() Expression> bindingsOne = (x, y) => checked(x - y); Expression> bindingsTwo = (x, y) => checked(x - y); - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -33,7 +33,7 @@ public void ShouldEquateCheckedMultiplications() Expression> bindingsOne = (x, y) => checked(x * y); Expression> bindingsTwo = (x, y) => checked(x * y); - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] public void ShouldEquateAModuloOperation() @@ -41,7 +41,7 @@ public void ShouldEquateAModuloOperation() Expression> bindingsOne = (x, y) => x % y == 0; Expression> bindingsTwo = (x, y) => x % y == 0; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -50,7 +50,7 @@ public void ShouldEquateNegatedDefaultComparisons() Expression> bindingsOne = x => !(x > default(int)); Expression> bindingsTwo = x => !(x > default(int)); - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -59,7 +59,7 @@ public void ShouldEquateTypeIsComparisons() Expression> bindingsOne = x => x is Person; Expression> bindingsTwo = x => x is Person; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -68,7 +68,7 @@ public void ShouldEquateTypeAsComparisons() Expression> bindingsOne = x => x as Person; Expression> bindingsTwo = x => x as Person; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -77,7 +77,7 @@ public void ShouldEquateAndComparisons() Expression> bindingsOne = x => x > 0 && x < 100; Expression> bindingsTwo = x => x > 0 && x < 100; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -86,7 +86,7 @@ public void ShouldEquateBitwiseAndComparisons() Expression> bindingsOne = x => x > 0 & x < 100; Expression> bindingsTwo = x => x > 0 & x < 100; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -95,7 +95,7 @@ public void ShouldEquateOrComparisons() Expression> bindingsOne = x => x > 0 || x < 100; Expression> bindingsTwo = x => x > 0 || x < 100; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -104,7 +104,7 @@ public void ShouldEquateBitwiseOrComparisons() Expression> bindingsOne = x => x > 0 | x < 100; Expression> bindingsTwo = x => x > 0 | x < 100; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -113,7 +113,25 @@ public void ShouldEquateExclusiveOrComparisons() Expression> bindingsOne = x => x > 0 ^ x < 100; Expression> bindingsTwo = x => x > 0 ^ x < 100; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); + } + + [Fact] + public void ShouldEquateMemberAccesses() + { + Expression> accessOne = m => m.Expression.NodeType; + Expression> accessTwo = m => m.Expression.NodeType; + + ExpressionEvaluation.AreEqual(accessOne, accessTwo).ShouldBeTrue(); + } + + [Fact] + public void ShouldEquivalateMemberAccesses() + { + Expression> accessOne = m => m.Expression.NodeType; + Expression> accessTwo = e => e.NodeType; + + ExpressionEvaluation.AreEquivalent(accessOne, accessTwo).ShouldBeTrue(); } [Fact] @@ -122,7 +140,7 @@ public void ShouldEquateListInitialisations() Expression>> bindingsOne = () => new List { 1, 2, 3 }; Expression>> bindingsTwo = () => new List { 1, 2, 3 }; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -131,7 +149,7 @@ public void ShouldEquateBitwiseLeftShiftOperations() Expression> bindingsOne = (x, y) => x << y; Expression> bindingsTwo = (x, y) => x << y; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -140,7 +158,7 @@ public void ShouldEquateBitwiseRightShiftOperations() Expression> bindingsOne = (x, y) => x >> y; Expression> bindingsTwo = (x, y) => x >> y; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -149,7 +167,7 @@ public void ShouldEquateCoalesceOperations() Expression> bindingsOne = (x, y) => x ?? y; Expression> bindingsTwo = (x, y) => x ?? y; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -158,7 +176,7 @@ public void ShouldEquateNegationOperations() Expression> bindingsOne = x => -x; Expression> bindingsTwo = x => -x; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -167,7 +185,7 @@ public void ShouldEquateCheckedNegationOperations() Expression> bindingsOne = x => checked(-x); Expression> bindingsTwo = x => checked(-x); - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -176,7 +194,7 @@ public void ShouldEquateANewBoundedArrayCreation() Expression> bindingsOne = x => new int[x]; Expression> bindingsTwo = x => new int[x]; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -185,7 +203,7 @@ public void ShouldEquateArrayIndexAccesses() Expression> bindingsOne = x => x[0]; Expression> bindingsTwo = x => x[0]; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -194,7 +212,7 @@ public void ShouldEquateArrayLengthAccesses() Expression> bindingsOne = x => x.Length == 1; Expression> bindingsTwo = x => x.Length == 1; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -203,7 +221,7 @@ public void ShouldEquateListBindings() Expression>>> bindingsOne = () => new PublicField> { Value = { 1 } }; Expression>>> bindingsTwo = () => new PublicField> { Value = { 1 } }; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeTrue(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeTrue(); } [Fact] @@ -212,7 +230,7 @@ public void ShouldDifferentiateListInitialisationsByInitialisationCount() Expression>> bindingsOne = () => new List { 1, 2, 3 }; Expression>> bindingsTwo = () => new List { 1, 2 }; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeFalse(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeFalse(); } [Fact] @@ -221,7 +239,7 @@ public void ShouldDifferentiateExpressionsByListBindingInitialiserCount() Expression>>> bindingsOne = () => new PublicField> { Value = { 1 } }; Expression>>> bindingsTwo = () => new PublicField> { Value = { 1, 2 } }; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeFalse(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeFalse(); } [Fact] @@ -230,7 +248,7 @@ public void ShouldDifferentiateExpressionsByBindingCount() Expression> oneBinding = () => new Address { Line1 = "One!" }; Expression> twoBindings = () => new Address { Line1 = "One!", Line2 = "Two!" }; - ExpressionEquator.Instance.Equals(oneBinding, twoBindings).ShouldBeFalse(); + ExpressionEvaluation.AreEqual(oneBinding, twoBindings).ShouldBeFalse(); } [Fact] @@ -248,7 +266,7 @@ public void ShouldDifferentiateExpressionsByBindingType() Value1 = "One!" }; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeFalse(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeFalse(); } [Fact] @@ -257,7 +275,7 @@ public void ShouldDifferentiateExpressionsBySubBindings() Expression> bindingsOne = () => new Person { Address = { Line1 = "One!" } }; Expression> bindingsTwo = () => new Person { Address = { Line1 = "One!", Line2 = "Two!" } }; - ExpressionEquator.Instance.Equals(bindingsOne, bindingsTwo).ShouldBeFalse(); + ExpressionEvaluation.AreEqual(bindingsOne, bindingsTwo).ShouldBeFalse(); } } } diff --git a/AgileMapper.UnitTests/MappingExtensions.cs b/AgileMapper.UnitTests/MappingExtensions.cs deleted file mode 100644 index 59006cfba..000000000 --- a/AgileMapper.UnitTests/MappingExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace AgileObjects.AgileMapper.UnitTests -{ - using System.Collections.Generic; - using System.Linq; - using ObjectPopulation; - - internal static class MappingExtensions - { - public static ICollection RootMappers(this IMapper mapper) - { - return ((Mapper)mapper).Context.ObjectMapperFactory.RootMappers.ToArray(); - } - } -} diff --git a/AgileMapper.UnitTests/Should.cs b/AgileMapper.UnitTests/Should.cs index 9b4300d45..26c900af5 100644 --- a/AgileMapper.UnitTests/Should.cs +++ b/AgileMapper.UnitTests/Should.cs @@ -1,15 +1,16 @@ namespace AgileObjects.AgileMapper.UnitTests { using System; + using System.Threading.Tasks; - internal static class Should + public static class Should { - public static TException Throw(Action testAction) + public static TException Throw(Action test) where TException : Exception { return Throw(() => { - testAction.Invoke(); + test.Invoke(); return new object(); }); @@ -30,6 +31,23 @@ public static TException Throw(Func testFunc) throw new Exception("Expected exception of type " + typeof(TException).Name); } + public static Task ThrowAsync(Func test) => ThrowAsync(test); + + public static async Task ThrowAsync(Func test) + where TException : Exception + { + try + { + await test.Invoke(); + } + catch (TException ex) + { + return ex; + } + + throw new Exception("Expected exception of type " + typeof(TException).Name); + } + public static void NotThrow(Action testAction) => NotThrow(testAction); public static void NotThrow(Action testAction) diff --git a/AgileMapper.UnitTests/ShouldExtensions.cs b/AgileMapper.UnitTests/ShouldExtensions.cs index fbb06f3b3..14ea9f666 100644 --- a/AgileMapper.UnitTests/ShouldExtensions.cs +++ b/AgileMapper.UnitTests/ShouldExtensions.cs @@ -6,12 +6,29 @@ using System.Reflection; using NetStandardPolyfills; - internal static class ShouldExtensions + public static class ShouldExtensions { public static void ShouldBeDefault(this T value) => value.ShouldBe(default(T)); public static void ShouldNotBeDefault(this T value) => value.ShouldNotBe(default(T)); + public static void ShouldBe(this DateTime value, DateTime expectedValue, TimeSpan tolerance) + { + var minimumExpectedValue = expectedValue.Subtract(tolerance); + + if (value < minimumExpectedValue) + { + Asplode($"a DateTime greater than {minimumExpectedValue}", $"{value}"); + } + + var maximumExpectedValue = expectedValue.Add(tolerance); + + if (value > maximumExpectedValue) + { + Asplode($"a DateTime less than {maximumExpectedValue}", $"{value}"); + } + } + public static void ShouldBe(this TActual? value, TExpected expectedValue) where TActual : struct { diff --git a/AgileMapper.UnitTests/WhenValidatingMappings.cs b/AgileMapper.UnitTests/WhenValidatingMappings.cs index 5b73316b9..fc4e9c48d 100644 --- a/AgileMapper.UnitTests/WhenValidatingMappings.cs +++ b/AgileMapper.UnitTests/WhenValidatingMappings.cs @@ -9,7 +9,7 @@ public class WhenValidatingMappings { [Fact] - public void ShouldSupportCachedMappingMemberValidation() + public void ShouldSupportCachedMapperValidation() { using (var mapper = Mapper.CreateNew()) { diff --git a/AgileMapper.UnitTests/WhenViewingMappingPlans.cs b/AgileMapper.UnitTests/WhenViewingMappingPlans.cs index bc201b44a..4900bca99 100644 --- a/AgileMapper.UnitTests/WhenViewingMappingPlans.cs +++ b/AgileMapper.UnitTests/WhenViewingMappingPlans.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; + using MoreTestClasses; using TestClasses; using Xunit; @@ -365,7 +366,7 @@ public void ShouldShowAllCachedMappingPlans() plan.ShouldContain("Rule Set: Merge"); plan.ShouldContain("Rule Set: Overwrite"); - mapper.RootMappers().Count.ShouldBe(5); + mapper.RootMapperCountShouldBe(5); } } } diff --git a/AgileMapper.UnitTests/packages.config b/AgileMapper.UnitTests/packages.config index 5e674ea6f..86d65fb03 100644 --- a/AgileMapper.UnitTests/packages.config +++ b/AgileMapper.UnitTests/packages.config @@ -7,7 +7,7 @@ - + diff --git a/AgileMapper.sln b/AgileMapper.sln index 157c0ab24..137b7e01f 100644 --- a/AgileMapper.sln +++ b/AgileMapper.sln @@ -28,6 +28,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AgileMapper.UnitTests.MoreT EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AgileMapper.UnitTests.NetCore2", "AgileMapper.UnitTests.NetCore2\AgileMapper.UnitTests.NetCore2.csproj", "{EBE1A3AE-1DE2-40A2-ACD3-2C15A5D10D66}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgileMapper.UnitTests.Orms.Ef6", "AgileMapper.UnitTests.Orms.Ef6\AgileMapper.UnitTests.Orms.Ef6.csproj", "{63B8975D-0CDE-48F5-8CA9-8AF8FE729610}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgileMapper.UnitTests.Orms", "AgileMapper.UnitTests.Orms\AgileMapper.UnitTests.Orms.csproj", "{66522D44-19F5-4AF5-9D43-483A3CD6F958}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgileMapper.UnitTests.Orms.Ef5", "AgileMapper.UnitTests.Orms.Ef5\AgileMapper.UnitTests.Orms.Ef5.csproj", "{D87103FD-3851-4724-BD8F-9CEF19C8F193}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgileMapper.UnitTests.Orms.EfCore2", "AgileMapper.UnitTests.Orms.EfCore2\AgileMapper.UnitTests.Orms.EfCore2.csproj", "{2E3DF5C2-8A38-4A03-86D7-8D463C917E47}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgileMapper.UnitTests.Orms.EfCore1", "AgileMapper.UnitTests.Orms.EfCore1\AgileMapper.UnitTests.Orms.EfCore1.csproj", "{FDEC5C57-9B2B-4599-80A6-DF0791BC7E1B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgileMapper.UnitTests.Orms.Ef5.LocalDb", "AgileMapper.UnitTests.Orms.Ef5.LocalDb\AgileMapper.UnitTests.Orms.Ef5.LocalDb.csproj", "{48B855A9-F42C-4D2A-9549-97ED27D59336}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgileMapper.UnitTests.Orms.Ef6.LocalDb", "AgileMapper.UnitTests.Orms.Ef6.LocalDb\AgileMapper.UnitTests.Orms.Ef6.LocalDb.csproj", "{B75D1A61-006A-4951-82FE-A2943296A872}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AgileMapper.UnitTests.Orms.EfCore2.NetCore2", "AgileMapper.UnitTests.Orms.EFCore2.NetCore2\AgileMapper.UnitTests.Orms.EfCore2.NetCore2.csproj", "{A6A9D59E-905E-4EC8-ABDD-62FA4E3B6B19}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -62,6 +78,38 @@ Global {EBE1A3AE-1DE2-40A2-ACD3-2C15A5D10D66}.Debug|Any CPU.Build.0 = Debug|Any CPU {EBE1A3AE-1DE2-40A2-ACD3-2C15A5D10D66}.Release|Any CPU.ActiveCfg = Release|Any CPU {EBE1A3AE-1DE2-40A2-ACD3-2C15A5D10D66}.Release|Any CPU.Build.0 = Release|Any CPU + {63B8975D-0CDE-48F5-8CA9-8AF8FE729610}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63B8975D-0CDE-48F5-8CA9-8AF8FE729610}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63B8975D-0CDE-48F5-8CA9-8AF8FE729610}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63B8975D-0CDE-48F5-8CA9-8AF8FE729610}.Release|Any CPU.Build.0 = Release|Any CPU + {66522D44-19F5-4AF5-9D43-483A3CD6F958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66522D44-19F5-4AF5-9D43-483A3CD6F958}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66522D44-19F5-4AF5-9D43-483A3CD6F958}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66522D44-19F5-4AF5-9D43-483A3CD6F958}.Release|Any CPU.Build.0 = Release|Any CPU + {D87103FD-3851-4724-BD8F-9CEF19C8F193}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D87103FD-3851-4724-BD8F-9CEF19C8F193}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D87103FD-3851-4724-BD8F-9CEF19C8F193}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D87103FD-3851-4724-BD8F-9CEF19C8F193}.Release|Any CPU.Build.0 = Release|Any CPU + {2E3DF5C2-8A38-4A03-86D7-8D463C917E47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E3DF5C2-8A38-4A03-86D7-8D463C917E47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E3DF5C2-8A38-4A03-86D7-8D463C917E47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E3DF5C2-8A38-4A03-86D7-8D463C917E47}.Release|Any CPU.Build.0 = Release|Any CPU + {FDEC5C57-9B2B-4599-80A6-DF0791BC7E1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FDEC5C57-9B2B-4599-80A6-DF0791BC7E1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FDEC5C57-9B2B-4599-80A6-DF0791BC7E1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FDEC5C57-9B2B-4599-80A6-DF0791BC7E1B}.Release|Any CPU.Build.0 = Release|Any CPU + {48B855A9-F42C-4D2A-9549-97ED27D59336}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48B855A9-F42C-4D2A-9549-97ED27D59336}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48B855A9-F42C-4D2A-9549-97ED27D59336}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48B855A9-F42C-4D2A-9549-97ED27D59336}.Release|Any CPU.Build.0 = Release|Any CPU + {B75D1A61-006A-4951-82FE-A2943296A872}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B75D1A61-006A-4951-82FE-A2943296A872}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B75D1A61-006A-4951-82FE-A2943296A872}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B75D1A61-006A-4951-82FE-A2943296A872}.Release|Any CPU.Build.0 = Release|Any CPU + {A6A9D59E-905E-4EC8-ABDD-62FA4E3B6B19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6A9D59E-905E-4EC8-ABDD-62FA4E3B6B19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6A9D59E-905E-4EC8-ABDD-62FA4E3B6B19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6A9D59E-905E-4EC8-ABDD-62FA4E3B6B19}.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 d39b14382..e0f876e82 100644 --- a/AgileMapper/AgileMapper.csproj +++ b/AgileMapper/AgileMapper.csproj @@ -29,6 +29,9 @@ + + 4.3.0 + @@ -37,6 +40,9 @@ + + 4.3.0 + diff --git a/AgileMapper/Api/Configuration/CallbackSpecifier.cs b/AgileMapper/Api/Configuration/CallbackSpecifier.cs index ad2fea6e9..fb5e23326 100644 --- a/AgileMapper/Api/Configuration/CallbackSpecifier.cs +++ b/AgileMapper/Api/Configuration/CallbackSpecifier.cs @@ -49,13 +49,13 @@ private CallbackSpecifier SetCondition(LambdaExpression condit return this; } - public MappingConfigContinuation Call(Action> callback) + public IMappingConfigContinuation Call(Action> callback) => CreateCallbackFactory(callback); - public MappingConfigContinuation Call(Action callback) + public IMappingConfigContinuation Call(Action callback) => CreateCallbackFactory(callback); - public MappingConfigContinuation Call(Action callback) + public IMappingConfigContinuation Call(Action callback) => CreateCallbackFactory(callback); private MappingConfigContinuation CreateCallbackFactory(TAction callback) diff --git a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs index 1017f20b6..20418ac6d 100644 --- a/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs +++ b/AgileMapper/Api/Configuration/CustomDataSourceTargetMemberSpecifier.cs @@ -12,20 +12,19 @@ using Members; using Members.Dictionaries; using NetStandardPolyfills; + using Projection; using ReadableExpressions.Extensions; - /// - /// Provides options for specifying a target member to which a configuration option should apply. - /// - /// The source type to which the configuration should apply. - /// The target type to which the configuration should apply. - public class CustomDataSourceTargetMemberSpecifier + + internal class CustomDataSourceTargetMemberSpecifier : + ICustomMappingDataSourceTargetMemberSpecifier, + ICustomProjectionDataSourceTargetMemberSpecifier { private readonly MappingConfigInfo _configInfo; private readonly LambdaExpression _customValueLambda; private readonly ConfiguredLambdaInfo _customValueLambdaInfo; - internal CustomDataSourceTargetMemberSpecifier( + public CustomDataSourceTargetMemberSpecifier( MappingConfigInfo configInfo, LambdaExpression customValueLambda) : this(configInfo, default(ConfiguredLambdaInfo)) @@ -33,7 +32,7 @@ internal CustomDataSourceTargetMemberSpecifier( _customValueLambda = customValueLambda; } - internal CustomDataSourceTargetMemberSpecifier( + public CustomDataSourceTargetMemberSpecifier( MappingConfigInfo configInfo, ConfiguredLambdaInfo customValueLambda) { @@ -41,29 +40,20 @@ internal CustomDataSourceTargetMemberSpecifier( _customValueLambdaInfo = customValueLambda; } - /// - /// Apply the configuration to the given . - /// - /// The target member's type. - /// The target member to which to apply the configuration. - /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and - /// target type being configured. - /// - public MappingConfigContinuation To( + + public IMappingConfigContinuation To( Expression> targetMember) - => RegisterDataSource(() => CreateFromLambda(targetMember)); - - /// - /// Apply the configuration to the given . - /// - /// The type of the target set method's argument. - /// The target set method to which to apply the configuration. - /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and - /// target type being configured. - /// - public MappingConfigContinuation To( + { + return RegisterDataSource(() => CreateFromLambda(targetMember)); + } + + IProjectionConfigContinuation ICustomProjectionDataSourceTargetMemberSpecifier.To( + Expression> resultMember) + { + return RegisterDataSource(() => CreateFromLambda(resultMember)); + } + + public IMappingConfigContinuation To( Expression>> targetSetMethod) => RegisterDataSource(() => CreateFromLambda(targetSetMethod)); @@ -149,28 +139,23 @@ private ConfiguredLambdaInfo GetValueLambda() return valueLambdaInfo; } - /// - /// Apply the configuration to the constructor parameter with the type specified by the type argument. - /// - /// The target constructor parameter's type. - /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and - /// target type being configured. - /// - public MappingConfigContinuation ToCtor() + public IMappingConfigContinuation ToCtor() => RegisterDataSource(CreateForCtorParam); - /// - /// Apply the configuration to the constructor parameter with the specified . - /// - /// The target constructor parameter's name. - /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and - /// target type being configured. - /// - public MappingConfigContinuation ToCtor(string parameterName) + IProjectionConfigContinuation ICustomProjectionDataSourceTargetMemberSpecifier.ToCtor() + => RegisterDataSource(CreateForCtorParam); + + public IMappingConfigContinuation ToCtor(string parameterName) => RegisterDataSource(() => CreateForCtorParam(parameterName)); + IProjectionConfigContinuation ICustomProjectionDataSourceTargetMemberSpecifier.ToCtor( + string parameterName) + { + return RegisterDataSource(() => CreateForCtorParam(parameterName)); + } + + #region Ctor Helpers + private ConfiguredDataSourceFactory CreateForCtorParam() => CreateForCtorParam(GetUniqueConstructorParameterOrThrow()); @@ -249,6 +234,8 @@ private ConfiguredDataSourceFactory CreateForCtorParam(ParameterInfo par return new ConfiguredDataSourceFactory(_configInfo, valueLambda, constructorParameter); } + #endregion + private MappingConfigContinuation RegisterDataSource( Func factoryFactory) { diff --git a/AgileMapper/Api/Configuration/DerivedPairTargetTypeSpecifier.cs b/AgileMapper/Api/Configuration/DerivedPairTargetTypeSpecifier.cs index 9c67ca4e2..b7ff16065 100644 --- a/AgileMapper/Api/Configuration/DerivedPairTargetTypeSpecifier.cs +++ b/AgileMapper/Api/Configuration/DerivedPairTargetTypeSpecifier.cs @@ -1,40 +1,29 @@ namespace AgileObjects.AgileMapper.Api.Configuration { using AgileMapper.Configuration; + using Projection; - /// - /// Enables the selection of a derived target type to which to match a configured, derived source type. - /// - /// - /// The type of source object for which the derived type pair is being configured. - /// - /// - /// The type of derived source object for which the specified derived target type is being configured. - /// - /// - /// The type of target object for which the derived type pair is being configured. - /// - public class DerivedPairTargetTypeSpecifier + internal class DerivedPairTargetTypeSpecifier : + IMappingDerivedPairTargetTypeSpecifier, + IProjectionDerivedPairTargetTypeSpecifier { private readonly MappingConfigInfo _configInfo; - internal DerivedPairTargetTypeSpecifier(MappingConfigInfo configInfo) + public DerivedPairTargetTypeSpecifier(MappingConfigInfo configInfo) { _configInfo = configInfo; } - /// - /// Map the derived source type being configured to the derived target type specified by the type argument. - /// - /// - /// The derived target type to create for the configured derived source type. - /// - /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and - /// target type being configured. - /// - public MappingConfigContinuation To() + public IMappingConfigContinuation To() where TDerivedTarget : TTarget + { + return SetDerivedTargetType(); + } + + IProjectionConfigContinuation IProjectionDerivedPairTargetTypeSpecifier.To() + => SetDerivedTargetType(); + + private MappingConfigContinuation SetDerivedTargetType() { var derivedTypePair = DerivedTypePair .For(_configInfo); diff --git a/AgileMapper/Api/Configuration/EnumPairSpecifier.cs b/AgileMapper/Api/Configuration/EnumPairSpecifier.cs index 8e5442bd7..5a2c6316b 100644 --- a/AgileMapper/Api/Configuration/EnumPairSpecifier.cs +++ b/AgileMapper/Api/Configuration/EnumPairSpecifier.cs @@ -6,15 +6,12 @@ using AgileMapper.Configuration; using Extensions.Internal; using NetStandardPolyfills; + using Projection; using ReadableExpressions.Extensions; - /// - /// Provides options for specifying the enum member to which the configured enum member should be paired. - /// - /// The source type being configured. - /// The target type being configured. - /// The type of the first enum being paired. - public class EnumPairSpecifier + internal class EnumPairSpecifier : + IMappingEnumPairSpecifier, + IProjectionEnumPairSpecifier { private readonly MappingConfigInfo _configInfo; private readonly TPairingEnum[] _pairingEnumMembers; @@ -29,14 +26,14 @@ private EnumPairSpecifier( #region Factory Method - internal static EnumPairSpecifier For( + public static EnumPairSpecifier For( MappingConfigInfo configInfo, - params TPairingEnum[] firstEnumMembers) + params TPairingEnum[] pairingEnumMembers) { ThrowIfNotEnumType(); - ThrowIfEmpty(firstEnumMembers); + ThrowIfEmpty(pairingEnumMembers); - return new EnumPairSpecifier(configInfo, firstEnumMembers); + return new EnumPairSpecifier(configInfo, pairingEnumMembers); } private static void ThrowIfNotEnumType() @@ -48,11 +45,11 @@ private static void ThrowIfNotEnumType() } } - private static void ThrowIfEmpty(ICollection firstEnumMembers) + private static void ThrowIfEmpty(ICollection pairingEnumMembers) { - if (firstEnumMembers.None()) + if (pairingEnumMembers.None()) { - throw new MappingConfigurationException("Source enum members must be provided."); + throw new MappingConfigurationException("Pairing enum members must be provided."); } } @@ -60,27 +57,14 @@ private static void ThrowIfEmpty(ICollection firstEnumMembers) private MapperContext MapperContext => _configInfo.MapperContext; - /// - /// Configure this mapper to map the specified first enum member to the given . - /// - /// The type of the second enum being paired. - /// The second enum member in the pair. - /// A MappingConfigContinuation with which to configure other aspects of mapping. - public MappingConfigContinuation With(TPairedEnum pairedEnumMember) + public IMappingConfigContinuation With(params TPairedEnum[] pairedEnumMembers) where TPairedEnum : struct { - return PairEnums(pairedEnumMember); + return PairEnums(pairedEnumMembers); } - /// - /// Configure this mapper to map the previously-specified set of enum members to the given - /// . - /// - /// The type of the second enum being paired. - /// The second set of enum members in the pairs. - /// A MappingConfigContinuation with which to configure other aspects of mapping. - public MappingConfigContinuation With(params TPairedEnum[] pairedEnumMembers) - where TPairedEnum : struct + IProjectionConfigContinuation IProjectionEnumPairSpecifier.With( + params TPairedEnum[] pairedEnumMembers) { return PairEnums(pairedEnumMembers); } @@ -134,7 +118,7 @@ private static void ThrowIfEmpty(ICollection pairedEnu { if (pairedEnumMembers.None()) { - throw new MappingConfigurationException("Target enum members must be provided."); + throw new MappingConfigurationException("Paired enum members must be provided."); } } diff --git a/AgileMapper/Api/Configuration/FactorySpecifier.cs b/AgileMapper/Api/Configuration/FactorySpecifier.cs index 9a40b4e67..5a2c3c2b2 100644 --- a/AgileMapper/Api/Configuration/FactorySpecifier.cs +++ b/AgileMapper/Api/Configuration/FactorySpecifier.cs @@ -6,9 +6,12 @@ namespace AgileObjects.AgileMapper.Api.Configuration using AgileMapper.Configuration; using Members; using ObjectPopulation; + using Projection; using ReadableExpressions.Extensions; - internal class FactorySpecifier : IFactorySpecifier + internal class FactorySpecifier : + IMappingFactorySpecifier, + IProjectionFactorySpecifier { private readonly MappingConfigInfo _configInfo; @@ -17,43 +20,56 @@ public FactorySpecifier(MappingConfigInfo configInfo) _configInfo = configInfo; } - public void Using(Expression, TObject>> factory) - { - var objectFactory = ConfiguredObjectFactory.For(_configInfo, typeof(TObject), factory); + public IMappingConfigContinuation Using( + Expression, TObject>> factory) + => RegisterObjectFactory(factory, ConfiguredObjectFactory.For); - _configInfo.MapperContext.UserConfigurations.Add(objectFactory); - } + public IProjectionConfigContinuation Using(Expression> factory) + => RegisterObjectFactory(factory, ConfiguredObjectFactory.For); - public void Using(TFactory factory) where TFactory : class + public IMappingConfigContinuation Using(LambdaExpression factory) + => RegisterObjectFactory(factory, ConfiguredObjectFactory.For); + + public IMappingConfigContinuation Using(TFactory factory) + where TFactory : class { var factoryInfo = ConfiguredLambdaInfo.ForFunc(factory, typeof(TSource), typeof(TTarget)); - if (factoryInfo == null) + if (factoryInfo != null) { - var contextTypeName = typeof(IMappingData).GetFriendlyName(); - var sourceTypeName = typeof(TSource).GetFriendlyName(); - var targetTypeName = typeof(TTarget).GetFriendlyName(); - var objectTypeName = typeof(TObject).GetFriendlyName(); - - string[] validSignatures = - { - $"Func<{objectTypeName}>", - $"Func<{contextTypeName}, {objectTypeName}>", - $"Func<{sourceTypeName}, {targetTypeName}, {objectTypeName}>", - $"Func<{sourceTypeName}, {targetTypeName}, int?, {objectTypeName}>" - }; - - throw new MappingConfigurationException(string.Format( - CultureInfo.InvariantCulture, - "Unable to create objects of type {0} using factory {1}: valid function signatures are {2}", - objectTypeName, - typeof(TFactory).GetFriendlyName(), - string.Join(", ", validSignatures))); + return RegisterObjectFactory(factoryInfo, ConfiguredObjectFactory.For); } - var objectFactory = ConfiguredObjectFactory.For(_configInfo, typeof(TObject), factoryInfo); + var contextTypeName = typeof(IMappingData).GetFriendlyName(); + var sourceTypeName = typeof(TSource).GetFriendlyName(); + var targetTypeName = typeof(TTarget).GetFriendlyName(); + var objectTypeName = typeof(TObject).GetFriendlyName(); + + string[] validSignatures = + { + $"Func<{objectTypeName}>", + $"Func<{contextTypeName}, {objectTypeName}>", + $"Func<{sourceTypeName}, {targetTypeName}, {objectTypeName}>", + $"Func<{sourceTypeName}, {targetTypeName}, int?, {objectTypeName}>" + }; + + throw new MappingConfigurationException(string.Format( + CultureInfo.InvariantCulture, + "Unable to create objects of type {0} using factory {1}: valid function signatures are {2}", + objectTypeName, + typeof(TFactory).GetFriendlyName(), + string.Join(", ", validSignatures))); + } + + private MappingConfigContinuation RegisterObjectFactory( + TFactory factory, + Func objectFactoryFactory) + { + var objectFactory = objectFactoryFactory.Invoke(_configInfo, typeof(TObject), factory); _configInfo.MapperContext.UserConfigurations.Add(objectFactory); + + return new MappingConfigContinuation(_configInfo); } } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/ICallbackSpecifier.cs b/AgileMapper/Api/Configuration/ICallbackSpecifier.cs index 780525545..e95eb90ca 100644 --- a/AgileMapper/Api/Configuration/ICallbackSpecifier.cs +++ b/AgileMapper/Api/Configuration/ICallbackSpecifier.cs @@ -17,10 +17,10 @@ public interface ICallbackSpecifier /// /// The callback to execute. /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. /// - MappingConfigContinuation Call(Action> callback); + IMappingConfigContinuation Call(Action> callback); /// /// Specify a callback to be executed. The condition expression is passed the current mapping's source @@ -28,10 +28,10 @@ public interface ICallbackSpecifier /// /// The callback to execute. /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. /// - MappingConfigContinuation Call(Action callback); + IMappingConfigContinuation Call(Action callback); /// /// Specify a callback to be executed. The condition expression is passed the current mapping's source @@ -39,9 +39,9 @@ public interface ICallbackSpecifier /// /// The callback to execute. /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. /// - MappingConfigContinuation Call(Action callback); + IMappingConfigContinuation Call(Action callback); } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/IConditionalRootMappingConfigurator.cs b/AgileMapper/Api/Configuration/IConditionalRootMappingConfigurator.cs index 05a74b4f4..cdcf65690 100644 --- a/AgileMapper/Api/Configuration/IConditionalRootMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/IConditionalRootMappingConfigurator.cs @@ -9,24 +9,24 @@ public interface IConditionalRootMappingConfigurator : IRootMappingConfigurator { /// - /// Map the source type being configured to the derived target type specified by the type argument if - /// the preceding condition evaluates to true. + /// Map the source type being configured to the derived target type specified by + /// if the preceding condition evaluates to true. /// /// The derived target type to create. /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. /// - MappingConfigContinuation MapTo() + IMappingConfigContinuation MapTo() where TDerivedTarget : TTarget; /// /// Map the target type being configured to null if the preceding condition evaluates to true. /// /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. /// - MappingConfigContinuation MapToNull(); + IMappingConfigContinuation MapToNull(); } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/ICustomMappingDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/ICustomMappingDataSourceTargetMemberSpecifier.cs new file mode 100644 index 000000000..1dd84ef36 --- /dev/null +++ b/AgileMapper/Api/Configuration/ICustomMappingDataSourceTargetMemberSpecifier.cs @@ -0,0 +1,57 @@ +namespace AgileObjects.AgileMapper.Api.Configuration +{ + using System; + using System.Linq.Expressions; + + /// + /// Provides options for specifying a target member to which a configuration option should apply. + /// + /// The source type to which the configuration should apply. + /// The target type to which the configuration should apply. + public interface ICustomMappingDataSourceTargetMemberSpecifier + { + /// + /// Apply the configuration to the given . + /// + /// The target member's type. + /// The target member to which to apply the configuration. + /// + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source + /// and target type being configured. + /// + IMappingConfigContinuation To( + Expression> targetMember); + + /// + /// Apply the configuration to the given . + /// + /// The type of the target set method's argument. + /// The target set method to which to apply the configuration. + /// + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source + /// and target type being configured. + /// + IMappingConfigContinuation To( + Expression>> targetSetMethod); + + /// + /// Apply the configuration to the constructor parameter with the type specified by the type argument. + /// + /// The target constructor parameter's type. + /// + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source + /// and target type being configured. + /// + IMappingConfigContinuation ToCtor(); + + /// + /// Apply the configuration to the constructor parameter with the specified . + /// + /// The target constructor parameter's name. + /// + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and + /// target type being configured. + /// + IMappingConfigContinuation ToCtor(string parameterName); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/IFullMappingConfigurator.cs b/AgileMapper/Api/Configuration/IFullMappingConfigurator.cs index 919de0dd2..ac333be96 100644 --- a/AgileMapper/Api/Configuration/IFullMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/IFullMappingConfigurator.cs @@ -23,8 +23,10 @@ public interface IFullMappingConfigurator : IFullMappingSettin /// /// The derived source type for which to configure a matching derived target type. /// - /// A DerivedPairTargetTypeSpecifier with which to specify the matching derived target type. - DerivedPairTargetTypeSpecifier Map() + /// + /// A IMappingDerivedPairTargetTypeSpecifier with which to specify the matching derived target type. + /// + IMappingDerivedPairTargetTypeSpecifier Map() where TDerivedSource : TSource; } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/IFullMappingInlineConfigurator.cs b/AgileMapper/Api/Configuration/IFullMappingInlineConfigurator.cs index 946460c8b..caf84b2c9 100644 --- a/AgileMapper/Api/Configuration/IFullMappingInlineConfigurator.cs +++ b/AgileMapper/Api/Configuration/IFullMappingInlineConfigurator.cs @@ -32,8 +32,9 @@ public interface IFullMappingInlineConfigurator : IFullMapping /// /// Throw an exception upon execution of this statement if the mapping being configured has any target members - /// which will not be mapped, or maps from a source enum to a target enum which does not support all of its - /// values. Use calls to this method to validate a mapping plan; remove them in production code. + /// which will not be mapped, maps from a source enum to a target enum which does not support all of its values, + /// or includes complex types which cannot be constructed. Use calls to this method to validate a mapping plan; + /// remove them in production code. /// void ThrowNowIfMappingPlanIsIncomplete(); diff --git a/AgileMapper/Api/Configuration/IFullMappingSettings.cs b/AgileMapper/Api/Configuration/IFullMappingSettings.cs index 7d89e2a3b..d3f3986e7 100644 --- a/AgileMapper/Api/Configuration/IFullMappingSettings.cs +++ b/AgileMapper/Api/Configuration/IFullMappingSettings.cs @@ -71,16 +71,17 @@ public interface IFullMappingSettings : IConditionalMappingCon IFullMappingSettings MapNullCollectionsToNull(); /// - /// Configure this mapper to pair the given with a member of another enum Type. + /// Configure this mapper to pair the given with a member of another + /// enum Type. /// - /// The type of the first enum being paired. + /// The type of the enum member to pair. /// The first enum member in the pair. /// - /// An EnumPairSpecifier with which to specify the enum member to which the given - /// should be paired. + /// An IMappingEnumPairSpecifier with which to specify the enum member to which the given + /// should be paired. /// - EnumPairSpecifier PairEnum(TFirstEnum enumMember) - where TFirstEnum : struct; + IMappingEnumPairSpecifier PairEnum(TPairingEnum enumMember) + where TPairingEnum : struct; /// /// Gets a link back to the full , for api fluency. diff --git a/AgileMapper/Api/Configuration/IGlobalConfigSettings.cs b/AgileMapper/Api/Configuration/IGlobalMappingSettings.cs similarity index 73% rename from AgileMapper/Api/Configuration/IGlobalConfigSettings.cs rename to AgileMapper/Api/Configuration/IGlobalMappingSettings.cs index 41e4aa994..f9b0377dd 100644 --- a/AgileMapper/Api/Configuration/IGlobalConfigSettings.cs +++ b/AgileMapper/Api/Configuration/IGlobalMappingSettings.cs @@ -6,7 +6,7 @@ /// /// Provides options for globally configuring how all mappers will perform mappings. /// - public interface IGlobalConfigSettings + public interface IGlobalMappingSettings { #region Exception Handling @@ -15,9 +15,9 @@ public interface IGlobalConfigSettings /// encounter an Exception will return null. /// /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - IGlobalConfigSettings SwallowAllExceptions(); + IGlobalMappingSettings SwallowAllExceptions(); /// /// Pass Exceptions thrown during a mapping to the given instead of throwing @@ -28,9 +28,9 @@ public interface IGlobalConfigSettings /// swallowed, it should be rethrown inside the callback. /// /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - IGlobalConfigSettings PassExceptionsTo(Action callback); + IGlobalMappingSettings PassExceptionsTo(Action callback); #endregion @@ -42,9 +42,9 @@ public interface IGlobalConfigSettings /// /// The prefix to ignore when matching source and target members. /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - IGlobalConfigSettings UseNamePrefix(string prefix); + IGlobalMappingSettings UseNamePrefix(string prefix); /// /// Expect members of all source and target types to potentially have any of the given name . @@ -52,9 +52,9 @@ public interface IGlobalConfigSettings /// /// The prefixes to ignore when matching source and target members. /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - IGlobalConfigSettings UseNamePrefixes(params string[] prefixes); + IGlobalMappingSettings UseNamePrefixes(params string[] prefixes); /// /// Expect members of all source and target types to potentially have the given name . @@ -62,9 +62,9 @@ public interface IGlobalConfigSettings /// /// The suffix to ignore when matching source and target members. /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - IGlobalConfigSettings UseNameSuffix(string suffix); + IGlobalMappingSettings UseNameSuffix(string suffix); /// /// Expect members of all source and target types to potentially have any of the given name . @@ -72,9 +72,9 @@ public interface IGlobalConfigSettings /// /// The suffixes to ignore when matching source and target members. /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - IGlobalConfigSettings UseNameSuffixes(params string[] suffixes); + IGlobalMappingSettings UseNameSuffixes(params string[] suffixes); /// /// Expect members of all source and target types to potentially match the given name . @@ -85,9 +85,9 @@ public interface IGlobalConfigSettings /// ^ character, end with the $ character and contain a single capturing group wrapped in parentheses, e.g. ^__(.+)__$ /// /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - IGlobalConfigSettings UseNamePattern(string pattern); + IGlobalMappingSettings UseNamePattern(string pattern); /// /// Expect members of all source and target types to potentially match the given name . @@ -98,9 +98,9 @@ public interface IGlobalConfigSettings /// ^ character, end with the $ character and contain a single capturing group wrapped in parentheses, e.g. ^__(.+)__$ /// /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - IGlobalConfigSettings UseNamePatterns(params string[] patterns); + IGlobalMappingSettings UseNamePatterns(params string[] patterns); #endregion @@ -111,9 +111,9 @@ public interface IGlobalConfigSettings /// this option is not necessary just to map circular relationships. /// /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - IGlobalConfigSettings MaintainIdentityIntegrity(); + IGlobalMappingSettings MaintainIdentityIntegrity(); /// /// Disable tracking of objects during circular relationship mapping between all source and target types. @@ -123,17 +123,17 @@ public interface IGlobalConfigSettings /// only once, disabling object tracking will increase mapping performance. /// /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - IGlobalConfigSettings DisableObjectTracking(); + IGlobalMappingSettings DisableObjectTracking(); /// /// Map null source collections to null instead of an empty collection, for all source and target types. /// /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - IGlobalConfigSettings MapNullCollectionsToNull(); + IGlobalMappingSettings MapNullCollectionsToNull(); /// /// Gets a link back to the full , for api fluency. diff --git a/AgileMapper/Api/Configuration/IMappingConfigContinuation.cs b/AgileMapper/Api/Configuration/IMappingConfigContinuation.cs new file mode 100644 index 000000000..c436f050d --- /dev/null +++ b/AgileMapper/Api/Configuration/IMappingConfigContinuation.cs @@ -0,0 +1,22 @@ +namespace AgileObjects.AgileMapper.Api.Configuration +{ + /// + /// Enables chaining of configurations for the same source and target type. + /// + /// The source type to which the configuration should apply. + /// The target type to which the configuration should apply. + public interface IMappingConfigContinuation + { + /// + /// Perform another configuration of how this mapper maps to and from the source and target types + /// being configured. This property exists purely to provide a more fluent configuration interface. + /// + IFullMappingConfigurator And { get; } + + /// + /// Perform an alternative configuration of how this mapper maps to and from the source and target types + /// being configured. This property exists purely to provide a more fluent configuration interface. + /// + IFullMappingConfigurator But { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/IMappingDerivedPairTargetTypeSpecifier.cs b/AgileMapper/Api/Configuration/IMappingDerivedPairTargetTypeSpecifier.cs new file mode 100644 index 000000000..7dc110073 --- /dev/null +++ b/AgileMapper/Api/Configuration/IMappingDerivedPairTargetTypeSpecifier.cs @@ -0,0 +1,27 @@ +namespace AgileObjects.AgileMapper.Api.Configuration +{ + /// + /// Enables the selection of a derived target type to which to match a configured source type. + /// + /// + /// The type of source object for which the derived type pair is being configured. + /// + /// + /// The type of target object for which the derived type pair is being configured. + /// + public interface IMappingDerivedPairTargetTypeSpecifier + { + /// + /// Map the derived source type being configured to the derived target type specified by the type argument. + /// + /// + /// The derived target type to create for the configured derived source type. + /// + /// + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and + /// target type being configured. + /// + IMappingConfigContinuation To() + where TDerivedTarget : TTarget; + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/IMappingEnumPairSpecifier.cs b/AgileMapper/Api/Configuration/IMappingEnumPairSpecifier.cs new file mode 100644 index 000000000..9f9bf7456 --- /dev/null +++ b/AgileMapper/Api/Configuration/IMappingEnumPairSpecifier.cs @@ -0,0 +1,23 @@ +namespace AgileObjects.AgileMapper.Api.Configuration +{ + /// + /// Provides options for specifying the enum member(s) with which the previously-specified enum member(s) + /// should be paired. + /// + /// The source type being configured. + /// The target type being configured. + public interface IMappingEnumPairSpecifier + { + /// + /// Configure this mapper to map the previously-specified enum member(s) to the given + /// . + /// + /// The type of enum the members of which are being paired. + /// + /// One or more enum members to pair to the previously-specified enum members. + /// + /// An IMappingConfigContinuation with which to configure other aspects of mapping. + IMappingConfigContinuation With(params TPairedEnum[] pairedEnumMembers) + where TPairedEnum : struct; + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/IFactorySpecifier.cs b/AgileMapper/Api/Configuration/IMappingFactorySpecifier.cs similarity index 69% rename from AgileMapper/Api/Configuration/IFactorySpecifier.cs rename to AgileMapper/Api/Configuration/IMappingFactorySpecifier.cs index 30e022bfb..7aec38b7f 100644 --- a/AgileMapper/Api/Configuration/IFactorySpecifier.cs +++ b/AgileMapper/Api/Configuration/IMappingFactorySpecifier.cs @@ -1,50 +1,58 @@ -namespace AgileObjects.AgileMapper.Api.Configuration -{ - using System; - using System.Linq.Expressions; - using Members; - - /// - /// Provides options for configuring custom factory objects with which to create instances of the type - /// specified by the type argument when mapping from and to the given - /// source and target types. - /// - /// The source type to which the configuration should apply. - /// The target type to which the configuration should apply. - /// The type of object which will be created by the configured factories. - public interface IFactorySpecifier - { - /// - /// Use the given expression to create instances of the object type being - /// configured. The factory expression is passed a context object containing the current mapping's source - /// and target objects. - /// - /// - /// The factory expression to use to create instances of the type being configured. - /// - void Using(Expression, TObject>> factory); - - /// - /// Use the given function to create instances of the object type being - /// configured. The following factory function signatures are supported: - /// - /// Func<TObject> - parameterless. - /// - /// - /// Func<IMappingData<TSource, TTarget>, TObject> - taking a context object containing the - /// current mapping's source and target objects. - /// - /// - /// Func<TSource, TTarget, TObject> - taking the source and target objects. - /// - /// - /// Func<TSource, TTarget, int?, TObject> - taking the source and target objects and the current - /// enumerable index, if applicable. - /// - /// - /// - /// The factory function to use to create instances of the type being configured. - /// - void Using(TFactory factory) where TFactory : class; - } +namespace AgileObjects.AgileMapper.Api.Configuration +{ + using System; + using System.Linq.Expressions; + using Members; + + /// + /// Provides options for configuring custom factory objects with which to create instances of the + /// Type, when mapping from and to the given source and target types. + /// + /// The source type to which the configuration should apply. + /// The target type to which the configuration should apply. + /// The type of object which will be created by the configured factories. + public interface IMappingFactorySpecifier + { + /// + /// Use the given expression to create instances of the object type being + /// configured. The factory expression is passed a context object containing the current mapping's source + /// and target objects. + /// + /// + /// The factory expression to use to create instances of the type being configured. + /// + /// + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source + /// and target type being configured. + /// + IMappingConfigContinuation Using(Expression, TObject>> factory); + + /// + /// Use the given function to create instances of the object type being + /// configured. The following factory function signatures are supported: + /// + /// Func<TObject> - parameterless. + /// + /// + /// Func<IMappingData<TSource, TTarget>, TObject> - taking a context object containing the + /// current mapping's source and target objects. + /// + /// + /// Func<TSource, TTarget, TObject> - taking the source and target objects. + /// + /// + /// Func<TSource, TTarget, int?, TObject> - taking the source and target objects and the current + /// enumerable index, if applicable. + /// + /// + /// + /// The factory function to use to create instances of the type being configured. + /// + /// + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source + /// and target type being configured. + /// + IMappingConfigContinuation Using(TFactory factory) + where TFactory : class; + } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/IPostInstanceCreationCallbackSpecifier.cs b/AgileMapper/Api/Configuration/IPostInstanceCreationCallbackSpecifier.cs index 73f796e09..fde81ccd1 100644 --- a/AgileMapper/Api/Configuration/IPostInstanceCreationCallbackSpecifier.cs +++ b/AgileMapper/Api/Configuration/IPostInstanceCreationCallbackSpecifier.cs @@ -18,10 +18,10 @@ public interface IPostInstanceCreationCallbackSpecifier /// The callback to call. /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. /// - MappingConfigContinuation Call( + IMappingConfigContinuation Call( Action> callback); /// @@ -30,10 +30,10 @@ MappingConfigContinuation Call( /// /// The callback to call. /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. - /// - MappingConfigContinuation Call(Action callback); + /// + IMappingConfigContinuation Call(Action callback); /// /// Configure a callback to call in the configured conditions. The callback is passed the current @@ -41,10 +41,10 @@ MappingConfigContinuation Call( /// /// The callback to call. /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. - /// - MappingConfigContinuation Call(Action callback); + /// + IMappingConfigContinuation Call(Action callback); /// /// Configure a callback to call in the configured conditions. The callback is passed the current @@ -52,9 +52,9 @@ MappingConfigContinuation Call( /// /// The callback to call. /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. /// - MappingConfigContinuation Call(Action callback); + IMappingConfigContinuation Call(Action callback); } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/IPreInstanceCreationCallbackSpecifier.cs b/AgileMapper/Api/Configuration/IPreInstanceCreationCallbackSpecifier.cs index 6cffbaa77..a62033a35 100644 --- a/AgileMapper/Api/Configuration/IPreInstanceCreationCallbackSpecifier.cs +++ b/AgileMapper/Api/Configuration/IPreInstanceCreationCallbackSpecifier.cs @@ -21,7 +21,7 @@ public interface IPreInstanceCreationCallbackSpecifier /// target type being configured. /// /// - MappingConfigContinuation Call(Action> callback); + IMappingConfigContinuation Call(Action> callback); /// /// Configure a callback to call in the configured conditions. The callback is passed the current @@ -32,7 +32,7 @@ public interface IPreInstanceCreationCallbackSpecifier /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. /// - MappingConfigContinuation Call(Action callback); + IMappingConfigContinuation Call(Action callback); /// /// Configure a callback to call in the configured conditions. The callback is passed the current @@ -43,6 +43,6 @@ public interface IPreInstanceCreationCallbackSpecifier /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. /// - MappingConfigContinuation Call(Action callback); + IMappingConfigContinuation Call(Action callback); } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/IRootMappingConfigurator.cs b/AgileMapper/Api/Configuration/IRootMappingConfigurator.cs index 3aa0fb7b6..fbed57229 100644 --- a/AgileMapper/Api/Configuration/IRootMappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/IRootMappingConfigurator.cs @@ -21,10 +21,10 @@ public interface IRootMappingConfigurator /// The factory expression to use to create instances of the type being configured. /// /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. /// - MappingConfigContinuation CreateInstancesUsing( + IMappingConfigContinuation CreateInstancesUsing( Expression, TTarget>> factory); /// @@ -49,30 +49,32 @@ MappingConfigContinuation CreateInstancesUsing( /// The factory function to use to create instances of the type being configured. /// /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. /// - MappingConfigContinuation CreateInstancesUsing(TFactory factory) where TFactory : class; + IMappingConfigContinuation CreateInstancesUsing(TFactory factory) + where TFactory : class; /// - /// Configure a factory to use to create instances of the type specified by the type argument. + /// Configure a factory to use to create instances of the Type. /// - /// The type of object the creation of which is to be configured. + /// The Type of object the creation of which is to be configured. /// - /// An IFactorySpecifier with which to configure the factory for the type specified by the type argument. + /// An IMappingFactorySpecifier with which to configure the factory for the + /// Type. /// - IFactorySpecifier CreateInstancesOf() where TObject : class; + IMappingFactorySpecifier CreateInstancesOf(); /// - /// Ignore the target member(s) identified by the argument when mapping - /// from and to the source and target types being configured. + /// Ignore the given when mappingfrom and to the source and target types + /// being configured. /// /// The target member(s) which should be ignored. /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and /// target type being configured. /// - MappingConfigContinuation Ignore(params Expression>[] targetMembers); + IMappingConfigContinuation Ignore(params Expression>[] targetMembers); /// /// Ignore all target member(s) of the given Type when mapping @@ -80,10 +82,10 @@ MappingConfigContinuation CreateInstancesUsing( /// /// The Type of target member to ignore. /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and - /// target type being configured. + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and + /// target types being configured. /// - MappingConfigContinuation IgnoreTargetMembersOfType(); + IMappingConfigContinuation IgnoreTargetMembersOfType(); /// /// Ignore all target member(s) matching the given when mapping @@ -91,10 +93,11 @@ MappingConfigContinuation CreateInstancesUsing( /// /// The matching function with which to select target members to ignore. /// - /// A MappingConfigContinuation to enable further configuration of mappings from and to the source and - /// target type being configured. + /// An IMappingConfigContinuation to enable further configuration of mappings from and to the source and + /// target types being configured. /// - MappingConfigContinuation IgnoreTargetMembersWhere(Expression> memberFilter); + IMappingConfigContinuation IgnoreTargetMembersWhere( + Expression> memberFilter); /// /// Configure a custom data source for a particular target member when mapping from and to the source and @@ -104,10 +107,10 @@ MappingConfigContinuation CreateInstancesUsing( /// The type of the custom value being configured. /// The expression to map to the configured target member. /// - /// A CustomDataSourceTargetMemberSpecifier with which to specify the target member to which the custom - /// value should be applied. + /// An ICustomMappingDataSourceTargetMemberSpecifier with which to specify the target member to which the + /// custom value should be applied. /// - CustomDataSourceTargetMemberSpecifier Map( + ICustomMappingDataSourceTargetMemberSpecifier Map( Expression, TSourceValue>> valueFactoryExpression); /// @@ -118,10 +121,10 @@ CustomDataSourceTargetMemberSpecifier Map( /// The type of the custom value being configured. /// The expression to map to the configured target member. /// - /// A CustomDataSourceTargetMemberSpecifier with which to specify the target member to which the custom - /// value should be applied. + /// An ICustomMappingDataSourceTargetMemberSpecifier with which to specify the target member to which the + /// custom value should be applied. /// - CustomDataSourceTargetMemberSpecifier Map( + ICustomMappingDataSourceTargetMemberSpecifier Map( Expression> valueFactoryExpression); /// @@ -132,10 +135,10 @@ CustomDataSourceTargetMemberSpecifier Map( /// The type of the custom value being configured. /// The expression to map to the configured target member. /// - /// A CustomDataSourceTargetMemberSpecifier with which to specify the target member to which the custom - /// value should be applied. + /// An ICustomMappingDataSourceTargetMemberSpecifier with which to specify the target member to which the + /// custom value should be applied. /// - CustomDataSourceTargetMemberSpecifier Map( + ICustomMappingDataSourceTargetMemberSpecifier Map( Expression> valueFactoryExpression); /// @@ -144,10 +147,10 @@ CustomDataSourceTargetMemberSpecifier Map( /// The type of value returned by the given Func. /// The Func object to map to the configured target member. /// - /// A CustomDataSourceTargetMemberSpecifier with which to specify the target member to which the custom - /// value should be applied. + /// An ICustomMappingDataSourceTargetMemberSpecifier with which to specify the target member to which the + /// custom value should be applied. /// - CustomDataSourceTargetMemberSpecifier MapFunc( + ICustomMappingDataSourceTargetMemberSpecifier MapFunc( Func valueFunc); /// @@ -157,9 +160,9 @@ CustomDataSourceTargetMemberSpecifier MapFunc( /// The type of the custom constant value being configured. /// The constant value to map to the configured target member. /// - /// A CustomDataSourceTargetMemberSpecifier with which to specify the target member to which the custom - /// constant value should be applied. + /// An ICustomMappingDataSourceTargetMemberSpecifier with which to specify the target member to which the + /// custom constant value should be applied. /// - CustomDataSourceTargetMemberSpecifier Map(TSourceValue value); + ICustomMappingDataSourceTargetMemberSpecifier Map(TSourceValue value); } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/InstanceCreationCallbackSpecifier.cs b/AgileMapper/Api/Configuration/InstanceCreationCallbackSpecifier.cs index 888754b3f..c2d614582 100644 --- a/AgileMapper/Api/Configuration/InstanceCreationCallbackSpecifier.cs +++ b/AgileMapper/Api/Configuration/InstanceCreationCallbackSpecifier.cs @@ -45,19 +45,19 @@ private InstanceCreationCallbackSpecifier SetConditio return this; } - MappingConfigContinuation IPreInstanceCreationCallbackSpecifier.Call( + IMappingConfigContinuation IPreInstanceCreationCallbackSpecifier.Call( Action> callback) { return CreateCallbackFactory(callback); } - MappingConfigContinuation IPreInstanceCreationCallbackSpecifier.Call( + IMappingConfigContinuation IPreInstanceCreationCallbackSpecifier.Call( Action callback) { return CreateCallbackFactory(callback); } - MappingConfigContinuation IPreInstanceCreationCallbackSpecifier.Call( + IMappingConfigContinuation IPreInstanceCreationCallbackSpecifier.Call( Action callback) { return CreateCallbackFactory(callback); @@ -87,19 +87,19 @@ MappingConfigContinuation IPreInstanceCreationCallbackSpecifie Expression> condition) => SetCondition(condition); - MappingConfigContinuation IPostInstanceCreationCallbackSpecifier.Call( + IMappingConfigContinuation IPostInstanceCreationCallbackSpecifier.Call( Action> callback) => CreateCallbackFactory(callback); - MappingConfigContinuation IPostInstanceCreationCallbackSpecifier.Call( + IMappingConfigContinuation IPostInstanceCreationCallbackSpecifier.Call( Action callback) => CreateCallbackFactory(callback); - MappingConfigContinuation IPostInstanceCreationCallbackSpecifier.Call( + IMappingConfigContinuation IPostInstanceCreationCallbackSpecifier.Call( Action callback) => CreateCallbackFactory(callback); - MappingConfigContinuation IPostInstanceCreationCallbackSpecifier.Call( + IMappingConfigContinuation IPostInstanceCreationCallbackSpecifier.Call( Action callback) => CreateCallbackFactory(callback); diff --git a/AgileMapper/Api/Configuration/MappingConfigContinuation.cs b/AgileMapper/Api/Configuration/MappingConfigContinuation.cs index 78db49766..d43fc8ae2 100644 --- a/AgileMapper/Api/Configuration/MappingConfigContinuation.cs +++ b/AgileMapper/Api/Configuration/MappingConfigContinuation.cs @@ -1,33 +1,31 @@ namespace AgileObjects.AgileMapper.Api.Configuration { using AgileMapper.Configuration; + using Projection; - /// - /// Enables chaining of configurations for the same source and target type. - /// - /// The source type to which the configuration should apply. - /// The target type to which the configuration should apply. - public class MappingConfigContinuation + + internal class MappingConfigContinuation : + IMappingConfigContinuation, + IProjectionConfigContinuation { private readonly MappingConfigInfo _configInfo; - internal MappingConfigContinuation(MappingConfigInfo configInfo) + public MappingConfigContinuation(MappingConfigInfo configInfo) { _configInfo = configInfo; - } + } + + public IFullMappingConfigurator And => CreateNewConfigurator(); - /// - /// Perform another configuration of how this mapper maps to and from the source and target types - /// being configured. This property exists purely to provide a more fluent configuration interface. - /// - public IFullMappingConfigurator And - => new MappingConfigurator(_configInfo.Clone()); + IFullProjectionConfigurator IProjectionConfigContinuation.And + => CreateNewConfigurator(); + + public IFullMappingConfigurator But => CreateNewConfigurator(); + + IFullProjectionConfigurator IProjectionConfigContinuation.But + => CreateNewConfigurator(); - /// - /// Perform an alternative configuration of how this mapper maps to and from the source and target types - /// being configured. This property exists purely to provide a more fluent configuration interface. - /// - public IFullMappingConfigurator But + private MappingConfigurator CreateNewConfigurator() => new MappingConfigurator(_configInfo.Clone()); } } \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs b/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs index 75b6dd52d..a5549fc04 100644 --- a/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs +++ b/AgileMapper/Api/Configuration/MappingConfigStartingPoint.cs @@ -11,13 +11,17 @@ using Dynamics; using Extensions.Internal; using Members; + using Projection; using static Constants; using static AgileMapper.Configuration.Dictionaries.DictionaryType; /// /// Provides options for configuring how a mapper performs a mapping. /// - public class MappingConfigStartingPoint : IGlobalConfigSettings + public class MappingConfigStartingPoint : + IGlobalMappingSettings, + IGlobalProjectionSettings, + IProjectionConfigStartingPoint { private readonly MappingConfigInfo _configInfo; @@ -37,9 +41,9 @@ internal MappingConfigStartingPoint(MapperContext mapperContext) /// encounter an Exception will return null. /// /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings SwallowAllExceptions() => PassExceptionsTo(ctx => { }); + public IGlobalMappingSettings SwallowAllExceptions() => PassExceptionsTo(ctx => { }); /// /// Pass Exceptions thrown during a mapping to the given instead of throwing @@ -50,9 +54,9 @@ internal MappingConfigStartingPoint(MapperContext mapperContext) /// swallowed, it should be rethrown inside the callback. /// /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings PassExceptionsTo(Action callback) + public IGlobalMappingSettings PassExceptionsTo(Action callback) { var exceptionCallback = new ExceptionCallback(GlobalConfigInfo, callback.ToConstantExpression()); @@ -70,9 +74,9 @@ public IGlobalConfigSettings PassExceptionsTo(Action call /// /// The prefix to ignore when matching source and target members. /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings UseNamePrefix(string prefix) => UseNamePrefixes(prefix); + public IGlobalMappingSettings UseNamePrefix(string prefix) => UseNamePrefixes(prefix); /// /// Expect members of all source and target types to potentially have any of the given name . @@ -80,9 +84,9 @@ public IGlobalConfigSettings PassExceptionsTo(Action call /// /// The prefixes to ignore when matching source and target members. /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings UseNamePrefixes(params string[] prefixes) + public IGlobalMappingSettings UseNamePrefixes(params string[] prefixes) { MapperContext.Naming.AddNamePrefixes(prefixes); return this; @@ -94,9 +98,9 @@ public IGlobalConfigSettings UseNamePrefixes(params string[] prefixes) /// /// The suffix to ignore when matching source and target members. /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings UseNameSuffix(string suffix) => UseNameSuffixes(suffix); + public IGlobalMappingSettings UseNameSuffix(string suffix) => UseNameSuffixes(suffix); /// /// Expect members of all source and target types to potentially have any of the given name . @@ -104,9 +108,9 @@ public IGlobalConfigSettings UseNamePrefixes(params string[] prefixes) /// /// The suffixes to ignore when matching source and target members. /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings UseNameSuffixes(params string[] suffixes) + public IGlobalMappingSettings UseNameSuffixes(params string[] suffixes) { MapperContext.Naming.AddNameSuffixes(suffixes); return this; @@ -121,9 +125,9 @@ public IGlobalConfigSettings UseNameSuffixes(params string[] suffixes) /// ^ character, end with the $ character and contain a single capturing group wrapped in parentheses, e.g. ^__(.+)__$ /// /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings UseNamePattern(string pattern) => UseNamePatterns(pattern); + public IGlobalMappingSettings UseNamePattern(string pattern) => UseNamePatterns(pattern); /// /// Expect members of all source and target types to potentially match the given name . @@ -134,9 +138,9 @@ public IGlobalConfigSettings UseNameSuffixes(params string[] suffixes) /// ^ character, end with the $ character and contain a single capturing group wrapped in parentheses, e.g. ^__(.+)__$ /// /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings UseNamePatterns(params string[] patterns) + public IGlobalMappingSettings UseNamePatterns(params string[] patterns) { MapperContext.Naming.AddNameMatchers(patterns); return this; @@ -151,9 +155,9 @@ public IGlobalConfigSettings UseNamePatterns(params string[] patterns) /// this option is not necessary when mapping circular relationships. /// /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings MaintainIdentityIntegrity() + public IGlobalMappingSettings MaintainIdentityIntegrity() { MapperContext.UserConfigurations.Add(MappedObjectCachingSettings.CacheAll); return this; @@ -167,9 +171,9 @@ public IGlobalConfigSettings MaintainIdentityIntegrity() /// only once, disabling object tracking will increase mapping performance. /// /// - /// An with which to globally configure other mapping aspects. + /// An with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings DisableObjectTracking() + public IGlobalMappingSettings DisableObjectTracking() { MapperContext.UserConfigurations.Add(MappedObjectCachingSettings.CacheNone); return this; @@ -179,9 +183,9 @@ public IGlobalConfigSettings DisableObjectTracking() /// Map null source collections to null instead of an empty collection, for all source and target types. /// /// - /// This with which to globally configure other mapping aspects. + /// This with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings MapNullCollectionsToNull() + public IGlobalMappingSettings MapNullCollectionsToNull() { MapperContext.UserConfigurations.Add(new NullCollectionsSetting(GlobalConfigInfo)); return this; @@ -189,45 +193,50 @@ public IGlobalConfigSettings MapNullCollectionsToNull() /// /// Throw an exception upon creation of a mapper if the mapping plan has any target members which will not be mapped, - /// or maps from a source enum to a target enum which does not support all of its values. Call this method to validate - /// mapping plans during development; remove it in production code. + /// maps from a source enum to a target enum which does not support all of its values, or includes complex types which + /// cannot be constructed. Call this method to validate mapping plans during development; remove it in production code. /// /// - /// This with which to globally configure other mapping aspects. + /// This with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings ThrowIfAnyMappingPlanIsIncomplete() + public IGlobalMappingSettings ThrowIfAnyMappingPlanIsIncomplete() { MapperContext.UserConfigurations.ValidateMappingPlans = true; return this; } /// - /// Configure this mapper to pair the given with a member of another enum Type. - /// This pairing will apply to mappings between all types and MappingRuleSets (create new, overwrite, etc). + /// Configure this mapper to pair the given with a member of another + /// enum Type. This pairing will apply to mappings between all types and MappingRuleSets (create new, + /// overwrite, etc). /// - /// The type of the first enum being paired. - /// The first enum member in the pair. + /// The type of the enum member to pair. + /// The enum member to pair. /// - /// An EnumPairSpecifier with which to specify the enum member to which the given - /// should be paired. + /// An IMappingEnumPairSpecifier with which to specify the enum member to which the given + /// should be paired. /// - public EnumPairSpecifier PairEnum(TFirstEnum enumMember) where TFirstEnum : struct - => PairEnums(enumMember); + public IMappingEnumPairSpecifier PairEnum(TPairingEnum enumMember) + where TPairingEnum : struct + { + return PairEnums(enumMember); + } /// - /// Configure this mapper to pair the given with members of another enum Type. - /// Pairings will apply to mappings between all types and MappingRuleSets (create new, overwrite, etc). + /// Configure this mapper to pair the given with members of another + /// enum Type. Pairings will apply to mappings between all types and MappingRuleSets (create new, + /// overwrite, etc). /// - /// The type of the first set of enum members being paired. - /// The first set of enum members to pair. + /// The type of the enum members to pair. + /// The enum members to pair. /// - /// An EnumPairSpecifier with which to specify the set of enum members to which the given - /// should be paired. + /// An IMappingEnumPairSpecifier with which to specify the set of enum members to which the given + /// should be paired. /// - public EnumPairSpecifier PairEnums(params TFirstEnum[] enumMembers) - where TFirstEnum : struct + public IMappingEnumPairSpecifier PairEnums(params TPairingEnum[] enumMembers) + where TPairingEnum : struct { - return EnumPairSpecifier.For(GlobalConfigInfo, enumMembers); + return EnumPairSpecifier.For(GlobalConfigInfo, enumMembers); } /// @@ -236,9 +245,9 @@ public EnumPairSpecifier PairEnums(param /// /// The assemblies in which to look for derived types. /// - /// This with which to globally configure other mapping aspects. + /// This with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings LookForDerivedTypesIn(params Assembly[] assemblies) + public IGlobalMappingSettings LookForDerivedTypesIn(params Assembly[] assemblies) { SetDerivedTypeAssemblies(assemblies); return this; @@ -271,9 +280,9 @@ internal static void SetDerivedTypeAssemblies(Assembly[] assemblies) /// /// The Type of target member to ignore. /// - /// This with which to globally configure other mapping aspects. + /// This with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings IgnoreTargetMembersOfType() + public IGlobalMappingSettings IgnoreTargetMembersOfType() { return IgnoreTargetMembersWhere(member => member.HasType()); } @@ -284,9 +293,9 @@ public IGlobalConfigSettings IgnoreTargetMembersOfType() /// /// The matching function with which to select target members to ignore. /// - /// This with which to globally configure other mapping aspects. + /// This with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings IgnoreTargetMembersWhere(Expression> memberFilter) + public IGlobalMappingSettings IgnoreTargetMembersWhere(Expression> memberFilter) { var configuredIgnoredMember = new ConfiguredIgnoredMember(GlobalConfigInfo, memberFilter); @@ -303,9 +312,19 @@ public IGlobalConfigSettings IgnoreTargetMembersWhere(ExpressionThe source value type to which to apply a formatting string. /// An action which supplies the formatting string. /// - /// This with which to globally configure other mapping aspects. + /// This with which to globally configure other mapping aspects. /// - public IGlobalConfigSettings StringsFrom(Action formatSelector) + public IGlobalMappingSettings StringsFrom(Action formatSelector) + => RegisterStringFormatter(formatSelector); + + IGlobalProjectionSettings IProjectionConfigStartingPoint.StringsFrom( + Action formatSelector) + { + return RegisterStringFormatter(formatSelector); + } + + private MappingConfigStartingPoint RegisterStringFormatter( + Action formatSelector) { var formatSpecifier = new StringFormatSpecifier(MapperContext, typeof(TSourceValue)); @@ -316,7 +335,9 @@ public IGlobalConfigSettings StringsFrom(Action this; + MappingConfigStartingPoint IGlobalMappingSettings.AndWhenMapping => this; + + IProjectionConfigStartingPoint IGlobalProjectionSettings.AndWhenMapping => this; private MappingConfigInfo GlobalConfigInfo => _configInfo.ForAllRuleSets().ForAllSourceTypes().ForAllTargetTypes(); @@ -428,27 +449,30 @@ private DictionaryMappingConfigurator CreateDictionaryConfigurator From() => GetTargetTypeSpecifier(ci => ci.ForSourceType()); + IProjectionResultSelector IProjectionConfigStartingPoint.From() + => From(); + /// - /// Configure how this mapper performs mappings from all source types and MappingRuleSets (create new, overwrite, - /// etc), to the target type specified by the type argument. + /// Configure how this mapper performs mappings from all source types and MappingRuleSets (create new, + /// overwrite, etc), to the Type. /// - /// The target type to which the configuration will apply. + /// The target Type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. public IFullMappingConfigurator To() => GetAllSourcesTargetTypeSpecifier(ci => ci.ForAllRuleSets()).To(); /// - /// Configure how this mapper performs object creation mappings from any source type to the target type - /// specified by the type argument. + /// Configure how this mapper performs object creation mappings from any source type to the + /// Type. /// - /// The target type to which the configuration will apply. + /// The result Type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. - public IFullMappingConfigurator ToANew() - => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(CreateNew)).ToANew(); + public IFullMappingConfigurator ToANew() + => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(CreateNew)).ToANew(); /// - /// Configure how this mapper performs OnTo (merge) mappings from any source type to the target type - /// specified by the type argument. + /// Configure how this mapper performs OnTo (merge) mappings from any source type to the + /// Type. /// /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. @@ -456,14 +480,23 @@ public IFullMappingConfigurator OnTo() => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(Merge)).OnTo(); /// - /// Configure how this mapper performs Over (overwrite) mappings from any source type to the target type - /// specified by the type argument. + /// Configure how this mapper performs Over (overwrite) mappings from any source type to the + /// Type. /// - /// The target type to which the configuration will apply. + /// The target Type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. public IFullMappingConfigurator Over() => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(Overwrite)).Over(); + /// + /// Configure how this mapper performs query projection mappings from any source type to the + /// Type. + /// + /// The result Type to which the configuration will apply. + /// An IFullProjectionConfigurator with which to complete the configuration. + public IFullProjectionConfigurator ProjectionsTo() + => GetAllSourcesTargetTypeSpecifier(ci => ci.ForRuleSet(Project)).ProjectedTo(); + private TargetSpecifier GetAllSourcesTargetTypeSpecifier( Func configInfoConfigurator) { diff --git a/AgileMapper/Api/Configuration/MappingConfigurator.cs b/AgileMapper/Api/Configuration/MappingConfigurator.cs index 65eb7ea12..e6d278ebb 100644 --- a/AgileMapper/Api/Configuration/MappingConfigurator.cs +++ b/AgileMapper/Api/Configuration/MappingConfigurator.cs @@ -1,18 +1,23 @@ namespace AgileObjects.AgileMapper.Api.Configuration { using System; + using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; using AgileMapper.Configuration; + using AgileMapper.Configuration.Projection; using Dictionaries; using Dynamics; using Extensions.Internal; using Members; + using Projection; using Validation; internal class MappingConfigurator : IFullMappingInlineConfigurator, - IConditionalRootMappingConfigurator + IFullProjectionInlineConfigurator, + IConditionalRootMappingConfigurator, + IConditionalRootProjectionConfigurator { public MappingConfigurator(MappingConfigInfo configInfo) { @@ -33,6 +38,9 @@ public MappingConfigurator(MappingConfigInfo configInfo) public MappingConfigStartingPoint WhenMapping => new MappingConfigStartingPoint(MapperContext); + IProjectionConfigStartingPoint IFullProjectionInlineConfigurator.WhenMapping + => WhenMapping; + public ITargetDictionaryMappingInlineConfigurator ForDictionaries => new TargetDictionaryMappingConfigurator(ConfigInfo); @@ -77,11 +85,62 @@ public IFullMappingInlineConfigurator UseNamePatterns(params s #endregion + #region IFullProjectionInlineConfigurator Members + + public IFullProjectionInlineConfigurator RecurseToDepth(int recursionDepth) + { + var depthSettings = new RecursionDepthSettings(ConfigInfo, recursionDepth); + + ConfigInfo.MapperContext.UserConfigurations.Add(depthSettings); + return this; + } + + public IConditionalRootProjectionConfigurator If(Expression> condition) + => SetCondition(condition); + + #region Naming + + IFullProjectionInlineConfigurator IFullProjectionInlineConfigurator.UseNamePrefix( + string prefix) => ((IFullProjectionInlineConfigurator)this).UseNamePrefixes(prefix); + + IFullProjectionInlineConfigurator IFullProjectionInlineConfigurator.UseNamePrefixes( + params string[] prefixes) + { + MapperContext.Naming.AddNamePrefixes(prefixes); + return this; + } + + IFullProjectionInlineConfigurator IFullProjectionInlineConfigurator.UseNameSuffix( + string suffix) => ((IFullProjectionInlineConfigurator)this).UseNameSuffixes(suffix); + + IFullProjectionInlineConfigurator IFullProjectionInlineConfigurator.UseNameSuffixes( + params string[] suffixes) + { + MapperContext.Naming.AddNameSuffixes(suffixes); + return this; + } + + IFullProjectionInlineConfigurator IFullProjectionInlineConfigurator.UseNamePattern( + string pattern) => ((IFullProjectionInlineConfigurator)this).UseNamePatterns(pattern); + + IFullProjectionInlineConfigurator IFullProjectionInlineConfigurator.UseNamePatterns( + params string[] patterns) + { + MapperContext.Naming.AddNameMatchers(patterns); + return this; + } + + #endregion + + #endregion + #region If Overloads public IConditionalRootMappingConfigurator If( Expression, bool>> condition) - => SetCondition(condition); + { + return SetCondition(condition); + } public IConditionalRootMappingConfigurator If(Expression> condition) => SetCondition(condition); @@ -89,7 +148,7 @@ public IConditionalRootMappingConfigurator If(Expression If(Expression> condition) => SetCondition(condition); - private IConditionalRootMappingConfigurator SetCondition(LambdaExpression conditionLambda) + private MappingConfigurator SetCondition(LambdaExpression conditionLambda) { ConfigInfo.AddConditionOrThrow(conditionLambda); return this; @@ -97,24 +156,46 @@ private IConditionalRootMappingConfigurator SetCondition(Lambd #endregion - public MappingConfigContinuation CreateInstancesUsing( + #region Instance Creation + + public IMappingConfigContinuation CreateInstancesUsing( Expression, TTarget>> factory) { - new FactorySpecifier(ConfigInfo).Using(factory); + return RegisterFactory(factory); + } + + public IProjectionConfigContinuation CreateInstancesUsing( + Expression> factory) + { + return RegisterFactory(factory); + } + + private MappingConfigContinuation RegisterFactory(LambdaExpression factory) + { + CreateFactorySpecifier().Using(factory); return new MappingConfigContinuation(ConfigInfo); } - public MappingConfigContinuation CreateInstancesUsing(TFactory factory) where TFactory : class + public IMappingConfigContinuation CreateInstancesUsing(TFactory factory) + where TFactory : class { - new FactorySpecifier(ConfigInfo).Using(factory); + CreateFactorySpecifier().Using(factory); return new MappingConfigContinuation(ConfigInfo); } - public IFactorySpecifier CreateInstancesOf() where TObject : class + public IMappingFactorySpecifier CreateInstancesOf() + => CreateFactorySpecifier(); + + IProjectionFactorySpecifier IRootProjectionConfigurator.CreateInstancesOf() + => CreateFactorySpecifier(); + + private FactorySpecifier CreateFactorySpecifier() => new FactorySpecifier(ConfigInfo); + #endregion + public IFullMappingSettings SwallowAllExceptions() => PassExceptionsTo(ctx => { }); public IFullMappingSettings PassExceptionsTo(Action> callback) @@ -145,20 +226,43 @@ public IFullMappingSettings MapNullCollectionsToNull() return this; } - public EnumPairSpecifier PairEnum(TFirstEnum enumMember) + public IMappingEnumPairSpecifier PairEnum(TFirstEnum enumMember) where TFirstEnum : struct - => EnumPairSpecifier.For(ConfigInfo, new[] { enumMember }); + { + return EnumPairSpecifier.For(ConfigInfo, enumMember); + } + + IProjectionEnumPairSpecifier IFullProjectionSettings.PairEnum( + TFirstEnum enumMember) + { + return EnumPairSpecifier.For(ConfigInfo, enumMember); + } IFullMappingConfigurator IFullMappingSettings.And => this; + IFullProjectionConfigurator IFullProjectionSettings.And => this; + #region Ignoring Members - public MappingConfigContinuation IgnoreTargetMembersOfType() + public IMappingConfigContinuation IgnoreTargetMembersOfType() + => IgnoreMembersByFilter(member => member.HasType()); + + IProjectionConfigContinuation IRootProjectionConfigurator.IgnoreTargetMembersOfType() + => IgnoreMembersByFilter(member => member.HasType()); + + public IMappingConfigContinuation IgnoreTargetMembersWhere( + Expression> memberFilter) { - return IgnoreTargetMembersWhere(member => member.HasType()); + return IgnoreMembersByFilter(memberFilter); } - public MappingConfigContinuation IgnoreTargetMembersWhere( + IProjectionConfigContinuation IRootProjectionConfigurator.IgnoreTargetMembersWhere( + Expression> memberFilter) + { + return IgnoreMembersByFilter(memberFilter); + } + + private MappingConfigContinuation IgnoreMembersByFilter( Expression> memberFilter) { var configuredIgnoredMember = new ConfiguredIgnoredMember(ConfigInfo, memberFilter); @@ -168,7 +272,17 @@ public MappingConfigContinuation IgnoreTargetMembersWhere( return new MappingConfigContinuation(ConfigInfo); } - public MappingConfigContinuation Ignore(params Expression>[] targetMembers) + public IMappingConfigContinuation Ignore(params Expression>[] targetMembers) + => IgnoreMembers(targetMembers); + + IProjectionConfigContinuation IRootProjectionConfigurator.Ignore( + params Expression>[] resultMembers) + { + return IgnoreMembers(resultMembers); + } + + private MappingConfigContinuation IgnoreMembers( + IEnumerable>> targetMembers) { foreach (var targetMember in targetMembers) { @@ -192,50 +306,65 @@ public PostEventMappingConfigStartingPoint After #region Map Overloads - public CustomDataSourceTargetMemberSpecifier Map( + public ICustomMappingDataSourceTargetMemberSpecifier Map( Expression, TSourceValue>> valueFactoryExpression) { - return new CustomDataSourceTargetMemberSpecifier( - ConfigInfo.ForSourceValueType(), - valueFactoryExpression); + return GetValueFactoryTargetMemberSpecifier(valueFactoryExpression); + } + + public ICustomProjectionDataSourceTargetMemberSpecifier Map( + Expression> valueFactoryExpression) + { + return GetValueFactoryTargetMemberSpecifier(valueFactoryExpression); } - public CustomDataSourceTargetMemberSpecifier Map( + public ICustomMappingDataSourceTargetMemberSpecifier Map( Expression> valueFactoryExpression) { - return new CustomDataSourceTargetMemberSpecifier( - ConfigInfo.ForSourceValueType(), - valueFactoryExpression); + return GetValueFactoryTargetMemberSpecifier(valueFactoryExpression); } - public CustomDataSourceTargetMemberSpecifier Map( + public ICustomMappingDataSourceTargetMemberSpecifier Map( Expression> valueFactoryExpression) { - return new CustomDataSourceTargetMemberSpecifier( - ConfigInfo.ForSourceValueType(), - valueFactoryExpression); + return GetValueFactoryTargetMemberSpecifier(valueFactoryExpression); } - public CustomDataSourceTargetMemberSpecifier MapFunc( + public ICustomMappingDataSourceTargetMemberSpecifier MapFunc( Func valueFunc) => GetConstantValueTargetMemberSpecifier(valueFunc); - public CustomDataSourceTargetMemberSpecifier Map(TSourceValue value) - { - var valueLambdaInfo = ConfiguredLambdaInfo.ForFunc(value, typeof(TSource), typeof(TTarget)); + public ICustomMappingDataSourceTargetMemberSpecifier Map(TSourceValue value) + => GetConstantValueTargetMemberSpecifier(value); - return (valueLambdaInfo != null) - ? new CustomDataSourceTargetMemberSpecifier( - ConfigInfo.ForSourceValueType(valueLambdaInfo.ReturnType), - valueLambdaInfo) - : GetConstantValueTargetMemberSpecifier(value); + ICustomProjectionDataSourceTargetMemberSpecifier IRootProjectionConfigurator.Map( + TSourceValue value) + { + return GetConstantValueTargetMemberSpecifier(value); } #region Map Helpers + private CustomDataSourceTargetMemberSpecifier GetValueFactoryTargetMemberSpecifier( + LambdaExpression valueFactoryExpression) + { + return new CustomDataSourceTargetMemberSpecifier( + ConfigInfo.ForSourceValueType(), + valueFactoryExpression); + } + private CustomDataSourceTargetMemberSpecifier GetConstantValueTargetMemberSpecifier( TSourceValue value) { + var valueLambdaInfo = ConfiguredLambdaInfo.ForFunc(value, typeof(TSource), typeof(TTarget)); + + if (valueLambdaInfo != null) + { + return new CustomDataSourceTargetMemberSpecifier( + ConfigInfo.ForSourceValueType(valueLambdaInfo.ReturnType), + valueLambdaInfo); + } + var valueConstant = value.ToConstantExpression(); var valueLambda = Expression.Lambda>(valueConstant); @@ -246,7 +375,7 @@ private CustomDataSourceTargetMemberSpecifier GetConstantValue #endregion - public MappingConfigContinuation MapTo() + public IMappingConfigContinuation MapTo() where TDerivedTarget : TTarget { var derivedTypePair = new DerivedPairTargetTypeSpecifier(ConfigInfo); @@ -254,7 +383,21 @@ public MappingConfigContinuation MapTo() return derivedTypePair.To(); } - public MappingConfigContinuation MapToNull() + IProjectionConfigContinuation IConditionalRootProjectionConfigurator.MapTo() + { + IProjectionDerivedPairTargetTypeSpecifier derivedTypePair = + new DerivedPairTargetTypeSpecifier(ConfigInfo); + + return derivedTypePair.To(); + } + + public IMappingConfigContinuation MapToNull() + => RegisterMapToNullCondition(); + + IProjectionConfigContinuation IConditionalRootProjectionConfigurator.MapToNull() + => RegisterMapToNullCondition(); + + private MappingConfigContinuation RegisterMapToNullCondition() { var condition = new MapToNullCondition(ConfigInfo); @@ -263,8 +406,11 @@ public MappingConfigContinuation MapToNull() return new MappingConfigContinuation(ConfigInfo); } - public DerivedPairTargetTypeSpecifier Map() where TDerivedSource : TSource - => new DerivedPairTargetTypeSpecifier(ConfigInfo); + public IMappingDerivedPairTargetTypeSpecifier Map() + where TDerivedSource : TSource + { + return new DerivedPairTargetTypeSpecifier(ConfigInfo); + } #endregion } diff --git a/AgileMapper/Api/Configuration/Projection/IConditionalProjectionConfigurator.cs b/AgileMapper/Api/Configuration/Projection/IConditionalProjectionConfigurator.cs new file mode 100644 index 000000000..83726461d --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/IConditionalProjectionConfigurator.cs @@ -0,0 +1,24 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + using System; + using System.Linq.Expressions; + + /// + /// Provides options for configuring a condition which must evaluate to true for the configuration to apply + /// to mappings from and to the source and result types being configured. + /// + /// The source type to which the configuration should apply. + /// The result type to which the configuration should apply. + public interface IConditionalProjectionConfigurator + : IRootProjectionConfigurator + { + /// + /// Configure a condition which must evaluate to true for the configuration to apply. The condition + /// expression is passed the source element being projected. + /// + /// The condition to evaluate. + /// An IConditionalRootProjectionConfigurator with which to complete the configuration. + IConditionalRootProjectionConfigurator If( + Expression> condition); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Projection/IConditionalRootProjectionConfigurator.cs b/AgileMapper/Api/Configuration/Projection/IConditionalRootProjectionConfigurator.cs new file mode 100644 index 000000000..7f3db44d4 --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/IConditionalRootProjectionConfigurator.cs @@ -0,0 +1,32 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + /// + /// Provides options for configuring a mapping based on the preceding condition. + /// + /// The source type to which the configuration should apply. + /// The result type to which the configuration should apply. + public interface IConditionalRootProjectionConfigurator : + IRootProjectionConfigurator + { + /// + /// Project the source Type being configured to the derived result type specified by + /// if the preceding condition evaluates to true. + /// + /// The derived result type to create. + /// + /// An IProjectionConfigContinuation to enable further configuration of mappings from and to the source + /// and result type being configured. + /// + IProjectionConfigContinuation MapTo() + where TDerivedResult : TResultElement; + + /// + /// Project the result Type being configured to null if the preceding condition evaluates to true. + /// + /// + /// An IProjectionConfigContinuation to enable further configuration of mappings from and to the + /// source and result Type being configured. + /// + IProjectionConfigContinuation MapToNull(); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Projection/ICustomProjectionDataSourceTargetMemberSpecifier.cs b/AgileMapper/Api/Configuration/Projection/ICustomProjectionDataSourceTargetMemberSpecifier.cs new file mode 100644 index 000000000..7edd754e1 --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/ICustomProjectionDataSourceTargetMemberSpecifier.cs @@ -0,0 +1,45 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + using System; + using System.Linq.Expressions; + + /// + /// Provides options for specifying a result member to which a configuration option should apply. + /// + /// The source type to which the configuration should apply. + /// The result type to which the configuration should apply. + public interface ICustomProjectionDataSourceTargetMemberSpecifier + { + /// + /// Apply the configuration to the given . + /// + /// The target member's type. + /// The result member to which to apply the configuration. + /// + /// An IProjectionConfigContinuation to enable further configuration of mappings from and to the + /// source and target type being configured. + /// + IProjectionConfigContinuation To( + Expression> resultMember); + + /// + /// Apply the configuration to the constructor parameter with the type specified by the type argument. + /// + /// The result constructor parameter's type. + /// + /// An IProjectionConfigContinuation to enable further configuration of mappings from and to the source + /// and result type being configured. + /// + IProjectionConfigContinuation ToCtor(); + + /// + /// Apply the configuration to the constructor parameter with the specified . + /// + /// The result constructor parameter's name. + /// + /// An IProjectionConfigContinuation to enable further configuration of mappings from and to the source and + /// result type being configured. + /// + IProjectionConfigContinuation ToCtor(string parameterName); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Projection/IFullProjectionConfigurator.cs b/AgileMapper/Api/Configuration/Projection/IFullProjectionConfigurator.cs new file mode 100644 index 000000000..ef7f4984a --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/IFullProjectionConfigurator.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + /// + /// Provides options for configuring query projections from and to given source and result element Types. + /// + /// The source element Type to which the configuration should apply. + /// The result element Type to which the configuration should apply. + public interface IFullProjectionConfigurator : + IFullProjectionSettings + { + } +} diff --git a/AgileMapper/Api/Configuration/Projection/IFullProjectionInlineConfigurator.cs b/AgileMapper/Api/Configuration/Projection/IFullProjectionInlineConfigurator.cs new file mode 100644 index 000000000..32ef53c1f --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/IFullProjectionInlineConfigurator.cs @@ -0,0 +1,109 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + /// + /// Provides options for configuring query projections from and to given source and result element Types, inline. + /// + /// The source element Type to which the configuration should apply. + /// The result element Type to which the configuration should apply. + public interface IFullProjectionInlineConfigurator : + IFullProjectionConfigurator + { + /// + /// Configure how this mapper performs a projection, inline. Use this property to switch from + /// configuration of the root Types on which the projection is being performed to configuration + /// of any other Types. + /// + IProjectionConfigStartingPoint WhenMapping { get; } + + /// + /// Throw an exception upon execution of this statement if the projection being configured has any result + /// members which will not be mapped, projects from a source enum to a target enum which does not support + /// all of its values, or includes complex Types which cannot be constructed. Use calls to this method to + /// validate a mapping plan; remove them in production + /// code. + /// + void ThrowNowIfMappingPlanIsIncomplete(); + + #region Naming + + /// + /// Expect members of the source and result element Types being projected to potentially have the given + /// name . Source and result element members will be matched as if the prefix + /// is absent. + /// + /// The prefix to ignore when matching source and result element members. + /// + /// This with which to + /// configure further settings for the source and result element Types being mapped. + /// + IFullProjectionInlineConfigurator UseNamePrefix(string prefix); + + /// + /// Expect members of the source and result element Types being mapped to potentially have any of the given + /// name . Source and result element members will be matched as if the prefixes + /// are absent. + /// + /// The prefixes to ignore when matching source and result element members. + /// + /// This with which to + /// configure further settings for the source and result element Types being mapped. + /// + IFullProjectionInlineConfigurator UseNamePrefixes(params string[] prefixes); + + /// + /// Expect members of the source and result element Types being mapped to potentially have the given name + /// . Source and target members will be matched as if the suffix is absent. + /// + /// The suffix to ignore when matching source and result element members. + /// + /// This with which to + /// configure further settings for the source and result element Types being mapped. + /// + IFullProjectionInlineConfigurator UseNameSuffix(string suffix); + + /// + /// Expect members of the source and result element Types being mapped to potentially have any of the given name + /// . Source and target members will be matched as if the suffixes are absent. + /// + /// The suffixes to ignore when matching source and result element members. + /// + /// This with which to + /// configure further settings for the source and result element Types being mapped. + /// + IFullProjectionInlineConfigurator UseNameSuffixes(params string[] suffixes); + + /// + /// Expect members of the source and result element Types being mapped to potentially match the given name + /// . The pattern will be used to find the part of a name which should be used + /// to match a source and result element member. + /// + /// + /// The Regex pattern to check against source and result element member names. The pattern is expected to + /// start with the ^ character, end with the $ character and contain a single capturing group wrapped in + /// parentheses, e.g. ^__(.+)__$ + /// + /// + /// This with which to + /// configure further settings for the source and result element Types being mapped. + /// + IFullProjectionInlineConfigurator UseNamePattern(string pattern); + + /// + /// Expect members of the source and result element Types being mapped to potentially match the given name + /// . The patterns will be used to find the part of a name which should be used + /// to match a source and result element member. + /// + /// + /// The Regex patterns to check against source and result element member names. Each pattern is expected to + /// start with the ^ character, end with the $ character and contain a single capturing group wrapped in + /// parentheses, e.g. ^__(.+)__$ + /// + /// + /// This with which to + /// configure further settings for the source and result element Types being mapped. + /// + IFullProjectionInlineConfigurator UseNamePatterns(params string[] patterns); + + #endregion + } +} diff --git a/AgileMapper/Api/Configuration/Projection/IFullProjectionSettings.cs b/AgileMapper/Api/Configuration/Projection/IFullProjectionSettings.cs new file mode 100644 index 000000000..d9b8a95c3 --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/IFullProjectionSettings.cs @@ -0,0 +1,41 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + /// + /// Provides options for configuring settings for mappings from and to a given source and result type. + /// + /// The source type to which the configured settings should apply. + /// The result type to which the configured settings should apply. + public interface IFullProjectionSettings + : IConditionalProjectionConfigurator + { + /// + /// Project recursive relationships to the specified . + /// For example, when projecting a Category entity which has a SubCategories property of Type + /// IEnumerable{Category}, a recursion depth of 1 will populate the sub-categories of the sub-categories + /// of the top-level Category selected; a recursion depth of 2 will populate the sub-categories of the + /// sub-categories of the sub-categories of the top-level Category selected, etc. The default is zero, + /// which only populates the first level of sub-categories. + /// + /// The depth to which to populate projected recursive relationships. + IFullProjectionInlineConfigurator RecurseToDepth(int recursionDepth); + + /// + /// Configure this mapper to pair the given with a member of another + /// enum Type. + /// + /// The type of the enum member to pair. + /// The first enum member in the pair. + /// + /// An IProjectionEnumPairSpecifier with which to specify the enum member to which the given + /// should be paired. + /// + IProjectionEnumPairSpecifier PairEnum(TPairingEnum enumMember) + where TPairingEnum : struct; + + /// + /// Gets a link back to the full , for + /// api fluency. + /// + IFullProjectionConfigurator And { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Projection/IGlobalProjectionSettings.cs b/AgileMapper/Api/Configuration/Projection/IGlobalProjectionSettings.cs new file mode 100644 index 000000000..b8a712068 --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/IGlobalProjectionSettings.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + /// + /// Provides options for globally configuring how all mappers will perform query projections. + /// + public interface IGlobalProjectionSettings + { + /// + /// Gets a link back to the full , for api fluency. + /// + IProjectionConfigStartingPoint AndWhenMapping { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Projection/IProjectionConfigContinuation.cs b/AgileMapper/Api/Configuration/Projection/IProjectionConfigContinuation.cs new file mode 100644 index 000000000..2802de537 --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/IProjectionConfigContinuation.cs @@ -0,0 +1,22 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + /// + /// Enables chaining of configurations for the same source and result type. + /// + /// The source type to which the configuration should apply. + /// The result type to which the configuration should apply. + public interface IProjectionConfigContinuation + { + /// + /// Perform another configuration of how this mapper projects to and from the source and result types + /// being configured. This property exists purely to provide a more fluent configuration interface. + /// + IFullProjectionConfigurator And { get; } + + /// + /// Perform an alternative configuration of how this mapper projects to and from the source and result + /// types being configured. This property exists purely to provide a more fluent configuration interface. + /// + IFullProjectionConfigurator But { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Projection/IProjectionConfigStartingPoint.cs b/AgileMapper/Api/Configuration/Projection/IProjectionConfigStartingPoint.cs new file mode 100644 index 000000000..44f8f8540 --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/IProjectionConfigStartingPoint.cs @@ -0,0 +1,42 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + using System; + using AgileMapper.Configuration; + + /// + /// Provides options for configuring how a mapper performs a query projection. + /// + public interface IProjectionConfigStartingPoint + { + /// + /// Configure a formatting string to use when projecting from the given + /// to strings, for all source and result Types. The configured formatting string will have to be supported + /// by the QueryProvider, and may be ignored if it is not. + /// + /// The source value type to which to apply a formatting string. + /// An action which supplies the formatting string. + /// + /// An with which to globally configure other query projection + /// aspects. + /// + IGlobalProjectionSettings StringsFrom(Action formatSelector); + + /// + /// Configure how this mapper performs projections from the Type. + /// + /// The source Type to which the configuration will apply. + /// + /// An IProjectionResultSelector with which to specify to which result Type the configuration + /// will apply. + /// + IProjectionResultSelector From() where TSource : class; + + /// + /// Configure how this mapper performs query projection mappings from any source type to the + /// Type. + /// + /// The result Type to which the configuration will apply. + /// An IFullProjectionConfigurator with which to complete the configuration. + IFullProjectionConfigurator ProjectionsTo(); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Projection/IProjectionDerivedPairTargetTypeSpecifier.cs b/AgileMapper/Api/Configuration/Projection/IProjectionDerivedPairTargetTypeSpecifier.cs new file mode 100644 index 000000000..0d30ab211 --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/IProjectionDerivedPairTargetTypeSpecifier.cs @@ -0,0 +1,27 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + /// + /// Enables the selection of a derived result type to which to match a configured source type. + /// + /// + /// The type of source object for which the derived type pair is being configured. + /// + /// + /// The type of result object for which the derived type pair is being configured. + /// + public interface IProjectionDerivedPairTargetTypeSpecifier + { + /// + /// Map the derived source type being configured to the derived result type specified by the type argument. + /// + /// + /// The derived result type to create for the configured derived source type. + /// + /// + /// An IProjectionConfigContinuation to enable further configuration of mappings from and to the source and + /// result type being configured. + /// + IProjectionConfigContinuation To() + where TDerivedResult : TResultElement; + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Projection/IProjectionEnumPairSpecifier.cs b/AgileMapper/Api/Configuration/Projection/IProjectionEnumPairSpecifier.cs new file mode 100644 index 000000000..5b6e21834 --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/IProjectionEnumPairSpecifier.cs @@ -0,0 +1,23 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + /// + /// Provides options for specifying the enum member(s) with which the previously-specified enum member(s) + /// should be paired. + /// + /// The source type being configured. + /// The target type being configured. + public interface IProjectionEnumPairSpecifier + { + /// + /// Configure this mapper to project the previously-specified enum member(s) to the given + /// . + /// + /// The type of enum the members of which are being paired. + /// + /// One or more enum members to pair to the previously-specified enum members. + /// + /// An IProjectionConfigContinuation with which to configure other aspects of mapping. + IProjectionConfigContinuation With(params TPairedEnum[] pairedEnumMembers) + where TPairedEnum : struct; + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Projection/IProjectionFactorySpecifier.cs b/AgileMapper/Api/Configuration/Projection/IProjectionFactorySpecifier.cs new file mode 100644 index 000000000..ae62a0335 --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/IProjectionFactorySpecifier.cs @@ -0,0 +1,30 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + using System; + using System.Linq.Expressions; + + /// + /// Provides an option for configuring a custom factory Expression with which to create instances of the + /// Type, when projecting from and to the source and result types being + /// configured. + /// + /// The source Type to which the configuration should apply. + /// The result Type to which the configuration should apply. + /// The Type of object which will be created by the configured factory. + public interface IProjectionFactorySpecifier + { + /// + /// Use the given expression to create instances of the object type being + /// configured. The factory expression is passed the source element being projected, and must be + /// translatable by the QueryProvider being used. + /// + /// + /// The factory expression to use to create instances of the type being configured. + /// + /// + /// An IProjectionConfigContinuation to enable further configuration of projections from and to the + /// source and result Type being configured. + /// + IProjectionConfigContinuation Using(Expression> factory); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Projection/IProjectionResultSelector.cs b/AgileMapper/Api/Configuration/Projection/IProjectionResultSelector.cs new file mode 100644 index 000000000..3181e6136 --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/IProjectionResultSelector.cs @@ -0,0 +1,18 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + /// + /// Provides options for specifying the target type and mapping rule set to which the configuration should + /// apply. + /// + /// The source type being configured. + public interface IProjectionResultSelector + { + /// + /// Configure how this mapper performs query projections from the source Type being configured to the + /// result Type specified by the given argument. + /// + /// The result Type to which the configuration will apply. + /// An IFullProjectionConfigurator with which to complete the configuration. + IFullProjectionConfigurator ProjectedTo(); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/Projection/IRootProjectionConfigurator.cs b/AgileMapper/Api/Configuration/Projection/IRootProjectionConfigurator.cs new file mode 100644 index 000000000..b143f6033 --- /dev/null +++ b/AgileMapper/Api/Configuration/Projection/IRootProjectionConfigurator.cs @@ -0,0 +1,100 @@ +namespace AgileObjects.AgileMapper.Api.Configuration.Projection +{ + using System; + using System.Linq.Expressions; + using AgileMapper.Configuration; + + /// + /// Provides options for configuring projections from and to a given source and result type. + /// + /// The source type to which the configuration should apply. + /// The result type to which the configuration should apply. + public interface IRootProjectionConfigurator + { + /// + /// Use the given expression to create instances of the result type being + /// configured. The factory expression is passed the source element being projected, and must be + /// translatable by the QueryProvider being used. + /// + /// + /// The factory expression to use to create instances of the Type being configured. + /// + /// + /// An IProjectionConfigContinuation to enable further configuration of projections from and to the + /// source and result Type being configured. + /// + IProjectionConfigContinuation CreateInstancesUsing( + Expression> factory); + + /// + /// Configure a factory to use to create instances of the Type. + /// + /// The Type of object the creation of which is to be configured. + /// + /// An IProjectionFactorySpecifier with which to configure the factory for the + /// Type. + /// + IProjectionFactorySpecifier CreateInstancesOf(); + + /// + /// Ignore the specified when projecting from and to the source and + /// result types being configured. + /// + /// The result member(s) which should be ignored. + /// + /// An IProjectionConfigContinuation to enable further configuration of mappings from and to the source + /// and result type being configured. + /// + IProjectionConfigContinuation Ignore( + params Expression>[] resultMembers); + + /// + /// Ignore all result member(s) of the given Type when projecting + /// from and to the source and result types being configured. + /// + /// The Type of result member to ignore. + /// + /// An IProjectionConfigContinuation to enable further configuration of projections from and to the source and + /// result types being configured. + /// + IProjectionConfigContinuation IgnoreTargetMembersOfType(); + + /// + /// Ignore all result member(s) matching the given when projecting + /// from and to the source and result types being configured. + /// + /// The matching function with which to select result members to ignore. + /// + /// An IProjectionConfigContinuation to enable further configuration of mappings from and to the source and + /// result types being configured. + /// + IProjectionConfigContinuation IgnoreTargetMembersWhere( + Expression> memberFilter); + + /// + /// Configure a custom data source for a particular result member when mapping from and to the source and + /// result types being configured. The factory expression is passed the source element being projected. + /// + /// The type of the custom value being configured. + /// The expression to map to the configured result member. + /// + /// A CustomDataSourceTargetMemberSpecifier with which to specify the result member to which the custom + /// value should be applied. + /// + ICustomProjectionDataSourceTargetMemberSpecifier Map( + Expression> valueFactoryExpression); + + /// + /// Configure a constant value for a particular result member when projecting from and to the source and + /// result types being configured. + /// + /// The type of the custom constant value being configured. + /// The constant value to map to the configured result member. + /// + /// A CustomDataSourceTargetMemberSpecifier with which to specify the result member to which the custom + /// constant value should be applied. + /// + ICustomProjectionDataSourceTargetMemberSpecifier Map( + TSourceValue value); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/Configuration/TargetSpecifier.cs b/AgileMapper/Api/Configuration/TargetSpecifier.cs index 6ed561cd3..8a396b79f 100644 --- a/AgileMapper/Api/Configuration/TargetSpecifier.cs +++ b/AgileMapper/Api/Configuration/TargetSpecifier.cs @@ -1,17 +1,16 @@ namespace AgileObjects.AgileMapper.Api.Configuration { - using System.Collections.Generic; using AgileMapper.Configuration; - using AgileMapper.Configuration.Dictionaries; using Dictionaries; using Dynamics; + using Projection; /// /// Provides options for specifying the target type and mapping rule set to which the configuration should /// apply. /// /// The source type being configured. - public class TargetSpecifier + public class TargetSpecifier : IProjectionResultSelector { private readonly MappingConfigInfo _configInfo; @@ -22,7 +21,8 @@ internal TargetSpecifier(MappingConfigInfo configInfo) /// /// Configure how this mapper performs mappings from the source type being configured in all MappingRuleSets - /// (create new, overwrite, etc), to the target type specified by the type argument. + /// (create new, overwrite, etc), to the target type specified by the given + /// argument. /// /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. @@ -30,17 +30,18 @@ public IFullMappingConfigurator To() => new MappingConfigurator(_configInfo.ForAllRuleSets()); /// - /// Configure how this mapper performs mappings from the source type being configured to the target - /// type specified by the type argument when mapping to new objects. + /// Configure how this mapper performs mappings from the source type being configured to the result + /// type specified by the given argument when mapping to new objects. /// - /// The target type to which the configuration will apply. + /// The result type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. - public IFullMappingConfigurator ToANew() - => UsingRuleSet(Constants.CreateNew); + public IFullMappingConfigurator ToANew() + => UsingRuleSet(Constants.CreateNew); /// /// Configure how this mapper performs mappings from the source type being configured to the target - /// type specified by the type argument when performing OnTo (merge) mappings. + /// type specified by the given argument when performing OnTo (merge) + /// mappings. /// /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. @@ -49,13 +50,23 @@ public IFullMappingConfigurator OnTo() /// /// Configure how this mapper performs mappings from the source type being configured to the target - /// type specified by the type argument when performing Over (overwrite) mappings. + /// type specified by the given argument when performing Over (overwrite) + /// mappings. /// /// The target type to which the configuration will apply. /// An IFullMappingConfigurator with which to complete the configuration. public IFullMappingConfigurator Over() => UsingRuleSet(Constants.Overwrite); + /// + /// Configure how this mapper performs query projections from the source type being configured to the + /// result type specified by the given argument. + /// + /// The result type to which the configuration will apply. + /// An IFullProjectionConfigurator with which to complete the configuration. + public IFullProjectionConfigurator ProjectedTo() + => UsingRuleSet(Constants.Project); + private MappingConfigurator UsingRuleSet(string name) => new MappingConfigurator(_configInfo.ForRuleSet(name)); diff --git a/AgileMapper/Api/IProjectionResultSpecifier.cs b/AgileMapper/Api/IProjectionResultSpecifier.cs new file mode 100644 index 000000000..f2dde5097 --- /dev/null +++ b/AgileMapper/Api/IProjectionResultSpecifier.cs @@ -0,0 +1,52 @@ +namespace AgileObjects.AgileMapper.Api +{ + using System; + using System.Linq; + using System.Linq.Expressions; + using Configuration.Projection; + + /// + /// Provides options for specifying the query projection result Type. + /// + /// + /// The Type of object contained in the source IQueryable{T} which should be projected + /// to a result Type. + /// + public interface IProjectionResultSpecifier + { + /// + /// Project the elements of the source IQueryable{T} to instances of the given + /// , using the default mapper. + /// + /// + /// The result Type to which the elements of the source IQueryable{T} should be projected. + /// + /// + /// An IQueryable{TResultElement} of the source IQueryable{T} projected to instances of the given + /// . The projection is not performed until the Queryable is + /// enumerated by a call to .ToArray() or similar. + /// + IQueryable To(); + + /// + /// Project the elements of the source IQueryable{T} to instances of the given + /// , using the default mapper and the given + /// . + /// + /// + /// The result Type to which the elements of the source IQueryable{T} should be projected. + /// + /// + /// An inline query projection configuration. If non-null, the query projection will be configured + /// by combining this inline with any applicable configuration + /// already set up via the Mapper.WhenMapping API. + /// + /// + /// An IQueryable{TResultElement} of the source IQueryable{T} projected to instances of the given + /// . The projection is not performed until the Queryable is + /// enumerated by a call to .ToArray() or similar. + /// + IQueryable To( + Expression>> configuration); + } +} \ No newline at end of file diff --git a/AgileMapper/Api/PlanTargetSelector.cs b/AgileMapper/Api/PlanTargetSelector.cs index 7beb75f33..624ab53b1 100644 --- a/AgileMapper/Api/PlanTargetSelector.cs +++ b/AgileMapper/Api/PlanTargetSelector.cs @@ -6,14 +6,24 @@ using System.Linq.Expressions; using AgileMapper.Configuration.Inline; using Configuration; + using ObjectPopulation; using Extensions.Internal; using Plans; + using Queryables.Api; internal class PlanTargetSelector : IPlanTargetSelector, - IPlanTargetAndRuleSetSelector + IPlanTargetAndRuleSetSelector, + IProjectionPlanTargetSelector { private readonly MapperContext _mapperContext; + private readonly IQueryable _exampleQueryable; + + public PlanTargetSelector(MapperContext mapperContext, IQueryable exampleQueryable) + : this(mapperContext) + { + _exampleQueryable = exampleQueryable; + } internal PlanTargetSelector(MapperContext mapperContext) { @@ -25,10 +35,12 @@ internal PlanTargetSelector(MapperContext mapperContext) public MappingPlanSet To( Expression>>[] configurations) { + // TODO: Include projection mapping plans: return new MappingPlanSet( _mapperContext .RuleSets .All + .Except(new[] { _mapperContext.RuleSets.Project }) .Select(rs => GetMappingPlan(rs, configurations)) .ToArray()); } @@ -45,19 +57,42 @@ public MappingPlan Over( Expression>>[] configurations) => GetMappingPlan(_mapperContext.RuleSets.Overwrite, configurations); + MappingPlan IProjectionPlanTargetSelector.To() + { + return GetMappingPlan( + _mapperContext.QueryProjectionMappingContext, + planContext => ObjectMappingDataFactory.ForProjection(_exampleQueryable, planContext)); + } + private MappingPlan GetMappingPlan( MappingRuleSet ruleSet, ICollection>>> configurations) { - var planContext = new SimpleMappingContext(ruleSet, _mapperContext); + var planContext = new SimpleMappingContext(ruleSet, _mapperContext) + { + AddUnsuccessfulMemberPopulations = true + }; + + return GetMappingPlan( + planContext, + ObjectMappingDataFactory.ForRootFixedTypes, + configurations); + } + private static MappingPlan GetMappingPlan( + IMappingContext planContext, + Func mappingDataFactory, + ICollection>>> configurations = null) + { if (configurations?.Any() == true) { InlineMappingConfigurator .ConfigureMapperContext(configurations, planContext); } - return MappingPlan.For(planContext); + var mappingData = mappingDataFactory.Invoke(planContext); + + return MappingPlan.For(mappingData); } } } \ No newline at end of file diff --git a/AgileMapper/Configuration/ConfiguredLambdaInfo.cs b/AgileMapper/Configuration/ConfiguredLambdaInfo.cs index 0db220cdb..a174fcfdb 100644 --- a/AgileMapper/Configuration/ConfiguredLambdaInfo.cs +++ b/AgileMapper/Configuration/ConfiguredLambdaInfo.cs @@ -11,14 +11,17 @@ internal class ConfiguredLambdaInfo { private readonly LambdaExpression _lambda; + private readonly Type[] _contextTypes; private readonly ParametersSwapper _parametersSwapper; private ConfiguredLambdaInfo( LambdaExpression lambda, + Type[] contextTypes, Type returnType, ParametersSwapper parametersSwapper) { _lambda = lambda; + _contextTypes = contextTypes; _parametersSwapper = parametersSwapper; ReturnType = returnType; } @@ -28,10 +31,25 @@ private ConfiguredLambdaInfo( public static ConfiguredLambdaInfo For(LambdaExpression lambda) { var funcArguments = lambda.Parameters.Select(p => p.Type).ToArray(); - var contextTypes = (funcArguments.Length != 1) ? funcArguments : funcArguments[0].GetGenericTypeArguments(); + var contextTypes = GetContextTypes(funcArguments); var parameterSwapper = ParametersSwapper.For(contextTypes, funcArguments); - return new ConfiguredLambdaInfo(lambda, lambda.ReturnType, parameterSwapper); + return new ConfiguredLambdaInfo(lambda, contextTypes, lambda.ReturnType, parameterSwapper); + } + + private static Type[] GetContextTypes(Type[] funcArguments) + { + if (funcArguments.Length != 1) + { + return funcArguments; + } + + if (funcArguments[0].IsGenericType()) + { + return funcArguments[0].GetGenericTypeArguments(); + } + + return new[] { funcArguments[0] }; } public static ConfiguredLambdaInfo ForFunc(TFunc func, params Type[] argumentTypes) @@ -97,13 +115,14 @@ private static ConfiguredLambdaInfo For( return new ConfiguredLambdaInfo( valueFactoryLambda, + contextTypes, returnTypeFactory.Invoke(funcTypes), parameterSwapper); } #endregion - public bool UsesMappingDataObjectParameter => _parametersSwapper.NumberOfParameters == 1; + public bool UsesMappingDataObjectParameter => _parametersSwapper.HasMappingContextParameter; public Type ReturnType { get; } @@ -120,7 +139,7 @@ public bool IsSameAs(ConfiguredLambdaInfo otherLambdaInfo) return false; } - return ExpressionEquator.Instance.Equals(_lambda.Body, otherLambdaInfo._lambda.Body); + return ExpressionEvaluation.AreEquivalent(_lambda.Body, otherLambdaInfo._lambda.Body); } public Expression GetBody( @@ -129,8 +148,8 @@ public Expression GetBody( QualifiedMember targetMember = null) { return position.IsPriorToObjectCreation(targetMember) - ? _parametersSwapper.Swap(_lambda, mapperData, ParametersSwapper.UseTargetMember) - : _parametersSwapper.Swap(_lambda, mapperData, ParametersSwapper.UseTargetInstance); + ? _parametersSwapper.Swap(_lambda, _contextTypes, mapperData, ParametersSwapper.UseTargetMember) + : _parametersSwapper.Swap(_lambda, _contextTypes, mapperData, ParametersSwapper.UseTargetInstance); } } } \ No newline at end of file diff --git a/AgileMapper/Configuration/Inline/IInlineMapperKey.cs b/AgileMapper/Configuration/Inline/IInlineMapperKey.cs index 49135fcde..46319ecb7 100644 --- a/AgileMapper/Configuration/Inline/IInlineMapperKey.cs +++ b/AgileMapper/Configuration/Inline/IInlineMapperKey.cs @@ -1,5 +1,6 @@ namespace AgileObjects.AgileMapper.Configuration.Inline { + using System; using System.Collections.Generic; using System.Linq.Expressions; using Members; @@ -10,6 +11,8 @@ internal interface IInlineMapperKey MappingRuleSet RuleSet { get; } + Type ConfiguratorType { get; } + IList Configurations { get; } MapperContext CreateInlineMapperContext(); diff --git a/AgileMapper/Configuration/Inline/InlineMapperContextSet.cs b/AgileMapper/Configuration/Inline/InlineMapperContextSet.cs index c2a578a0f..060cc666a 100644 --- a/AgileMapper/Configuration/Inline/InlineMapperContextSet.cs +++ b/AgileMapper/Configuration/Inline/InlineMapperContextSet.cs @@ -5,22 +5,55 @@ using System.Collections.Generic; using System.Linq.Expressions; using Api.Configuration; + using Api.Configuration.Projection; using Caching; internal class InlineMapperContextSet : IEnumerable { private readonly ICache _inlineContextsCache; + private readonly IMappingContext _queryProjectionMappingContext; public InlineMapperContextSet(MapperContext parentMapperContext) { _inlineContextsCache = parentMapperContext.Cache.CreateScoped(); + _queryProjectionMappingContext = parentMapperContext.QueryProjectionMappingContext; } public MapperContext GetContextFor( Expression>>[] configurations, MappingExecutor executor) { - var key = new InlineMapperKey(configurations, executor); + return GetContextFor>( + configurations, + CreateMappingConfigurator, + executor); + } + + public MapperContext GetContextFor( + Expression>>[] configurations, + ProjectionExecutor executor) + { + return GetContextFor>( + configurations, + CreateMappingConfigurator, + _queryProjectionMappingContext); + } + + private static MappingConfigurator CreateMappingConfigurator( + MappingConfigInfo configInfo) + { + return new MappingConfigurator(configInfo); + } + + private MapperContext GetContextFor( + Expression>[] configurations, + Func configuratorFactory, + IMappingContext mappingContext) + { + var key = new InlineMapperKey( + configurations, + configuratorFactory, + mappingContext); var inlineMapperContext = _inlineContextsCache.GetOrAdd( key, diff --git a/AgileMapper/Configuration/Inline/InlineMapperKey.cs b/AgileMapper/Configuration/Inline/InlineMapperKey.cs index 1208280f7..c77b9b502 100644 --- a/AgileMapper/Configuration/Inline/InlineMapperKey.cs +++ b/AgileMapper/Configuration/Inline/InlineMapperKey.cs @@ -3,26 +3,30 @@ namespace AgileObjects.AgileMapper.Configuration.Inline using System; using System.Collections.Generic; using System.Linq.Expressions; - using Api.Configuration; using Extensions.Internal; using Members; - internal class InlineMapperKey : IInlineMapperKey + internal class InlineMapperKey : IInlineMapperKey { - private readonly Expression>>[] _configurations; - private readonly MappingExecutor _executor; + private readonly Expression>[] _configurations; + private readonly Func _configuratorFactory; + private readonly IMappingContext _mappingContext; public InlineMapperKey( - Expression>>[] configurations, - MappingExecutor executor) + Expression>[] configurations, + Func configuratorFactory, + IMappingContext mappingContext) { _configurations = configurations; - _executor = executor; + _configuratorFactory = configuratorFactory; + _mappingContext = mappingContext; } public MappingTypes MappingTypes => MappingTypes.Fixed; - public MappingRuleSet RuleSet => _executor.RuleSet; + public MappingRuleSet RuleSet => _mappingContext.RuleSet; + + public Type ConfiguratorType => typeof(TConfigurator); // ReSharper disable once CoVariantArrayConversion public IList Configurations => _configurations; @@ -30,7 +34,10 @@ public InlineMapperKey( public MapperContext CreateInlineMapperContext() { return InlineMappingConfigurator - .ConfigureInlineMapperContext(_configurations, _executor); + .ConfigureInlineMapperContext( + _configurations, + _configuratorFactory, + _mappingContext); } public override bool Equals(object obj) @@ -39,6 +46,7 @@ public override bool Equals(object obj) // ReSharper disable once PossibleNullReferenceException if ((_configurations.Length != otherKey.Configurations.Count) || + (ConfiguratorType != otherKey.ConfiguratorType) || (RuleSet != otherKey.RuleSet) || !MappingTypes.Equals(otherKey.MappingTypes)) { @@ -50,7 +58,7 @@ public override bool Equals(object obj) var configuration = Configurations[i].Body; var otherConfiguration = otherKey.Configurations[i].Body; - if (!ExpressionEquator.Instance.Equals(configuration, otherConfiguration)) + if (!ExpressionEvaluation.AreEquivalent(configuration, otherConfiguration)) { return false; } diff --git a/AgileMapper/Configuration/Inline/InlineMappingConfigurator.cs b/AgileMapper/Configuration/Inline/InlineMappingConfigurator.cs index 103a2f6d4..7da8de485 100644 --- a/AgileMapper/Configuration/Inline/InlineMappingConfigurator.cs +++ b/AgileMapper/Configuration/Inline/InlineMappingConfigurator.cs @@ -7,24 +7,34 @@ namespace AgileObjects.AgileMapper.Configuration.Inline internal static class InlineMappingConfigurator { - public static MapperContext ConfigureInlineMapperContext( - IEnumerable>>> configurations, + public static MapperContext ConfigureInlineMapperContext( + IEnumerable>> configurations, + Func configuratorFactory, IMappingContext mappingContext) { var inlineMapperContext = mappingContext.MapperContext.Clone(); - return ConfigureMapperContext(configurations, mappingContext.RuleSet, inlineMapperContext); + return ConfigureMapperContext( + configurations, + configuratorFactory, + mappingContext.RuleSet, + inlineMapperContext); } public static MapperContext ConfigureMapperContext( IEnumerable>>> configurations, IMappingContext mappingContext) { - return ConfigureMapperContext(configurations, mappingContext.RuleSet, mappingContext.MapperContext); + return ConfigureMapperContext( + configurations, + configInfo => new MappingConfigurator(configInfo), + mappingContext.RuleSet, + mappingContext.MapperContext); } - private static MapperContext ConfigureMapperContext( - IEnumerable>>> configurations, + private static MapperContext ConfigureMapperContext( + IEnumerable>> configurations, + Func configuratorFactory, MappingRuleSet ruleSet, MapperContext mapperContext) { @@ -33,7 +43,7 @@ private static MapperContext ConfigureMapperContext( .ForSourceType() .ForTargetType(); - var configurator = new MappingConfigurator(configInfo); + var configurator = configuratorFactory.Invoke(configInfo); foreach (var configuration in configurations) { diff --git a/AgileMapper/Configuration/MappingConfigInfo.cs b/AgileMapper/Configuration/MappingConfigInfo.cs index 7cdaf885f..c449750f5 100644 --- a/AgileMapper/Configuration/MappingConfigInfo.cs +++ b/AgileMapper/Configuration/MappingConfigInfo.cs @@ -4,14 +4,13 @@ using System.Collections.Generic; using System.Globalization; using System.Linq.Expressions; - using Extensions.Internal; using Members; using ObjectPopulation; using ReadableExpressions; internal class MappingConfigInfo : ITypePair { - private static readonly MappingRuleSet _allRuleSets = new MappingRuleSet("*", true, null, null, null); + private static readonly MappingRuleSet _allRuleSets = new MappingRuleSet("*", null, null, null, null, null); private ConfiguredLambdaInfo _conditionLambda; private bool _negateCondition; @@ -161,8 +160,7 @@ public Expression GetConditionOrNull( var conditionNestedAccessesChecks = mapperData .GetExpressionInfoFor(condition, targetCanBeNull) - .NestedAccesses - .GetIsNotDefaultComparisonsOrNull(); + .NestedAccessChecks; if (conditionNestedAccessesChecks != null) { diff --git a/AgileMapper/Configuration/ParametersSwapper.cs b/AgileMapper/Configuration/ParametersSwapper.cs index f2104f331..c6a734cab 100644 --- a/AgileMapper/Configuration/ParametersSwapper.cs +++ b/AgileMapper/Configuration/ParametersSwapper.cs @@ -17,7 +17,8 @@ internal class ParametersSwapper private static readonly ParametersSwapper[] _implementations = { new ParametersSwapper(0, (ct, ft) => true, SwapNothing), - new ParametersSwapper(1, IsContext, SwapForContextParameter), + new ParametersSwapper(1, IsContext, SwapForContextParameter, true), + new ParametersSwapper(1, IsSourceOnly, SwapForSource), new ParametersSwapper(2, IsSourceAndTarget, SwapForSourceAndTarget), new ParametersSwapper(3, IsSourceTargetAndIndex, SwapForSourceTargetAndIndex), new ParametersSwapper(3, IsSourceTargetAndCreatedObject, SwapForSourceTargetAndCreatedObject), @@ -53,8 +54,14 @@ private static bool Is( return parametersChecker.Invoke(contextTypes, contextTypeArgument.GetGenericTypeArguments()); } + private static bool IsSourceOnly(Type[] contextTypes, Type[] funcArguments) + => (contextTypes.Length == 1) && IsSource(contextTypes, funcArguments); + + private static bool IsSource(IList contextTypes, IList funcArguments) + => contextTypes[0].IsAssignableTo(funcArguments[0]); + private static bool IsSourceAndTarget(Type[] contextTypes, Type[] funcArguments) - => contextTypes[0].IsAssignableTo(funcArguments[0]) && contextTypes[1].IsAssignableTo(funcArguments[1]); + => IsSource(contextTypes, funcArguments) && contextTypes[1].IsAssignableTo(funcArguments[1]); private static bool IsSourceTargetAndIndex(Type[] contextTypes, Type[] funcArguments) => IsSourceAndTarget(contextTypes, funcArguments) && IsIndex(funcArguments); @@ -82,7 +89,7 @@ private static Expression SwapForContextParameter(SwapArgs swapArgs) } var contextTypes = contextType.GetGenericTypeArguments(); - var contextInfo = GetAppropriateMappingContext(contextTypes, swapArgs); + var contextInfo = GetAppropriateMappingContext(swapArgs); if (swapArgs.Lambda.Body.NodeType == ExpressionType.Invoke) { @@ -132,6 +139,9 @@ private static Expression GetInvocationContextArgument(MappingContextInfo contex return lambda.ReplaceParameterWith(createObjectCreationContextCall); } + private static Expression SwapForSource(SwapArgs swapArgs) => + ReplaceParameters(swapArgs, c => c.SourceAccess); + private static Expression SwapForSourceAndTarget(SwapArgs swapArgs) => ReplaceParameters(swapArgs, c => c.SourceAccess, c => c.TargetAccess); @@ -148,49 +158,50 @@ private static Expression ReplaceParameters( SwapArgs swapArgs, params Func[] parameterFactories) { - var contextInfo = GetAppropriateMappingContext( - swapArgs.Lambda.Parameters.Select(p => p.Type).ToArray(), - swapArgs); + var contextInfo = GetAppropriateMappingContext(swapArgs); return swapArgs.Lambda.ReplaceParametersWith(parameterFactories.Select(f => f.Invoke(contextInfo)).ToArray()); } - private static MappingContextInfo GetAppropriateMappingContext(Type[] contextTypes, SwapArgs swapArgs) + private static MappingContextInfo GetAppropriateMappingContext(SwapArgs swapArgs) { - if (swapArgs.MapperData.TypesMatch(contextTypes)) + if (swapArgs.ContextTypesMatch()) { - return new MappingContextInfo(swapArgs, contextTypes); + return new MappingContextInfo(swapArgs); } - var dataAccess = swapArgs.MapperData.GetAppropriateMappingContextAccess(contextTypes); + var dataAccess = swapArgs.GetAppropriateMappingContextAccess(); - return new MappingContextInfo(swapArgs, dataAccess, contextTypes); + return new MappingContextInfo(swapArgs, dataAccess); } #endregion #endregion + private readonly int _numberOfParameters; private readonly Func _applicabilityPredicate; private readonly Func _parametersSwapper; private ParametersSwapper( int numberOfParameters, Func applicabilityPredicate, - Func parametersSwapper) + Func parametersSwapper, + bool hasMappingContextParameter = false) { - NumberOfParameters = numberOfParameters; + _numberOfParameters = numberOfParameters; _applicabilityPredicate = applicabilityPredicate; _parametersSwapper = parametersSwapper; + HasMappingContextParameter = hasMappingContextParameter; } public static ParametersSwapper For(Type[] contextTypes, Type[] funcArguments) => _implementations.FirstOrDefault(pso => pso.AppliesTo(contextTypes, funcArguments)); - public int NumberOfParameters { get; } + public bool HasMappingContextParameter { get; } public bool AppliesTo(Type[] contextTypes, Type[] funcArguments) - => (funcArguments.Length == NumberOfParameters) && _applicabilityPredicate.Invoke(contextTypes, funcArguments); + => (funcArguments.Length == _numberOfParameters) && _applicabilityPredicate.Invoke(contextTypes, funcArguments); public static Expression UseTargetMember(IMemberMapperData mapperData, Expression contextAccess, Type targetType) => mapperData.GetTargetAccess(contextAccess, targetType); @@ -212,21 +223,15 @@ public static Expression UseTargetInstance(IMemberMapperData mapperData, Express } private static bool ConvertTargetType(Type targetType, Expression targetInstanceAccess) - { - if (targetInstanceAccess.Type.IsAssignableTo(targetType)) - { - return targetInstanceAccess.Type.IsValueType(); - } - - return true; - } + => targetInstanceAccess.Type.IsValueType() || !targetInstanceAccess.Type.IsAssignableTo(targetType); public Expression Swap( LambdaExpression lambda, + Type[] contextTypes, IMemberMapperData mapperData, Func targetValueFactory) { - var swapArgs = new SwapArgs(lambda, mapperData, targetValueFactory); + var swapArgs = new SwapArgs(lambda, contextTypes, mapperData, targetValueFactory); return _parametersSwapper.Invoke(swapArgs); } @@ -235,33 +240,29 @@ public Expression Swap( public class MappingContextInfo { - public MappingContextInfo(SwapArgs swapArgs, Type[] contextTypes) - : this(swapArgs, swapArgs.MapperData.MappingDataObject, contextTypes) + private readonly SwapArgs _swapArgs; + + public MappingContextInfo(SwapArgs swapArgs) + : this(swapArgs, swapArgs.MapperData.MappingDataObject) { } - public MappingContextInfo(SwapArgs swapArgs, Expression contextAccess, Type[] contextTypes) + public MappingContextInfo(SwapArgs swapArgs, Expression contextAccess) { - var contextSourceType = contextTypes[0]; - var contextTargetType = contextTypes[1]; - var sourceAccess = swapArgs.MapperData.GetSourceAccess(contextAccess, contextSourceType); - var targetAccess = swapArgs.TargetValueFactory.Invoke(swapArgs.MapperData, contextAccess, contextTargetType); - - ContextTypes = contextTypes; - CreatedObject = GetCreatedObject(swapArgs, contextTypes); - SourceAccess = GetValueAccess(sourceAccess, contextSourceType); - TargetAccess = GetValueAccess(targetAccess, contextTargetType); - Index = swapArgs.MapperData.EnumerableIndex; - Parent = swapArgs.MapperData.ParentObject; - MappingDataAccess = swapArgs.MapperData.GetTypedContextAccess(contextAccess, contextTypes); + _swapArgs = swapArgs; + + CreatedObject = GetCreatedObject(swapArgs); + SourceAccess = GetValueAccess(swapArgs.GetSourceAccess(contextAccess), ContextTypes[0]); + TargetAccess = GetValueAccess(swapArgs.GetTargetAccess(contextAccess), ContextTypes[1]); + MappingDataAccess = swapArgs.GetTypedContextAccess(contextAccess); } - private static Expression GetCreatedObject(SwapArgs swapArgs, ICollection contextTypes) + private static Expression GetCreatedObject(SwapArgs swapArgs) { - var neededCreatedObjectType = contextTypes.Last(); + var neededCreatedObjectType = swapArgs.ContextTypes.Last(); var createdObject = swapArgs.MapperData.CreatedObject; - if ((contextTypes.Count == 3) && (neededCreatedObjectType == typeof(int?))) + if ((swapArgs.ContextTypes.Length == 3) && (neededCreatedObjectType == typeof(int?))) { return createdObject; } @@ -276,7 +277,7 @@ private static Expression GetValueAccess(Expression valueAccess, Type neededAcce : valueAccess; } - public Type[] ContextTypes { get; } + public Type[] ContextTypes => _swapArgs.ContextTypes; public Expression CreatedObject { get; } @@ -286,28 +287,46 @@ private static Expression GetValueAccess(Expression valueAccess, Type neededAcce public Expression TargetAccess { get; } - public Expression Index { get; } + public Expression Index => _swapArgs.MapperData.EnumerableIndex; - public Expression Parent { get; } + public Expression Parent => _swapArgs.MapperData.ParentObject; } public class SwapArgs { public SwapArgs( LambdaExpression lambda, + Type[] contextTypes, IMemberMapperData mapperData, Func targetValueFactory) { Lambda = lambda; + ContextTypes = (contextTypes.Length > 1) ? contextTypes : contextTypes.Append(typeof(object)); MapperData = mapperData; TargetValueFactory = targetValueFactory; } public LambdaExpression Lambda { get; } + public Type[] ContextTypes { get; } + public IMemberMapperData MapperData { get; } public Func TargetValueFactory { get; } + + public bool ContextTypesMatch() => MapperData.TypesMatch(ContextTypes); + + public Expression GetAppropriateMappingContextAccess() + => MapperData.GetAppropriateMappingContextAccess(ContextTypes); + + public Expression GetTypedContextAccess(Expression contextAccess) + => MapperData.GetTypedContextAccess(contextAccess, ContextTypes); + + public Expression GetSourceAccess(Expression contextAccess) + => MapperData.GetSourceAccess(contextAccess, ContextTypes[0]); + + public Expression GetTargetAccess(Expression contextAccess) + => TargetValueFactory.Invoke(MapperData, contextAccess, ContextTypes[1]); } #endregion diff --git a/AgileMapper/Configuration/Projection/RecursionDepthSettings.cs b/AgileMapper/Configuration/Projection/RecursionDepthSettings.cs new file mode 100644 index 000000000..1317d67ac --- /dev/null +++ b/AgileMapper/Configuration/Projection/RecursionDepthSettings.cs @@ -0,0 +1,37 @@ +namespace AgileObjects.AgileMapper.Configuration.Projection +{ + using Members; + + internal class RecursionDepthSettings : UserConfiguredItemBase + { + private readonly int _recursionDepth; + + public RecursionDepthSettings(MappingConfigInfo configInfo, int recursionDepth) + : base(configInfo) + { + _recursionDepth = recursionDepth; + } + + public bool IsBeyondDepth(IBasicMapperData mapperData) + { + if (_recursionDepth == 0) + { + return true; + } + + var recursionDepth = -1; + + while (mapperData != null) + { + if (mapperData.TargetMember.IsRecursion) + { + ++recursionDepth; + } + + mapperData = mapperData.Parent; + } + + return recursionDepth > _recursionDepth; + } + } +} diff --git a/AgileMapper/Configuration/UserConfigurationSet.cs b/AgileMapper/Configuration/UserConfigurationSet.cs index 38b5119b7..21ef01597 100644 --- a/AgileMapper/Configuration/UserConfigurationSet.cs +++ b/AgileMapper/Configuration/UserConfigurationSet.cs @@ -9,6 +9,7 @@ using Extensions.Internal; using Members; using ObjectPopulation; + using Projection; internal class UserConfigurationSet { @@ -26,6 +27,7 @@ internal class UserConfigurationSet private List _creationCallbackFactories; private List _exceptionCallbackFactories; private DerivedTypePairSet _derivedTypes; + private List _recursionDepthSettings; public UserConfigurationSet(MapperContext mapperContext) { @@ -34,7 +36,7 @@ public UserConfigurationSet(MapperContext mapperContext) public bool ValidateMappingPlans { get; set; } - #region Mapped Object Caching Settings + #region MappedObjectCachingSettings private List MappedObjectCachingSettings => _mappedObjectCachingSettings ?? (_mappedObjectCachingSettings = new List()); @@ -179,7 +181,7 @@ public void Add(ConfiguredDataSourceFactory dataSourceFactory) DataSourceFactories.AddSortFilter(dataSourceFactory); } - public ICollection GetDataSources(IMemberMapperData mapperData) + public IList GetDataSources(IMemberMapperData mapperData) => QueryDataSourceFactories(mapperData).Select(dsf => dsf.Create(mapperData)).ToArray(); public IEnumerable QueryDataSourceFactories(IBasicMapperData mapperData) @@ -226,6 +228,28 @@ public Expression GetExceptionCallbackOrNull(IBasicMapperData mapperData) public DerivedTypePairSet DerivedTypes => _derivedTypes ?? (_derivedTypes = new DerivedTypePairSet()); + #region RecursionDepthSettings + + private List RecursionDepthSettings + => _recursionDepthSettings ?? (_recursionDepthSettings = new List()); + + public void Add(RecursionDepthSettings settings) + { + RecursionDepthSettings.Add(settings); + } + + public bool ShortCircuitRecursion(IBasicMapperData mapperData) + { + if (_recursionDepthSettings == null) + { + return true; + } + + return RecursionDepthSettings.FindMatch(mapperData)?.IsBeyondDepth(mapperData) != false; + } + + #endregion + #region Validation internal void ThrowIfMemberIsUnmappable(ConfiguredIgnoredMember ignoredMember) diff --git a/AgileMapper/Constants.cs b/AgileMapper/Constants.cs index aa623bcd6..b7a2993ee 100644 --- a/AgileMapper/Constants.cs +++ b/AgileMapper/Constants.cs @@ -25,6 +25,8 @@ internal static class Constants public const string Overwrite = "Overwrite"; + public const string Project = "Project"; + public const int BeforeLoopExitCheck = 0; public const int AfterLoopExitCheck = 1; diff --git a/AgileMapper/DataSources/DataSourceBase.cs b/AgileMapper/DataSources/DataSourceBase.cs index 0f6d26189..3e9385f2f 100644 --- a/AgileMapper/DataSources/DataSourceBase.cs +++ b/AgileMapper/DataSources/DataSourceBase.cs @@ -30,9 +30,9 @@ protected DataSourceBase( Expression condition = null) { SourceMember = sourceMember; - Condition = condition; Variables = variables; Value = value; + Condition = condition; } protected DataSourceBase( @@ -45,10 +45,10 @@ protected DataSourceBase( ProcessMemberAccesses( mapperData, ref value, - out var nestedAccesses, + out var nestedAccessChecks, out var variables); - Condition = GetCondition(nestedAccesses, mapperData); + Condition = GetCondition(nestedAccessChecks, mapperData); Variables = variables; Value = value; } @@ -58,11 +58,11 @@ protected DataSourceBase( private static void ProcessMemberAccesses( IMemberMapperData mapperData, ref Expression value, - out IList nestedAccesses, + out Expression nestedAccessChecks, out IList variables) { var valueInfo = mapperData.GetExpressionInfoFor(value, targetCanBeNull: false); - nestedAccesses = valueInfo.NestedAccesses; + nestedAccessChecks = valueInfo.NestedAccessChecks; if (valueInfo.MultiInvocations.None()) { @@ -91,18 +91,16 @@ private static void ProcessMemberAccesses( value = Expression.Block(valueExpressions); } - private Expression GetCondition(IList nestedAccesses, IMemberMapperData mapperData) + private Expression GetCondition(Expression nestedAccessChecks, IMemberMapperData mapperData) { - if (nestedAccesses.None()) + if (nestedAccessChecks == null) { return null; } - var notNullChecks = nestedAccesses.GetIsNotDefaultComparisonsOrNull(); - - if (!IsOptionalEntityMemberId(mapperData)) + if (IsNotOptionalEntityMemberId(mapperData)) { - return notNullChecks; + return nestedAccessChecks; } var sourceMemberValue = SourceMember.GetQualifiedAccess(mapperData); @@ -110,7 +108,7 @@ private Expression GetCondition(IList nestedAccesses, IMemberMapperD if (!sourceValueType.IsNumeric()) { - return notNullChecks; + return nestedAccessChecks; } if (sourceMemberValue.Type.IsNullableType()) @@ -121,16 +119,16 @@ private Expression GetCondition(IList nestedAccesses, IMemberMapperD var zero = 0.ToConstantExpression(sourceValueType); var sourceValueNonZero = Expression.NotEqual(sourceMemberValue, zero); - return Expression.AndAlso(notNullChecks, sourceValueNonZero); + return Expression.AndAlso(nestedAccessChecks, sourceValueNonZero); } - private static bool IsOptionalEntityMemberId(IMemberMapperData mapperData) + private static bool IsNotOptionalEntityMemberId(IMemberMapperData mapperData) { var targetMember = mapperData.TargetMember; if (!targetMember.Type.IsNullableType()) { - return false; + return true; } var targetMemberNameSuffix = default(string); @@ -151,12 +149,12 @@ private static bool IsOptionalEntityMemberId(IMemberMapperData mapperData) break; default: - return false; + return true; } if (!mapperData.TargetTypeIsEntity()) { - return false; + return true; } var entityMemberNameLength = targetMember.Name.Length - targetMemberNameSuffix.Length; @@ -168,7 +166,7 @@ private static bool IsOptionalEntityMemberId(IMemberMapperData mapperData) .GetTargetMembers(mapperData.TargetType) .FirstOrDefault(m => m.Name == entityMemberName); - return mapperData.IsEntity(entityMember?.Type); + return !mapperData.IsEntity(entityMember?.Type, out var _); } #endregion @@ -197,8 +195,5 @@ public Expression AddCondition(Expression value, Expression alternateBranch = nu ? Expression.IfThenElse(Condition, value, alternateBranch) : Expression.IfThen(Condition, value); } - - public Expression GetTargetMemberPopulation(IMemberMapperData mapperData) - => mapperData.GetTargetMemberPopulation(Value); } } \ No newline at end of file diff --git a/AgileMapper/DataSources/DataSourceFinder.cs b/AgileMapper/DataSources/DataSourceFinder.cs index 69e215f58..570e4af27 100644 --- a/AgileMapper/DataSources/DataSourceFinder.cs +++ b/AgileMapper/DataSources/DataSourceFinder.cs @@ -28,7 +28,7 @@ public DataSourceSet FindFor(IChildMemberMappingData childMappingData) .Where(ds => ds.IsValid) .ToArray(); - return new DataSourceSet(validDataSources); + return new DataSourceSet(childMappingData.MapperData, validDataSources); } private IEnumerable EnumerateDataSources(IChildMemberMappingData childMappingData) @@ -77,7 +77,7 @@ private IEnumerable EnumerateDataSources(IChildMemberMappingData ch private static bool DataSourcesAreConfigured( IMemberMapperData mapperData, - out IEnumerable configuredDataSources) + out IList configuredDataSources) { configuredDataSources = mapperData .MapperContext @@ -105,7 +105,7 @@ private bool UseMaptimeDataSources( } private static IEnumerable GetSourceMemberDataSources( - IEnumerable configuredDataSources, + IList configuredDataSources, int dataSourceIndex, IChildMemberMappingData mappingData) { @@ -118,7 +118,7 @@ private static IEnumerable GetSourceMemberDataSources( var matchingSourceMemberDataSource = GetSourceMemberDataSourceOrNull(bestMatchingSourceMember, contextMappingData); if ((matchingSourceMemberDataSource == null) || - configuredDataSources.Any(cds => cds.IsSameAs(matchingSourceMemberDataSource))) + configuredDataSources.Any(cds => cds.IsSameAs(matchingSourceMemberDataSource))) { if (dataSourceIndex == 0) { @@ -128,7 +128,7 @@ private static IEnumerable GetSourceMemberDataSources( yield return new ComplexTypeMappingDataSource(dataSourceIndex, mappingData); } } - else + else if (configuredDataSources.Any() && configuredDataSources.Last().IsConditional) { yield return GetFallbackDataSourceFor(mappingData); } @@ -143,7 +143,8 @@ private static IEnumerable GetSourceMemberDataSources( yield break; } - if (matchingSourceMemberDataSource.IsConditional) + if (matchingSourceMemberDataSource.IsConditional && + (matchingSourceMemberDataSource.IsValid || configuredDataSources.Any())) { yield return GetFallbackDataSourceFor(mappingData); } diff --git a/AgileMapper/DataSources/DataSourceSet.cs b/AgileMapper/DataSources/DataSourceSet.cs index 111bb2f20..a9ad3c72e 100644 --- a/AgileMapper/DataSources/DataSourceSet.cs +++ b/AgileMapper/DataSources/DataSourceSet.cs @@ -11,8 +11,11 @@ internal class DataSourceSet : IEnumerable private readonly IList _dataSources; private readonly List _variables; - public DataSourceSet(params IDataSource[] dataSources) + public DataSourceSet( + IMemberMapperData mapperData, + params IDataSource[] dataSources) { + MapperData = mapperData; _dataSources = dataSources; _variables = new List(); None = dataSources.Length == 0; @@ -38,10 +41,12 @@ public DataSourceSet(params IDataSource[] dataSources) } } - public bool None { get; } + public IMemberMapperData MapperData { get; } - public bool HasValue { get; } + public bool None { get; } + public bool HasValue { get; } + public Expression SourceMemberTypeTest { get; } public ICollection Variables => _variables; @@ -50,9 +55,9 @@ public DataSourceSet(params IDataSource[] dataSources) public Expression GetValueExpression() => _dataSources.ReverseChain(); - public Expression GetPopulationExpression(IMemberMapperData mapperData) + public Expression GetPopulationExpression() { - var fallbackValue = GetFallbackValueOrNull(mapperData); + var fallbackValue = GetFallbackValueOrNull(); var excludeFallback = fallbackValue == null; Expression population = null; @@ -68,7 +73,7 @@ public Expression GetPopulationExpression(IMemberMapperData mapperData) continue; } - population = mapperData.GetTargetMemberPopulation(fallbackValue); + population = MapperData.GetTargetMemberPopulation(fallbackValue); if (dataSource.IsConditional) { @@ -79,20 +84,20 @@ public Expression GetPopulationExpression(IMemberMapperData mapperData) continue; } - var memberPopulation = dataSource.GetTargetMemberPopulation(mapperData); - - population = dataSource.AddCondition(memberPopulation, population); + var memberPopulation = MapperData.GetTargetMemberPopulation(dataSource.Value); + + population = dataSource.AddCondition(memberPopulation, population); population = dataSource.AddPreCondition(population); } return population; } - private Expression GetFallbackValueOrNull(IMemberMapperData mapperData) + private Expression GetFallbackValueOrNull() { var finalDataSource = _dataSources.Last(); - var fallbackValue = finalDataSource.Value; - + var fallbackValue = finalDataSource.Value; + if (finalDataSource.IsConditional || _dataSources.HasOne()) { return fallbackValue; @@ -103,27 +108,27 @@ private Expression GetFallbackValueOrNull(IMemberMapperData mapperData) return ((BinaryExpression)fallbackValue).Right; } - var targetMemberAccess = mapperData.GetTargetMemberAccess(); + var targetMemberAccess = MapperData.GetTargetMemberAccess(); - if (fallbackValue.ToString() == targetMemberAccess.ToString()) + if (ExpressionEvaluation.AreEqual(fallbackValue, targetMemberAccess)) { return null; } return fallbackValue; - } - - #region IEnumerable Members - - public IEnumerator GetEnumerator() => _dataSources.GetEnumerator(); - - #region ExcludeFromCodeCoverage -#if DEBUG - [ExcludeFromCodeCoverage] -#endif - #endregion - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - #endregion - } + } + + #region IEnumerable Members + + public IEnumerator GetEnumerator() => _dataSources.GetEnumerator(); + + #region ExcludeFromCodeCoverage +#if DEBUG + [ExcludeFromCodeCoverage] +#endif + #endregion + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + #endregion + } } \ No newline at end of file diff --git a/AgileMapper/DataSources/IDataSource.cs b/AgileMapper/DataSources/IDataSource.cs index 49078e2ae..6476e71c8 100644 --- a/AgileMapper/DataSources/IDataSource.cs +++ b/AgileMapper/DataSources/IDataSource.cs @@ -20,7 +20,5 @@ internal interface IDataSource : IConditionallyChainable Expression AddPreCondition(Expression population); Expression AddCondition(Expression value, Expression alternateBranch = null); - - Expression GetTargetMemberPopulation(IMemberMapperData mapperData); } } diff --git a/AgileMapper/Extensions/Internal/ExpressionEquator.cs b/AgileMapper/Extensions/Internal/ExpressionEquator.cs deleted file mode 100644 index 16d9308b9..000000000 --- a/AgileMapper/Extensions/Internal/ExpressionEquator.cs +++ /dev/null @@ -1,292 +0,0 @@ -namespace AgileObjects.AgileMapper.Extensions.Internal -{ - using System; - using System.Collections.Generic; - using System.Linq.Expressions; - using NetStandardPolyfills; - - internal class ExpressionEquator : IEqualityComparer - { - public static IEqualityComparer Instance = new ExpressionEquator(); - - public bool Equals(Expression x, Expression y) - { - while (true) - { - if (x.NodeType != y.NodeType) - { - return false; - } - - switch (x.NodeType) - { - case ExpressionType.Call: - return AreEqual((MethodCallExpression)x, (MethodCallExpression)y); - - case ExpressionType.Conditional: - return AreEqual((ConditionalExpression)x, (ConditionalExpression)y); - - case ExpressionType.Constant: - return AreEqual((ConstantExpression)x, (ConstantExpression)y); - - case ExpressionType.ArrayLength: - case ExpressionType.Convert: - case ExpressionType.Negate: - case ExpressionType.NegateChecked: - case ExpressionType.Not: - case ExpressionType.TypeAs: - return AreEqual((UnaryExpression)x, (UnaryExpression)y); - - case ExpressionType.Index: - return AreEqual((IndexExpression)x, (IndexExpression)y); - - case ExpressionType.Lambda: - x = ((LambdaExpression)x).Body; - y = ((LambdaExpression)y).Body; - continue; - - case ExpressionType.ListInit: - return AreEqual((ListInitExpression)x, (ListInitExpression)y); - - case ExpressionType.MemberAccess: - return AreEqual((MemberExpression)x, (MemberExpression)y); - - case ExpressionType.Add: - case ExpressionType.AddChecked: - case ExpressionType.And: - case ExpressionType.AndAlso: - case ExpressionType.ArrayIndex: - case ExpressionType.Coalesce: - case ExpressionType.Divide: - case ExpressionType.Equal: - case ExpressionType.ExclusiveOr: - case ExpressionType.GreaterThan: - case ExpressionType.GreaterThanOrEqual: - case ExpressionType.LeftShift: - case ExpressionType.LessThan: - case ExpressionType.LessThanOrEqual: - case ExpressionType.Modulo: - case ExpressionType.Multiply: - case ExpressionType.MultiplyChecked: - case ExpressionType.NotEqual: - case ExpressionType.Or: - case ExpressionType.OrElse: - case ExpressionType.RightShift: - case ExpressionType.Subtract: - case ExpressionType.SubtractChecked: - return AreEqual((BinaryExpression)x, (BinaryExpression)y); - - case ExpressionType.MemberInit: - return AreEqual((MemberInitExpression)x, (MemberInitExpression)y); - - case ExpressionType.New: - return AreEqual((NewExpression)x, (NewExpression)y); - - case ExpressionType.NewArrayBounds: - case ExpressionType.NewArrayInit: - return AreEqual((NewArrayExpression)x, (NewArrayExpression)y); - - case ExpressionType.Parameter: - return AreEqual((ParameterExpression)x, (ParameterExpression)y); - - case ExpressionType.Quote: - x = ((UnaryExpression)x).Operand; - y = ((UnaryExpression)y).Operand; - continue; - - case ExpressionType.TypeIs: - return AreEqual((TypeBinaryExpression)x, (TypeBinaryExpression)y); - } - - throw new NotImplementedException("Unable to equate Expressions of type " + x.NodeType); - } - } - - private bool AllEqual(IList xItems, IList yItems) - { - if (xItems.Count != yItems.Count) - { - return false; - } - - for (var i = 0; i < xItems.Count; i++) - { - if (!Equals(xItems[i], yItems[i])) - { - return false; - } - } - - return true; - } - - private bool AreEqual(MethodCallExpression x, MethodCallExpression y) - { - return ReferenceEquals(x.Method, y.Method) && - (((x.Object == null) && (y.Object == null)) || ((x.Object != null) && Equals(x.Object, y.Object))) && - AllEqual(x.Arguments, y.Arguments); - } - - private bool AreEqual(ConditionalExpression x, ConditionalExpression y) - { - return (x.Type == y.Type) && Equals(x.Test, y.Test) && - Equals(x.IfTrue, y.IfTrue) && Equals(x.IfFalse, y.IfFalse); - } - - private static bool AreEqual(ConstantExpression x, ConstantExpression y) - { - return ReferenceEquals(x.Value, y.Value) || x.Value.Equals(y.Value); - } - - private bool AreEqual(UnaryExpression x, UnaryExpression y) - { - return (x.Type == y.Type) && Equals(x.Operand, y.Operand); - } - - private bool AreEqual(IndexExpression x, IndexExpression y) - { - return ReferenceEquals(x.Indexer, y.Indexer) && - AllEqual(x.Arguments, y.Arguments); - } - - private bool AreEqual(ListInitExpression x, ListInitExpression y) - { - return (x.Type == y.Type) && Equals(x.NewExpression, y.NewExpression) && - AllEqual(x.Initializers, y.Initializers); - } - - private bool AllEqual(IList xInitializers, IList yInitializers) - { - if (xInitializers.Count != yInitializers.Count) - { - return false; - } - - for (var i = 0; i < xInitializers.Count; i++) - { - var x = xInitializers[i]; - var y = yInitializers[i]; - - if (!ReferenceEquals(x.AddMethod, y.AddMethod) || - !AllEqual(x.Arguments, y.Arguments)) - { - return false; - } - } - - return true; - } - - private static bool AreEqual(MemberExpression x, MemberExpression y) - { - if (ReferenceEquals(x.Member, y.Member)) - { - return true; - } - - // ReSharper disable once PossibleNullReferenceException - return (x.Member.Name == y.Member.Name) && - x.Member.DeclaringType.IsAssignableTo(y.Member.DeclaringType); - } - - private bool AreEqual(BinaryExpression x, BinaryExpression y) - { - return ReferenceEquals(x.Method, y.Method) && - Equals(x.Left, y.Left) && Equals(x.Right, y.Right); - } - - private bool AreEqual(MemberInitExpression x, MemberInitExpression y) - { - return Equals(x.NewExpression, y.NewExpression) && - AllEqual(x.Bindings, y.Bindings); - } - - private bool AllEqual(IList xBindings, IList yBindings) - { - if (xBindings.Count != yBindings.Count) - { - return false; - } - - for (var i = 0; i < xBindings.Count; i++) - { - var x = xBindings[i]; - var y = yBindings[i]; - - if ((x.BindingType != y.BindingType) || !ReferenceEquals(x.Member, y.Member)) - { - return false; - } - - switch (x.BindingType) - { - case MemberBindingType.Assignment: - if (Equals(((MemberAssignment)x).Expression, ((MemberAssignment)y).Expression)) - { - break; - } - return false; - - case MemberBindingType.MemberBinding: - if (AllEqual(((MemberMemberBinding)x).Bindings, ((MemberMemberBinding)y).Bindings)) - { - break; - } - return false; - - case MemberBindingType.ListBinding: - if (AreEqual((MemberListBinding)x, (MemberListBinding)y)) - { - break; - } - return false; - } - } - - return true; - } - - private bool AreEqual(MemberListBinding x, MemberListBinding y) - { - if (x.Initializers.Count != y.Initializers.Count) - { - return false; - } - - for (var i = 0; i < x.Initializers.Count; i++) - { - var xInitialiser = x.Initializers[i]; - var yInitialiser = y.Initializers[i]; - - if (!ReferenceEquals(xInitialiser.AddMethod, yInitialiser.AddMethod) || - !AllEqual(xInitialiser.Arguments, yInitialiser.Arguments)) - { - return false; - } - } - - return true; - } - - private bool AreEqual(NewExpression x, NewExpression y) - { - return (x.Type == y.Type) && ReferenceEquals(x.Constructor, y.Constructor) && - AllEqual(x.Arguments, y.Arguments); - } - - private bool AreEqual(NewArrayExpression x, NewArrayExpression y) - { - return (x.Type == y.Type) && AllEqual(x.Expressions, y.Expressions); - } - - private static bool AreEqual(ParameterExpression x, ParameterExpression y) - { - return (x.Type == y.Type) && (x.Name == y.Name); - } - - private bool AreEqual(TypeBinaryExpression x, TypeBinaryExpression y) - => (x.TypeOperand == y.TypeOperand) && Equals(x.Expression, y.Expression); - - public int GetHashCode(Expression obj) => 0; - } -} \ No newline at end of file diff --git a/AgileMapper/Extensions/Internal/ExpressionEvaluation.cs b/AgileMapper/Extensions/Internal/ExpressionEvaluation.cs new file mode 100644 index 000000000..00590268f --- /dev/null +++ b/AgileMapper/Extensions/Internal/ExpressionEvaluation.cs @@ -0,0 +1,319 @@ +namespace AgileObjects.AgileMapper.Extensions.Internal +{ + using System; + using System.Collections.Generic; + using System.Linq.Expressions; + using NetStandardPolyfills; + + internal static class ExpressionEvaluation + { + private static readonly IEqualityComparer _equator = new ExpressionEquator(MemberAccessesAreEqual); + + public static readonly IEqualityComparer Equivalator = new ExpressionEquator(MemberAccessesAreEquivalent); + + public static bool AreEqual(Expression x, Expression y) => _equator.Equals(x, y); + + public static bool AreEquivalent(Expression x, Expression y) => Equivalator.Equals(x, y); + + #region Member Access Evaluation + + private static bool MemberAccessesAreEqual(ExpressionEquator equator, MemberExpression x, MemberExpression y) + => MemberAccessesAreEquivalent(equator, x, y) && equator.Equals(x.Expression, y.Expression); + + private static bool MemberAccessesAreEquivalent(ExpressionEquator equator, MemberExpression x, MemberExpression y) + { + if (ReferenceEquals(x.Member, y.Member)) + { + return true; + } + + // ReSharper disable once PossibleNullReferenceException + return (x.Member.Name == y.Member.Name) && + x.Member.DeclaringType.IsAssignableTo(y.Member.DeclaringType); + } + + #endregion + + private class ExpressionEquator : IEqualityComparer + { + private readonly Func _memberAccessComparer; + + public ExpressionEquator( + Func memberAccessComparer) + { + _memberAccessComparer = memberAccessComparer; + } + + public bool Equals(Expression x, Expression y) + { + while (true) + { + if (x.NodeType != y.NodeType) + { + return false; + } + + switch (x.NodeType) + { + case ExpressionType.Call: + return AreEqual((MethodCallExpression)x, (MethodCallExpression)y); + + case ExpressionType.Conditional: + return AreEqual((ConditionalExpression)x, (ConditionalExpression)y); + + case ExpressionType.Constant: + return AreEqual((ConstantExpression)x, (ConstantExpression)y); + + case ExpressionType.ArrayLength: + case ExpressionType.Convert: + case ExpressionType.Negate: + case ExpressionType.NegateChecked: + case ExpressionType.Not: + case ExpressionType.TypeAs: + return AreEqual((UnaryExpression)x, (UnaryExpression)y); + + case ExpressionType.Default: + return ((DefaultExpression)x).Type == ((DefaultExpression)y).Type; + + case ExpressionType.Index: + return AreEqual((IndexExpression)x, (IndexExpression)y); + + case ExpressionType.Lambda: + x = ((LambdaExpression)x).Body; + y = ((LambdaExpression)y).Body; + continue; + + case ExpressionType.ListInit: + return AreEqual((ListInitExpression)x, (ListInitExpression)y); + + case ExpressionType.MemberAccess: + return _memberAccessComparer.Invoke(this, (MemberExpression)x, (MemberExpression)y); + + case ExpressionType.Add: + case ExpressionType.AddChecked: + case ExpressionType.And: + case ExpressionType.AndAlso: + case ExpressionType.ArrayIndex: + case ExpressionType.Coalesce: + case ExpressionType.Divide: + case ExpressionType.Equal: + case ExpressionType.ExclusiveOr: + case ExpressionType.GreaterThan: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.LeftShift: + case ExpressionType.LessThan: + case ExpressionType.LessThanOrEqual: + case ExpressionType.Modulo: + case ExpressionType.Multiply: + case ExpressionType.MultiplyChecked: + case ExpressionType.NotEqual: + case ExpressionType.Or: + case ExpressionType.OrElse: + case ExpressionType.RightShift: + case ExpressionType.Subtract: + case ExpressionType.SubtractChecked: + return AreEqual((BinaryExpression)x, (BinaryExpression)y); + + case ExpressionType.MemberInit: + return AreEqual((MemberInitExpression)x, (MemberInitExpression)y); + + case ExpressionType.New: + return AreEqual((NewExpression)x, (NewExpression)y); + + case ExpressionType.NewArrayBounds: + case ExpressionType.NewArrayInit: + return AreEqual((NewArrayExpression)x, (NewArrayExpression)y); + + case ExpressionType.Parameter: + return AreEqual((ParameterExpression)x, (ParameterExpression)y); + + case ExpressionType.Quote: + x = ((UnaryExpression)x).Operand; + y = ((UnaryExpression)y).Operand; + continue; + + case ExpressionType.TypeIs: + return AreEqual((TypeBinaryExpression)x, (TypeBinaryExpression)y); + } + + throw new NotImplementedException("Unable to equate Expressions of type " + x.NodeType); + } + } + + private bool AllEqual(IList xItems, IList yItems) + { + if (xItems.Count != yItems.Count) + { + return false; + } + + for (var i = 0; i < xItems.Count; i++) + { + if (!Equals(xItems[i], yItems[i])) + { + return false; + } + } + + return true; + } + + private bool AreEqual(MethodCallExpression x, MethodCallExpression y) + { + return ReferenceEquals(x.Method, y.Method) && + (((x.Object == null) && (y.Object == null)) || ((x.Object != null) && Equals(x.Object, y.Object))) && + AllEqual(x.Arguments, y.Arguments); + } + + private bool AreEqual(ConditionalExpression x, ConditionalExpression y) + { + return (x.Type == y.Type) && Equals(x.Test, y.Test) && + Equals(x.IfTrue, y.IfTrue) && Equals(x.IfFalse, y.IfFalse); + } + + private static bool AreEqual(ConstantExpression x, ConstantExpression y) + { + return ReferenceEquals(x.Value, y.Value) || x.Value.Equals(y.Value); + } + + private bool AreEqual(UnaryExpression x, UnaryExpression y) + { + return (x.Type == y.Type) && Equals(x.Operand, y.Operand); + } + + private bool AreEqual(IndexExpression x, IndexExpression y) + { + return ReferenceEquals(x.Indexer, y.Indexer) && + AllEqual(x.Arguments, y.Arguments); + } + + private bool AreEqual(ListInitExpression x, ListInitExpression y) + { + return (x.Type == y.Type) && Equals(x.NewExpression, y.NewExpression) && + AllEqual(x.Initializers, y.Initializers); + } + + private bool AllEqual(IList xInitializers, IList yInitializers) + { + if (xInitializers.Count != yInitializers.Count) + { + return false; + } + + for (var i = 0; i < xInitializers.Count; i++) + { + var x = xInitializers[i]; + var y = yInitializers[i]; + + if (!ReferenceEquals(x.AddMethod, y.AddMethod) || + !AllEqual(x.Arguments, y.Arguments)) + { + return false; + } + } + + return true; + } + + private bool AreEqual(BinaryExpression x, BinaryExpression y) + { + return ReferenceEquals(x.Method, y.Method) && + Equals(x.Left, y.Left) && Equals(x.Right, y.Right); + } + + private bool AreEqual(MemberInitExpression x, MemberInitExpression y) + { + return Equals(x.NewExpression, y.NewExpression) && + AllEqual(x.Bindings, y.Bindings); + } + + private bool AllEqual(IList xBindings, IList yBindings) + { + if (xBindings.Count != yBindings.Count) + { + return false; + } + + for (var i = 0; i < xBindings.Count; i++) + { + var x = xBindings[i]; + var y = yBindings[i]; + + if ((x.BindingType != y.BindingType) || !ReferenceEquals(x.Member, y.Member)) + { + return false; + } + + switch (x.BindingType) + { + case MemberBindingType.Assignment: + if (Equals(((MemberAssignment)x).Expression, ((MemberAssignment)y).Expression)) + { + break; + } + return false; + + case MemberBindingType.MemberBinding: + if (AllEqual(((MemberMemberBinding)x).Bindings, ((MemberMemberBinding)y).Bindings)) + { + break; + } + return false; + + case MemberBindingType.ListBinding: + if (AreEqual((MemberListBinding)x, (MemberListBinding)y)) + { + break; + } + return false; + } + } + + return true; + } + + private bool AreEqual(MemberListBinding x, MemberListBinding y) + { + if (x.Initializers.Count != y.Initializers.Count) + { + return false; + } + + for (var i = 0; i < x.Initializers.Count; i++) + { + var xInitialiser = x.Initializers[i]; + var yInitialiser = y.Initializers[i]; + + if (!ReferenceEquals(xInitialiser.AddMethod, yInitialiser.AddMethod) || + !AllEqual(xInitialiser.Arguments, yInitialiser.Arguments)) + { + return false; + } + } + + return true; + } + + private bool AreEqual(NewExpression x, NewExpression y) + { + return (x.Type == y.Type) && ReferenceEquals(x.Constructor, y.Constructor) && + AllEqual(x.Arguments, y.Arguments); + } + + private bool AreEqual(NewArrayExpression x, NewArrayExpression y) + { + return (x.Type == y.Type) && AllEqual(x.Expressions, y.Expressions); + } + + private static bool AreEqual(ParameterExpression x, ParameterExpression y) + { + return (x.Type == y.Type) && (x.Name == y.Name); + } + + private bool AreEqual(TypeBinaryExpression x, TypeBinaryExpression y) + => (x.TypeOperand == y.TypeOperand) && Equals(x.Expression, y.Expression); + + public int GetHashCode(Expression obj) => 0; + } + } +} \ No newline at end of file diff --git a/AgileMapper/Extensions/Internal/ExpressionExtensions.cs b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs index 017cb82a6..43dfd034f 100644 --- a/AgileMapper/Extensions/Internal/ExpressionExtensions.cs +++ b/AgileMapper/Extensions/Internal/ExpressionExtensions.cs @@ -12,20 +12,25 @@ internal static partial class ExpressionExtensions { - private static readonly MethodInfo _listToArrayMethod = typeof(EnumerableExtensions) - .GetPublicStaticMethods("ToArray").First(); + private static readonly MethodInfo _listToArrayMethod; + private static readonly MethodInfo _collectionToArrayMethod; + private static readonly MethodInfo _linqToArrayMethod; + private static readonly MethodInfo _linqToListMethod; + private static readonly MethodInfo _stringEqualsMethod; - private static readonly MethodInfo _collectionToArrayMethod = typeof(EnumerableExtensions) - .GetPublicStaticMethods("ToArray").ElementAt(1); - - private static readonly MethodInfo _linqToArrayMethod = typeof(Enumerable) - .GetPublicStaticMethod("ToArray"); + static ExpressionExtensions() + { + var toArrayExtensionMethods = typeof(EnumerableExtensions).GetPublicStaticMethods("ToArray").ToArray(); + _listToArrayMethod = toArrayExtensionMethods.First(); + _collectionToArrayMethod = toArrayExtensionMethods.ElementAt(1); - private static readonly MethodInfo _linqToListMethod = typeof(Enumerable) - .GetPublicStaticMethod("ToList"); + var linqEnumerableMethods = typeof(Enumerable).GetPublicStaticMethods().ToArray(); + _linqToArrayMethod = linqEnumerableMethods.First(m => m.Name == "ToArray"); + _linqToListMethod = linqEnumerableMethods.First(m => m.Name == "ToList"); - private static readonly MethodInfo _stringEqualsMethod = typeof(string) - .GetPublicStaticMethod("Equals", parameterCount: 3); + _stringEqualsMethod = typeof(string) + .GetPublicStaticMethod("Equals", parameterCount: 3); + } [DebuggerStepThrough] public static BinaryExpression AssignTo(this Expression subject, Expression value) @@ -118,7 +123,20 @@ public static Expression GetIsNotDefaultComparison(this Expression expression) var typeDefault = expression.Type.ToDefaultExpression(); - return Expression.NotEqual(expression, typeDefault); + if (!expression.Type.IsValueType() || !expression.Type.IsComplex()) + { + return Expression.NotEqual(expression, typeDefault); + } + + var objectEquals = typeof(object).GetPublicStaticMethod("Equals"); + + var objectEqualsCall = Expression.Call( + null, + objectEquals, + expression.GetConversionToObject(), + typeDefault.GetConversionToObject()); + + return Expression.IsFalse(objectEqualsCall); } public static Expression GetIndexAccess(this Expression indexedExpression, Expression indexValue) @@ -173,6 +191,9 @@ public static Expression GetConversionTo(this Expression expression, Type target return Expression.Convert(expression, targetType); } + public static Expression WithToArrayLinqCall(this Expression enumerable, Type elementType) + => GetToEnumerableCall(enumerable, _linqToArrayMethod, elementType); + public static Expression WithToArrayCall(this Expression enumerable, Type elementType) { var conversionMethod = GetToArrayConversionMethod(enumerable, elementType); @@ -217,6 +238,10 @@ private static bool TryGetWrapperMethod( private static MethodInfo GetNonListToArrayConversionMethod(EnumerableTypeHelper typeHelper) => typeHelper.HasCollectionInterface ? _collectionToArrayMethod : _linqToArrayMethod; + [DebuggerStepThrough] + public static MethodCallExpression WithToStringCall(this Expression value) + => Expression.Call(value, value.Type.GetPublicInstanceMethod("ToString", parameterCount: 0)); + public static Expression WithToReadOnlyCollectionCall(this Expression enumerable, Type elementType) { var typeHelper = new EnumerableTypeHelper(enumerable.Type, elementType); @@ -258,7 +283,7 @@ public static Expression WithToCollectionCall(this Expression enumerable, Type e return GetCollectionCreation(typeHelper, toArrayCall); } - public static Expression WithToListCall(this Expression enumerable, Type elementType) + public static Expression WithToListLinqCall(this Expression enumerable, Type elementType) => GetToEnumerableCall(enumerable, _linqToListMethod, elementType); private static Expression GetToEnumerableCall(Expression enumerable, MethodInfo method, Type elementType) @@ -273,7 +298,10 @@ private static Expression GetToEnumerableCall(Expression enumerable, MethodInfo return Expression.Call(typedToEnumerableMethod, enumerable); } - public static Expression GetEmptyInstanceCreation(this Type enumerableType, Type elementType) + public static Expression GetEmptyInstanceCreation( + this Type enumerableType, + Type elementType, + EnumerableTypeHelper typeHelper = null) { if (enumerableType.IsArray) { @@ -285,7 +313,10 @@ public static Expression GetEmptyInstanceCreation(this Type enumerableType, Type return Expression.New(enumerableType); } - var typeHelper = new EnumerableTypeHelper(enumerableType, elementType); + if (typeHelper == null) + { + typeHelper = new EnumerableTypeHelper(enumerableType, elementType); + } if (typeHelper.IsEnumerableInterface) { diff --git a/AgileMapper/Extensions/Internal/ExpressionReplacementDictionary.cs b/AgileMapper/Extensions/Internal/ExpressionReplacementDictionary.cs index 4de3cac55..3c2eacb7c 100644 --- a/AgileMapper/Extensions/Internal/ExpressionReplacementDictionary.cs +++ b/AgileMapper/Extensions/Internal/ExpressionReplacementDictionary.cs @@ -6,7 +6,7 @@ namespace AgileObjects.AgileMapper.Extensions.Internal internal class ExpressionReplacementDictionary : Dictionary { public ExpressionReplacementDictionary(int capacity) - : base(capacity, ExpressionEquator.Instance) + : base(capacity, ExpressionEvaluation.Equivalator) { } } diff --git a/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs b/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs index 92e142651..8379fd125 100644 --- a/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs +++ b/AgileMapper/Extensions/Internal/StringExpressionExtensions.cs @@ -38,6 +38,9 @@ static StringExpressionExtensions() .ToArray(); } + public static MethodInfo GetConcatMethod(int parameterCount) + => _stringConcatMethods.First(m => m.GetParameters().Length == parameterCount); + public static Expression GetStringConcatCall(this IList expressions) { if (expressions.None()) diff --git a/AgileMapper/Extensions/Internal/TypeExtensions.cs b/AgileMapper/Extensions/Internal/TypeExtensions.cs index 5f5e96e88..f39740928 100644 --- a/AgileMapper/Extensions/Internal/TypeExtensions.cs +++ b/AgileMapper/Extensions/Internal/TypeExtensions.cs @@ -177,6 +177,11 @@ public static bool IsEnumerable(this Type type) type.IsAssignableTo(typeof(IEnumerable))); } + public static bool IsQueryable(this Type type) + { + return type.IsGenericType() && type.GetGenericTypeDefinition() == typeof(IQueryable<>); + } + public static bool IsComplex(this Type type) { return !type.IsSimple() && !type.IsEnumerable(); @@ -284,6 +289,9 @@ public static bool IsUnsignedNumeric(this Type type) public static bool IsWholeNumberNumeric(this Type type) => Constants.WholeNumberNumericTypes.Contains(type); + public static bool IsNonWholeNumberNumeric(this Type type) + => IsNumeric(type) && !IsWholeNumberNumeric(type); + private static double GetMaxValueFor(Type type) => GetValueFor(type, Constants.NumericTypeMaxValuesByType, values => values.Max()); diff --git a/AgileMapper/IMapper.cs b/AgileMapper/IMapper.cs index 1aa1c75c1..79f4b41ba 100644 --- a/AgileMapper/IMapper.cs +++ b/AgileMapper/IMapper.cs @@ -1,9 +1,11 @@ namespace AgileObjects.AgileMapper { using System; + using System.Linq; using System.Linq.Expressions; using Api; using Api.Configuration; + using Queryables.Api; /// /// Provides mapping and mapping configuration services. @@ -17,7 +19,7 @@ public interface IMapper : IDisposable IMapper CloneSelf(); /// - /// Create and compile mapping functions for a particular type of mapping of the source type specified by + /// Create and compile a mapping function for a particular type of mapping of the source type specified by /// the given . Use this overload for anonymous types. /// /// The type of the given . @@ -25,22 +27,38 @@ public interface IMapper : IDisposable /// An instance specifying the source type for which a mapping plan should be created. /// /// - /// An IPlanTargetAndRuleSetSelector with which to specify the type of mapping the functions for which + /// An IPlanTargetAndRuleSetSelector with which to specify the type of mapping the function for which /// should be cached. /// IPlanTargetAndRuleSetSelector GetPlanFor(TSource exampleInstance); /// - /// Create and compile mapping functions for a particular type of mapping of the source type + /// Create and compile a mapping function for a particular type of mapping of the source type /// specified by the type argument. /// /// The source type for which to create the mapping functions. /// - /// An IPlanTargetAndRuleSetSelector with which to specify the type of mapping the functions for which + /// An IPlanTargetAndRuleSetSelector with which to specify the type of mapping the function for which /// should be cached. /// IPlanTargetAndRuleSetSelector GetPlanFor(); + /// + /// Create and compile a query projection function from the source IQueryable Type specified by the given + /// . + /// + /// + /// The type of element contained in the source IQueryable from which the projection function to be created will project. + /// + /// + /// An IQueryable instance specifying the source IQueryable for which a query projection mapping plan should be created. + /// + /// + /// An IProjectionPlanTargetSelector with which to specify the target Type to which the query projection function to + /// be created should be cached. + /// + IProjectionPlanTargetSelector GetPlanForProjecting(IQueryable exampleQueryable); + /// /// Create and compile mapping functions for mapping from the source type specified by the given /// , for all mapping types (create new, merge, overwrite). Use this diff --git a/AgileMapper/IMappingContext.cs b/AgileMapper/IMappingContext.cs index bafafd714..a247626e1 100644 --- a/AgileMapper/IMappingContext.cs +++ b/AgileMapper/IMappingContext.cs @@ -5,5 +5,7 @@ internal interface IMappingContext MapperContext MapperContext { get; } MappingRuleSet RuleSet { get; } + + bool AddUnsuccessfulMemberPopulations { get; } } } \ No newline at end of file diff --git a/AgileMapper/Mapper.cs b/AgileMapper/Mapper.cs index 4abb699d4..c1e4b22ef 100644 --- a/AgileMapper/Mapper.cs +++ b/AgileMapper/Mapper.cs @@ -1,10 +1,12 @@ namespace AgileObjects.AgileMapper { using System; + using System.Linq; using System.Linq.Expressions; using Api; using Api.Configuration; using Plans; + using Queryables.Api; using Validation; /// @@ -173,6 +175,12 @@ public static IFlatteningSelector Flatten(TSource source) wher IPlanTargetAndRuleSetSelector IMapper.GetPlanFor() => GetPlan(); + IProjectionPlanTargetSelector IMapper.GetPlanForProjecting( + IQueryable exampleQueryable) + { + return new PlanTargetSelector(Context, exampleQueryable); + } + IPlanTargetSelector IMapper.GetPlansFor(TSource exampleInstance) => GetPlan(); IPlanTargetSelector IMapper.GetPlansFor() => GetPlan(); diff --git a/AgileMapper/MapperContext.cs b/AgileMapper/MapperContext.cs index 89992f581..ecac125d7 100644 --- a/AgileMapper/MapperContext.cs +++ b/AgileMapper/MapperContext.cs @@ -28,6 +28,7 @@ public MapperContext() ConstructionFactory = new ComplexTypeConstructionFactory(Cache); ValueConverters = new ConverterSet(UserConfigurations); RuleSets = new MappingRuleSetCollection(); + QueryProjectionMappingContext = new SimpleMappingContext(RuleSets.Project, this); } public CacheSet Cache { get; } @@ -52,6 +53,8 @@ public MapperContext() public MappingRuleSetCollection RuleSets { get; } + public IMappingContext QueryProjectionMappingContext { get; } + public MapperContext Clone() { var context = new MapperContext(); diff --git a/AgileMapper/MappingExecutor.cs b/AgileMapper/MappingExecutor.cs index 7aecbbd8b..88062decb 100644 --- a/AgileMapper/MappingExecutor.cs +++ b/AgileMapper/MappingExecutor.cs @@ -27,6 +27,8 @@ public MappingExecutor(TSource source, MapperContext mapperContext) public MappingRuleSet RuleSet { get; private set; } + public bool AddUnsuccessfulMemberPopulations => false; + #region ToANew Overloads public TResult ToANew() => PerformMapping(MapperContext.RuleSets.CreateNew, default(TResult)); diff --git a/AgileMapper/MappingRuleSet.cs b/AgileMapper/MappingRuleSet.cs index 5ee1b4216..fdcdfdb77 100644 --- a/AgileMapper/MappingRuleSet.cs +++ b/AgileMapper/MappingRuleSet.cs @@ -3,30 +3,35 @@ namespace AgileObjects.AgileMapper using DataSources; using Members.Population; using ObjectPopulation.Enumerables; + using ObjectPopulation.Recursion; internal class MappingRuleSet { public MappingRuleSet( string name, - bool rootHasPopulatedTarget, + MappingRuleSetSettings settings, IEnumerablePopulationStrategy enumerablePopulationStrategy, - IMemberPopulationGuardFactory populationGuardFactory, + IRecursiveMemberMappingStrategy recursiveMemberMappingStrategy, + IMemberPopulationFactory populationFactory, IDataSourceFactory fallbackDataSourceFactory) { Name = name; - RootHasPopulatedTarget = rootHasPopulatedTarget; + Settings = settings; EnumerablePopulationStrategy = enumerablePopulationStrategy; - PopulationGuardFactory = populationGuardFactory; + RecursiveMemberMappingStrategy = recursiveMemberMappingStrategy; + PopulationFactory = populationFactory; FallbackDataSourceFactory = fallbackDataSourceFactory; } public string Name { get; } - public bool RootHasPopulatedTarget { get; } + public MappingRuleSetSettings Settings { get; } public IEnumerablePopulationStrategy EnumerablePopulationStrategy { get; } - public IMemberPopulationGuardFactory PopulationGuardFactory { get; } + public IRecursiveMemberMappingStrategy RecursiveMemberMappingStrategy { get; } + + public IMemberPopulationFactory PopulationFactory { get; } public IDataSourceFactory FallbackDataSourceFactory { get; } } diff --git a/AgileMapper/MappingRuleSetCollection.cs b/AgileMapper/MappingRuleSetCollection.cs index 9ba548543..21012af63 100644 --- a/AgileMapper/MappingRuleSetCollection.cs +++ b/AgileMapper/MappingRuleSetCollection.cs @@ -5,6 +5,8 @@ namespace AgileObjects.AgileMapper using Members.Population; using ObjectPopulation; using ObjectPopulation.Enumerables; + using ObjectPopulation.Recursion; + using Queryables.Recursion; internal class MappingRuleSetCollection { @@ -12,23 +14,69 @@ internal class MappingRuleSetCollection private static readonly MappingRuleSet _createNew = new MappingRuleSet( Constants.CreateNew, - false, - CopySourceEnumerablePopulationStrategy.Instance, - NullMemberPopulationGuardFactory.Instance, + new MappingRuleSetSettings + { + SourceElementsCouldBeNull = true, + UseTryCatch = true, + CheckDerivedSourceTypes = true, + GuardMemberAccesses = value => true, + AllowObjectTracking = true, + AllowGetMethods = true, + AllowSetMethods = true + }, + new CopySourceEnumerablePopulationStrategy(), + MapRecursionCallRecursiveMemberMappingStrategy.Instance, + DefaultMemberPopulationFactory.Instance, ExistingOrDefaultValueDataSourceFactory.Instance); private static readonly MappingRuleSet _merge = new MappingRuleSet( Constants.Merge, - true, - MergeEnumerablePopulationStrategy.Instance, - PreserveExistingValueMemberPopulationGuardFactory.Instance, + new MappingRuleSetSettings + { + RootHasPopulatedTarget = true, + SourceElementsCouldBeNull = true, + UseTryCatch = true, + CheckDerivedSourceTypes = true, + GuardMemberAccesses = value => true, + AllowObjectTracking = true, + AllowGetMethods = true, + AllowSetMethods = true + }, + new MergeEnumerablePopulationStrategy(), + MapRecursionCallRecursiveMemberMappingStrategy.Instance, + new MemberMergePopulationFactory(), ExistingOrDefaultValueDataSourceFactory.Instance); private static readonly MappingRuleSet _overwrite = new MappingRuleSet( Constants.Overwrite, - true, + new MappingRuleSetSettings + { + RootHasPopulatedTarget = true, + SourceElementsCouldBeNull = true, + UseTryCatch = true, + CheckDerivedSourceTypes = true, + GuardMemberAccesses = value => true, + AllowObjectTracking = true, + AllowGetMethods = true, + AllowSetMethods = true + }, OverwriteEnumerablePopulationStrategy.Instance, - NullMemberPopulationGuardFactory.Instance, + MapRecursionCallRecursiveMemberMappingStrategy.Instance, + DefaultMemberPopulationFactory.Instance, + DefaultValueDataSourceFactory.Instance); + + private static readonly MappingRuleSet _project = new MappingRuleSet( + Constants.Project, + new MappingRuleSetSettings + { + UseMemberInitialisation = true, + UseSingleRootMappingExpression = true, + GuardMemberAccesses = value => value.Type.IsComplex(), + AllowEnumerableAssignment = true + }, + new ProjectSourceEnumerablePopulationStrategy(), + new MapToDepthRecursiveMemberMappingStrategy(), + DefaultMemberPopulationFactory.Instance, DefaultValueDataSourceFactory.Instance); #endregion @@ -37,7 +85,7 @@ internal class MappingRuleSetCollection public MappingRuleSetCollection() { - _ruleSets = new List { CreateNew, Merge, Overwrite }; + _ruleSets = new List { CreateNew, Merge, Overwrite, Project }; } public IEnumerable All => _ruleSets; @@ -48,6 +96,8 @@ public MappingRuleSetCollection() public MappingRuleSet Overwrite => _overwrite; + public MappingRuleSet Project => _project; + public MappingRuleSet GetByName(string name) => _ruleSets.First(rs => rs.Name == name); } } \ No newline at end of file diff --git a/AgileMapper/MappingRuleSetSettings.cs b/AgileMapper/MappingRuleSetSettings.cs new file mode 100644 index 000000000..6f858924e --- /dev/null +++ b/AgileMapper/MappingRuleSetSettings.cs @@ -0,0 +1,31 @@ +namespace AgileObjects.AgileMapper +{ + using System; + using System.Linq.Expressions; + using Members; + + internal class MappingRuleSetSettings + { + public bool RootHasPopulatedTarget { get; set; } + + public bool SourceElementsCouldBeNull { get; set; } + + public bool UseSingleRootMappingExpression { get; set; } + + public bool UseMemberInitialisation { get; set; } + + public bool UseTryCatch { get; set; } + + public bool CheckDerivedSourceTypes { get; set; } + + public Func GuardMemberAccesses { get; set; } + + public bool AllowEnumerableAssignment { get; set; } + + public bool AllowObjectTracking { get; set; } + + public bool AllowGetMethods { get; set; } + + public bool AllowSetMethods { get; set; } + } +} \ No newline at end of file diff --git a/AgileMapper/Members/ExpressionInfoFinder.cs b/AgileMapper/Members/ExpressionInfoFinder.cs index 9b511d2e1..84ab593ae 100644 --- a/AgileMapper/Members/ExpressionInfoFinder.cs +++ b/AgileMapper/Members/ExpressionInfoFinder.cs @@ -11,6 +11,9 @@ namespace AgileObjects.AgileMapper.Members internal class ExpressionInfoFinder { + public static readonly ExpressionInfo EmptyExpressionInfo = + new ExpressionInfo(null, Enumerable.EmptyArray); + private readonly Expression _mappingDataObject; public ExpressionInfoFinder(Expression mappingDataObject) @@ -29,39 +32,37 @@ public ExpressionInfo FindIn(Expression expression, bool targetCanBeNull) private class ExpressionInfoFinderInstance : ExpressionVisitor { private readonly Expression _mappingDataObject; + private readonly bool _includeTargetNullChecking; private readonly ICollection _stringMemberAccessSubjects; private readonly ICollection _allInvocations; private readonly ICollection _multiInvocations; private readonly ICollection _nullCheckSubjects; private readonly Dictionary _nestedAccessesByPath; - private readonly bool _includeTargetNullChecking; - public ExpressionInfoFinderInstance( - Expression mappingDataObject, - bool targetCanBeNull) + public ExpressionInfoFinderInstance(Expression mappingDataObject, bool targetCanBeNull) { _mappingDataObject = mappingDataObject; + _includeTargetNullChecking = targetCanBeNull; _stringMemberAccessSubjects = new List(); _allInvocations = new List(); _multiInvocations = new List(); _nullCheckSubjects = new List(); _nestedAccessesByPath = new Dictionary(); - _includeTargetNullChecking = targetCanBeNull; } public ExpressionInfo FindIn(Expression expression) { Visit(expression); - var nestedAccesses = _nestedAccessesByPath.None() - ? Enumerable.EmptyArray - : _nestedAccessesByPath.Values.Reverse().ToArray(); + var nestedAccessChecks = _nestedAccessesByPath.Any() + ? _nestedAccessesByPath.Values.Reverse().ToArray().GetIsNotDefaultComparisonsOrNull() + : null; var multiInvocations = _multiInvocations .OrderBy(inv => inv.ToString()) .ToArray(); - return new ExpressionInfo(nestedAccesses, multiInvocations); + return new ExpressionInfo(nestedAccessChecks, multiInvocations); } protected override Expression VisitBinary(BinaryExpression binary) @@ -121,6 +122,7 @@ private bool IsNotRootObject(MemberExpression memberAccess) { if (memberAccess.Member.Name == "Parent") { + // ReSharper disable once PossibleNullReferenceException return !memberAccess.Member.DeclaringType.Name .StartsWith(nameof(IMappingData), StringComparison.Ordinal); } @@ -150,6 +152,11 @@ private static bool IsNullableHasValueAccess(MemberExpression memberAccess) (memberAccess.Expression.Type.IsNullableType()); } + protected override MemberBinding VisitMemberBinding(MemberBinding binding) + { + return base.VisitMemberBinding(binding); + } + protected override Expression VisitMethodCall(MethodCallExpression methodCall) { if ((methodCall.Object != _mappingDataObject) && @@ -182,7 +189,7 @@ private void AddExistingNullCheck(Expression checkedAccess) private void AddStringMemberAccessSubjectIfAppropriate(Expression member) { - if ((member != null) && (member.Type == typeof(string)) && AccessSubjectCouldBeNull(member)) + if ((member?.Type == typeof(string)) && AccessSubjectCouldBeNull(member)) { _stringMemberAccessSubjects.Add(member); } @@ -293,14 +300,14 @@ private static bool IsNonNullReturnMethodCall(Expression memberAccess) public class ExpressionInfo { public ExpressionInfo( - IList nestedAccesses, + Expression nestedAccessChecks, IList multiInvocations) { - NestedAccesses = nestedAccesses; + NestedAccessChecks = nestedAccessChecks; MultiInvocations = multiInvocations; } - public IList NestedAccesses { get; } + public Expression NestedAccessChecks { get; } public IList MultiInvocations { get; } } diff --git a/AgileMapper/Members/IChildMemberMappingData.cs b/AgileMapper/Members/IChildMemberMappingData.cs index 51b6c6fb7..9c4d0b113 100644 --- a/AgileMapper/Members/IChildMemberMappingData.cs +++ b/AgileMapper/Members/IChildMemberMappingData.cs @@ -1,7 +1,6 @@ namespace AgileObjects.AgileMapper.Members { using System; - using System.Linq.Expressions; using ObjectPopulation; internal interface IChildMemberMappingData @@ -14,10 +13,4 @@ internal interface IChildMemberMappingData Type GetSourceMemberRuntimeType(IQualifiedMember sourceMember); } - - internal static class ChildMemberMappingDataExtensions - { - public static Expression GetRuleSetPopulationGuardOrNull(this IChildMemberMappingData childMappingData) - => childMappingData.RuleSet.PopulationGuardFactory.GetPopulationGuardOrNull(childMappingData.MapperData); - } } \ No newline at end of file diff --git a/AgileMapper/Members/MemberMapperDataExtensions.cs b/AgileMapper/Members/MemberMapperDataExtensions.cs index 2d259a7b7..d3e2234c9 100644 --- a/AgileMapper/Members/MemberMapperDataExtensions.cs +++ b/AgileMapper/Members/MemberMapperDataExtensions.cs @@ -19,16 +19,17 @@ public static bool IsStandalone(this IObjectMappingData mappingData) => mappingData.IsRoot || mappingData.MapperKey.MappingTypes.RuntimeTypesNeeded; public static bool TargetTypeIsEntity(this IMemberMapperData mapperData) - => IsEntity(mapperData, mapperData.TargetType); + => IsEntity(mapperData, mapperData.TargetType, out var _); - public static bool IsEntity(this IMemberMapperData mapperData, Type type) + public static bool IsEntity(this IMemberMapperData mapperData, Type type, out Member idMember) { if (type == null) { + idMember = null; return false; } - var idMember = mapperData + idMember = mapperData .MapperContext .Naming .GetIdentifierOrNull(TypeKey.ForTypeId(type)); @@ -36,6 +37,15 @@ public static bool IsEntity(this IMemberMapperData mapperData, Type type) return idMember?.IsEntityId() == true; } + public static bool UseSingleMappingExpression(this IBasicMapperData mapperData) + => mapperData.IsRoot && mapperData.RuleSet.Settings.UseSingleRootMappingExpression; + + public static bool UseMemberInitialisations(this IMemberMapperData mapperData) + => mapperData.RuleSet.Settings.UseMemberInitialisation || mapperData.Context.IsPartOfUserStructMapping(); + + public static bool MapToNullCollections(this IMemberMapperData mapperData) + => mapperData.MapperContext.UserConfigurations.MapToNullCollections(mapperData); + public static IMemberMapperData GetRootMapperData(this IMemberMapperData mapperData) { while (!mapperData.IsRoot) @@ -77,15 +87,12 @@ public static bool TargetCouldBePopulated(this IMemberMapperData mapperData) public static bool TargetIsDefinitelyPopulated(this IBasicMapperData mapperData) { - return mapperData.RuleSet.RootHasPopulatedTarget && + return mapperData.RuleSet.Settings.RootHasPopulatedTarget && (mapperData.IsRoot || mapperData.TargetMemberIsUserStruct()); } public static bool TargetIsDefinitelyUnpopulated(this IMemberMapperData mapperData) - { - return mapperData.Context.IsForNewElement || - (mapperData.TargetMember.IsRoot && !mapperData.RuleSet.RootHasPopulatedTarget); - } + => mapperData.Context.IsForNewElement || (mapperData.TargetMember.IsRoot && !mapperData.RuleSet.Settings.RootHasPopulatedTarget); public static bool HasSameSourceAsParent(this IMemberMapperData mapperData) { @@ -120,28 +127,9 @@ public static ExpressionInfoFinder.ExpressionInfo GetExpressionInfoFor( Expression value, bool targetCanBeNull) { - return mapperData.ExpressionInfoFinder.FindIn(value, targetCanBeNull); - } - - public static bool DoNotMapRecursion(this IMemberMapperData mapperData) - { - if (mapperData.SourceType.IsDictionary()) - { - return true; - } - - while (mapperData != null) - { - if (mapperData.TargetType.Name.EndsWith("Dto", StringComparison.Ordinal) || - mapperData.TargetType.Name.EndsWith("DataTransferObject", StringComparison.Ordinal)) - { - return true; - } - - mapperData = mapperData.Parent; - } - - return false; + return mapperData.RuleSet.Settings.GuardMemberAccesses(value) + ? mapperData.ExpressionInfoFinder.FindIn(value, targetCanBeNull) + : ExpressionInfoFinder.EmptyExpressionInfo; } public static bool SourceMemberIsStringKeyedDictionary( @@ -178,6 +166,13 @@ public static void RegisterTargetMemberDataSourcesIfRequired( public static bool TargetMemberIsUnmappable(this IMemberMapperData mapperData, out string reason) { + if (!mapperData.RuleSet.Settings.AllowSetMethods && + (mapperData.TargetMember.LeafMember.MemberType == MemberType.SetMethod)) + { + reason = "Set methods are unsupported by rule set '" + mapperData.RuleSet.Name + "'"; + return true; + } + return TargetMemberIsUnmappable( mapperData, mapperData.TargetMember, @@ -215,7 +210,7 @@ public static bool TargetMemberIsEnumerableElement(this IBasicMapperData mapperD [DebuggerStepThrough] public static bool TargetMemberHasInitAccessibleValue(this IMemberMapperData mapperData) - => mapperData.TargetMember.IsReadable && !mapperData.Context.IsPartOfUserStructMapping; + => mapperData.TargetMember.IsReadable && !mapperData.Context.IsPartOfUserStructMapping(); [DebuggerStepThrough] public static bool TargetMemberIsUserStruct(this IBasicMapperData mapperData) @@ -294,7 +289,6 @@ private static bool TargetMemberIsRecursionWithin( public static Expression GetFallbackCollectionValue(this IMemberMapperData mapperData) { var targetMember = mapperData.TargetMember; - var mapToNullCollections = mapperData.MapperContext.UserConfigurations.MapToNullCollections(mapperData); Expression emptyEnumerable; @@ -302,7 +296,7 @@ public static Expression GetFallbackCollectionValue(this IMemberMapperData mappe { var existingValue = mapperData.GetTargetMemberAccess(); - if (mapToNullCollections) + if (mapperData.MapToNullCollections()) { return existingValue; } @@ -312,7 +306,7 @@ public static Expression GetFallbackCollectionValue(this IMemberMapperData mappe return Expression.Coalesce(existingValue, emptyEnumerable); } - if (mapToNullCollections) + if (mapperData.MapToNullCollections()) { return targetMember.Type.ToDefaultExpression(); } @@ -448,8 +442,8 @@ public static Expression GetTypedContextAccess( private static Expression GetFinalContextAccess( Expression contextAccess, - Type[] contextTypes, - Type[] contextAccessTypes = null) + IList contextTypes, + IList contextAccessTypes = null) { if ((contextAccessTypes == null) && !contextAccess.Type.IsGenericType()) { @@ -470,9 +464,7 @@ private static Expression GetFinalContextAccess( } public static Expression GetTargetMemberPopulation(this IMemberMapperData mapperData, Expression value) - { - return mapperData.TargetMember.GetPopulation(value, mapperData); - } + => mapperData.TargetMember.GetPopulation(value, mapperData); public static Expression GetAsCall(this IMemberMapperData mapperData, Type sourceType, Type targetType) => GetAsCall(mapperData.MappingDataObject, sourceType, targetType); diff --git a/AgileMapper/Members/MemberTypeExtensions.cs b/AgileMapper/Members/MemberTypeExtensions.cs index dfec01b0a..693ab6f14 100644 --- a/AgileMapper/Members/MemberTypeExtensions.cs +++ b/AgileMapper/Members/MemberTypeExtensions.cs @@ -13,5 +13,17 @@ public static bool IsReadable(this MemberType memberType) return true; } + + public static bool IsMethod(this MemberType memberType) + { + switch (memberType) + { + case MemberType.GetMethod: + case MemberType.SetMethod: + return true; + } + + return false; + } } } \ No newline at end of file diff --git a/AgileMapper/Members/Population/DefaultMemberPopulationFactory.cs b/AgileMapper/Members/Population/DefaultMemberPopulationFactory.cs new file mode 100644 index 000000000..1c13a98fc --- /dev/null +++ b/AgileMapper/Members/Population/DefaultMemberPopulationFactory.cs @@ -0,0 +1,36 @@ +namespace AgileObjects.AgileMapper.Members.Population +{ + using System.Linq.Expressions; + using Extensions.Internal; + + internal class DefaultMemberPopulationFactory : MemberPopulationFactoryBase + { + public static readonly IMemberPopulationFactory Instance = new DefaultMemberPopulationFactory(); + + protected override Expression GetPopulationGuard(IMemberPopulationContext context) + => context.PopulateCondition; + + protected override Expression GetGuardedBindingValue(Expression bindingValue, Expression populationGuard) + { + if (populationGuard == null) + { + return bindingValue; + } + + return Expression.Condition( + populationGuard, + bindingValue, + bindingValue.Type.ToDefaultExpression()); + } + + public override Expression GetGuardedPopulation( + Expression population, + Expression populationGuard, + bool useSingleExpression) + { + return useSingleExpression + ? population + : base.GetGuardedPopulation(population, populationGuard, false); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Members/Population/IMemberPopulationContext.cs b/AgileMapper/Members/Population/IMemberPopulationContext.cs new file mode 100644 index 000000000..da75c487f --- /dev/null +++ b/AgileMapper/Members/Population/IMemberPopulationContext.cs @@ -0,0 +1,16 @@ +namespace AgileObjects.AgileMapper.Members.Population +{ + using System.Linq.Expressions; + using DataSources; + + internal interface IMemberPopulationContext + { + IMemberMapperData MapperData { get; } + + bool IsSuccessful { get; } + + DataSourceSet DataSources { get; } + + Expression PopulateCondition { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/Members/Population/IMemberPopulationFactory.cs b/AgileMapper/Members/Population/IMemberPopulationFactory.cs new file mode 100644 index 000000000..bec1b445e --- /dev/null +++ b/AgileMapper/Members/Population/IMemberPopulationFactory.cs @@ -0,0 +1,9 @@ +namespace AgileObjects.AgileMapper.Members.Population +{ + using System.Linq.Expressions; + + internal interface IMemberPopulationFactory + { + Expression GetPopulation(IMemberPopulationContext context); + } +} \ No newline at end of file diff --git a/AgileMapper/Members/Population/IMemberPopulationGuardFactory.cs b/AgileMapper/Members/Population/IMemberPopulationGuardFactory.cs deleted file mode 100644 index 3a94a10e1..000000000 --- a/AgileMapper/Members/Population/IMemberPopulationGuardFactory.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace AgileObjects.AgileMapper.Members.Population -{ - using System.Linq.Expressions; - using Members; - - internal interface IMemberPopulationGuardFactory - { - Expression GetPopulationGuardOrNull(IMemberMapperData mapperData); - } -} \ No newline at end of file diff --git a/AgileMapper/Members/Population/IMemberPopulation.cs b/AgileMapper/Members/Population/IMemberPopulator.cs similarity index 71% rename from AgileMapper/Members/Population/IMemberPopulation.cs rename to AgileMapper/Members/Population/IMemberPopulator.cs index a5b2d6015..3dd455d80 100644 --- a/AgileMapper/Members/Population/IMemberPopulation.cs +++ b/AgileMapper/Members/Population/IMemberPopulator.cs @@ -2,11 +2,11 @@ namespace AgileObjects.AgileMapper.Members.Population { using System.Linq.Expressions; - internal interface IMemberPopulation + internal interface IMemberPopulator { IMemberMapperData MapperData { get; } - bool IsSuccessful { get; } + bool CanPopulate { get; } Expression GetPopulation(); } diff --git a/AgileMapper/Members/Population/MemberMergePopulationFactory.cs b/AgileMapper/Members/Population/MemberMergePopulationFactory.cs new file mode 100644 index 000000000..c349a7059 --- /dev/null +++ b/AgileMapper/Members/Population/MemberMergePopulationFactory.cs @@ -0,0 +1,54 @@ +namespace AgileObjects.AgileMapper.Members.Population +{ + using System.Linq.Expressions; + + internal class MemberMergePopulationFactory : MemberPopulationFactoryBase + { + protected override Expression GetPopulationGuard(IMemberPopulationContext context) + { + var mapperData = context.MapperData; + var populateCondition = context.PopulateCondition; + + if (SkipPopulationGuarding(mapperData)) + { + return populateCondition; + } + + var existingValueIsDefault = mapperData.TargetMember.GetHasDefaultValueCheck(mapperData); + + if (populateCondition == null) + { + return existingValueIsDefault; + } + + return Expression.AndAlso(populateCondition, existingValueIsDefault); + } + + private static bool SkipPopulationGuarding(IBasicMapperData mapperData) + { + var targetMember = mapperData.TargetMember; + + if (!targetMember.IsReadable) + { + return true; + } + + if (targetMember.IsSimple) + { + return false; + } + + if (targetMember.Type != typeof(object)) + { + return true; + } + + var skipObjectValueGuarding = !targetMember.GuardObjectValuePopulations; + + return skipObjectValueGuarding; + } + + protected override Expression GetGuardedBindingValue(Expression bindingValue, Expression populationGuard) + => bindingValue; + } +} \ No newline at end of file diff --git a/AgileMapper/Members/Population/MemberPopulation.cs b/AgileMapper/Members/Population/MemberPopulation.cs deleted file mode 100644 index bbfca6153..000000000 --- a/AgileMapper/Members/Population/MemberPopulation.cs +++ /dev/null @@ -1,165 +0,0 @@ -namespace AgileObjects.AgileMapper.Members.Population -{ - using System; - using System.Linq; - using System.Linq.Expressions; - using Configuration; - using DataSources; - using Extensions.Internal; - using ReadableExpressions; - - internal class MemberPopulation : IMemberPopulation - { - private readonly DataSourceSet _dataSources; - private readonly Expression _populateCondition; - - private MemberPopulation( - IMemberMapperData mapperData, - DataSourceSet dataSources, - Expression populateCondition = null) - { - MapperData = mapperData; - _dataSources = dataSources; - _populateCondition = populateCondition; - } - - #region Factory Methods - - public static IMemberPopulation WithRegistration( - IChildMemberMappingData mappingData, - DataSourceSet dataSources, - Expression populateCondition) - { - var memberPopulation = WithoutRegistration(mappingData, dataSources, populateCondition); - - memberPopulation.MapperData.RegisterTargetMemberDataSourcesIfRequired(dataSources); - - return memberPopulation; - } - - public static IMemberPopulation WithoutRegistration( - IChildMemberMappingData mappingData, - DataSourceSet dataSources, - Expression populateCondition = null) - { - if (!dataSources.None) - { - populateCondition = GetPopulateCondition(populateCondition, mappingData); - } - - return new MemberPopulation(mappingData.MapperData, dataSources, populateCondition); - } - - private static Expression GetPopulateCondition(Expression populateCondition, IChildMemberMappingData mappingData) - { - var populationGuard = mappingData.GetRuleSetPopulationGuardOrNull(); - - if (populationGuard == null) - { - return populateCondition; - } - - if (populateCondition == null) - { - return populationGuard; - } - - return Expression.AndAlso(populateCondition, populationGuard); - } - - public static IMemberPopulation Unmappable(IMemberMapperData mapperData, string reason) - => CreateNullMemberPopulation(mapperData, targetMember => $"No way to populate {targetMember.Name} ({reason})"); - - public static IMemberPopulation IgnoredMember(IMemberMapperData mapperData, ConfiguredIgnoredMember configuredIgnore) - => CreateNullMemberPopulation(mapperData, configuredIgnore.GetIgnoreMessage); - - public static IMemberPopulation NoDataSource(IMemberMapperData mapperData) - { - var noDataSources = CreateNullDataSourceSet(mapperData, GetNoDataSourceMessage); - - mapperData.RegisterTargetMemberDataSourcesIfRequired(noDataSources); - - return new MemberPopulation(mapperData, noDataSources); - } - - private static string GetNoDataSourceMessage(QualifiedMember targetMember) - { - return targetMember.IsSimple - ? "No data source for " + targetMember.Name - : $"No data source for {targetMember.Name} or any of its child members"; - } - - private static MemberPopulation CreateNullMemberPopulation( - IMemberMapperData mapperData, - Func commentFactory) - { - return new MemberPopulation(mapperData, CreateNullDataSourceSet(mapperData, commentFactory)); - } - - private static DataSourceSet CreateNullDataSourceSet( - IBasicMapperData mapperData, - Func commentFactory) - { - return new DataSourceSet( - new NullDataSource( - ReadableExpression.Comment(commentFactory.Invoke(mapperData.TargetMember)))); - } - - #endregion - - public IMemberMapperData MapperData { get; } - - public bool IsSuccessful => _dataSources.HasValue; - - public Expression GetPopulation() - { - if (!IsSuccessful) - { - return _dataSources.GetValueExpression(); - } - - var population = MapperData.Context.IsPartOfUserStructMapping - ? GetBinding() - : MapperData.TargetMember.IsReadOnly - ? GetReadOnlyMemberPopulation() - : _dataSources.GetPopulationExpression(MapperData); - - if (_dataSources.Variables.Any()) - { - population = Expression.Block(_dataSources.Variables, population); - } - - if (_populateCondition != null) - { - population = Expression.IfThen(_populateCondition, population); - } - - return population; - } - - private Expression GetBinding() - { - var bindingValue = _dataSources.GetValueExpression(); - var binding = MapperData.GetTargetMemberPopulation(bindingValue); - - return binding; - } - - private Expression GetReadOnlyMemberPopulation() - { - var dataSourcesValue = _dataSources.GetValueExpression(); - var targetMemberAccess = MapperData.GetTargetMemberAccess(); - var targetMemberNotNull = targetMemberAccess.GetIsNotDefaultComparison(); - - return Expression.IfThen(targetMemberNotNull, dataSourcesValue); - } - - #region ExcludeFromCodeCoverage -#if DEBUG - [ExcludeFromCodeCoverage] -#endif - #endregion - public override string ToString() - => $"{MapperData.TargetMember} ({_dataSources.Count()} data source(s))"; - } -} \ No newline at end of file diff --git a/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs b/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs new file mode 100644 index 000000000..a92dab789 --- /dev/null +++ b/AgileMapper/Members/Population/MemberPopulationFactoryBase.cs @@ -0,0 +1,64 @@ +namespace AgileObjects.AgileMapper.Members.Population +{ + using System.Linq.Expressions; + using Extensions.Internal; + + internal abstract class MemberPopulationFactoryBase : IMemberPopulationFactory + { + public Expression GetPopulation(IMemberPopulationContext context) + { + if (!context.IsSuccessful) + { + return context.DataSources.GetValueExpression(); + } + + var useSingleExpression = context.MapperData.UseMemberInitialisations(); + var populationGuard = GetPopulationGuard(context); + + var population = useSingleExpression + ? GetBinding(context, populationGuard) + : context.MapperData.TargetMember.IsReadOnly + ? GetReadOnlyMemberPopulation(context) + : context.DataSources.GetPopulationExpression(); + + if (context.DataSources.Variables.Any()) + { + population = Expression.Block(context.DataSources.Variables, population); + } + + return GetGuardedPopulation(population, populationGuard, useSingleExpression); + } + + protected abstract Expression GetPopulationGuard(IMemberPopulationContext context); + + private Expression GetBinding(IMemberPopulationContext context, Expression populationGuard) + { + var bindingValue = context.DataSources.GetValueExpression(); + var guardedBindingValue = GetGuardedBindingValue(bindingValue, populationGuard); + var binding = context.MapperData.GetTargetMemberPopulation(guardedBindingValue); + + return binding; + } + + protected abstract Expression GetGuardedBindingValue(Expression bindingValue, Expression populationGuard); + + private static Expression GetReadOnlyMemberPopulation(IMemberPopulationContext context) + { + var dataSourcesValue = context.DataSources.GetValueExpression(); + var targetMemberAccess = context.MapperData.GetTargetMemberAccess(); + var targetMemberNotNull = targetMemberAccess.GetIsNotDefaultComparison(); + + return Expression.IfThen(targetMemberNotNull, dataSourcesValue); + } + + public virtual Expression GetGuardedPopulation( + Expression population, + Expression populationGuard, + bool useSingleExpression) + { + return (populationGuard != null) + ? Expression.IfThen(populationGuard, population) + : population; + } + } +} \ No newline at end of file diff --git a/AgileMapper/Members/Population/MemberPopulator.cs b/AgileMapper/Members/Population/MemberPopulator.cs new file mode 100644 index 000000000..4ef9a06a1 --- /dev/null +++ b/AgileMapper/Members/Population/MemberPopulator.cs @@ -0,0 +1,102 @@ +namespace AgileObjects.AgileMapper.Members.Population +{ + using System; + using System.Linq; + using System.Linq.Expressions; + using Configuration; + using DataSources; + using ReadableExpressions; + + internal class MemberPopulator : IMemberPopulationContext, IMemberPopulator + { + private MemberPopulator(DataSourceSet dataSources, Expression populateCondition = null) + { + DataSources = dataSources; + PopulateCondition = populateCondition; + } + + #region Factory Methods + + public static IMemberPopulator WithRegistration( + IChildMemberMappingData mappingData, + DataSourceSet dataSources, + Expression populateCondition) + { + var memberPopulation = WithoutRegistration(mappingData, dataSources, populateCondition); + + memberPopulation.MapperData.RegisterTargetMemberDataSourcesIfRequired(dataSources); + + return memberPopulation; + } + + public static IMemberPopulator WithoutRegistration( + IChildMemberMappingData mappingData, + DataSourceSet dataSources, + Expression populateCondition = null) + { + return new MemberPopulator(dataSources, populateCondition); + } + + public static IMemberPopulator Unmappable(IMemberMapperData mapperData, string reason) + => CreateNullMemberPopulation(mapperData, targetMember => $"No way to populate {targetMember.Name} ({reason})"); + + public static IMemberPopulator IgnoredMember(IMemberMapperData mapperData, ConfiguredIgnoredMember configuredIgnore) + => CreateNullMemberPopulation(mapperData, configuredIgnore.GetIgnoreMessage); + + public static IMemberPopulator NoDataSource(IMemberMapperData mapperData) + { + var noDataSources = CreateNullDataSourceSet(mapperData, GetNoDataSourceMessage); + + mapperData.RegisterTargetMemberDataSourcesIfRequired(noDataSources); + + return new MemberPopulator(noDataSources); + } + + private static string GetNoDataSourceMessage(QualifiedMember targetMember) + { + return targetMember.IsSimple + ? "No data source for " + targetMember.Name + : $"No data source for {targetMember.Name} or any of its child members"; + } + + private static MemberPopulator CreateNullMemberPopulation( + IMemberMapperData mapperData, + Func commentFactory) + { + return new MemberPopulator(CreateNullDataSourceSet(mapperData, commentFactory)); + } + + private static DataSourceSet CreateNullDataSourceSet( + IMemberMapperData mapperData, + Func commentFactory) + { + return new DataSourceSet( + mapperData, + new NullDataSource( + ReadableExpression.Comment(commentFactory.Invoke(mapperData.TargetMember)))); + } + + #endregion + + public IMemberMapperData MapperData => DataSources.MapperData; + + public bool IsSuccessful => CanPopulate; + + public bool CanPopulate => DataSources.HasValue; + + public DataSourceSet DataSources { get; } + + public Expression PopulateCondition { get; } + + public Expression GetPopulation() + => MapperData.RuleSet.PopulationFactory.GetPopulation(this); + + #region ExcludeFromCodeCoverage +#if DEBUG + [ExcludeFromCodeCoverage] +#endif + #endregion + public override string ToString() + => $"{MapperData.TargetMember} ({DataSources.Count()} data source(s))"; + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/MemberPopulationFactory.cs b/AgileMapper/Members/Population/MemberPopulatorFactory.cs similarity index 60% rename from AgileMapper/ObjectPopulation/MemberPopulationFactory.cs rename to AgileMapper/Members/Population/MemberPopulatorFactory.cs index 27f7d2dae..be52511de 100644 --- a/AgileMapper/ObjectPopulation/MemberPopulationFactory.cs +++ b/AgileMapper/Members/Population/MemberPopulatorFactory.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.ObjectPopulation +namespace AgileObjects.AgileMapper.Members.Population { using System; using System.Collections.Generic; @@ -6,12 +6,13 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System.Linq.Expressions; using Configuration; using DataSources; + using Extensions.Internal; using Members; - using Members.Population; + using ObjectPopulation; - internal class MemberPopulationFactory + internal class MemberPopulatorFactory { - public static readonly MemberPopulationFactory Default = new MemberPopulationFactory(mapperData => + public static readonly MemberPopulatorFactory Default = new MemberPopulatorFactory(mapperData => GlobalContext.Instance .MemberCache .GetTargetMembers(mapperData.TargetType) @@ -19,25 +20,37 @@ internal class MemberPopulationFactory private readonly Func> _targetMembersFactory; - public MemberPopulationFactory(Func> targetMembersFactory) + public MemberPopulatorFactory(Func> targetMembersFactory) { _targetMembersFactory = targetMembersFactory; } - public IEnumerable Create(IObjectMappingData mappingData) + public IEnumerable Create(IObjectMappingData mappingData) { return _targetMembersFactory .Invoke(mappingData.MapperData) - .Select(tm => Create(tm, mappingData)); + .Select(tm => + { + var memberPopulation = Create(tm, mappingData); + + if (memberPopulation.CanPopulate || + mappingData.MappingContext.AddUnsuccessfulMemberPopulations) + { + return memberPopulation; + } + + return null; + }) + .WhereNotNull(); } - private static IMemberPopulation Create(QualifiedMember targetMember, IObjectMappingData mappingData) + private static IMemberPopulator Create(QualifiedMember targetMember, IObjectMappingData mappingData) { var childMapperData = new ChildMemberMapperData(targetMember, mappingData.MapperData); if (childMapperData.TargetMemberIsUnmappable(out var reason)) { - return MemberPopulation.Unmappable(childMapperData, reason); + return MemberPopulator.Unmappable(childMapperData, reason); } if (TargetMemberIsUnconditionallyIgnored( @@ -45,7 +58,7 @@ private static IMemberPopulation Create(QualifiedMember targetMember, IObjectMap out var configuredIgnore, out var populateCondition)) { - return MemberPopulation.IgnoredMember(childMapperData, configuredIgnore); + return MemberPopulator.IgnoredMember(childMapperData, configuredIgnore); } var childMappingData = mappingData.GetChildMappingData(childMapperData); @@ -53,10 +66,10 @@ private static IMemberPopulation Create(QualifiedMember targetMember, IObjectMap if (dataSources.None) { - return MemberPopulation.NoDataSource(childMapperData); + return MemberPopulator.NoDataSource(childMapperData); } - return MemberPopulation.WithRegistration(childMappingData, dataSources, populateCondition); + return MemberPopulator.WithRegistration(childMappingData, dataSources, populateCondition); } private static bool TargetMemberIsUnconditionallyIgnored( diff --git a/AgileMapper/Members/Population/NullMemberPopulationGuardFactory.cs b/AgileMapper/Members/Population/NullMemberPopulationGuardFactory.cs deleted file mode 100644 index cefd003e6..000000000 --- a/AgileMapper/Members/Population/NullMemberPopulationGuardFactory.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace AgileObjects.AgileMapper.Members.Population -{ - using System.Linq.Expressions; - using Members; - - internal class NullMemberPopulationGuardFactory : IMemberPopulationGuardFactory - { - public static readonly IMemberPopulationGuardFactory Instance = new NullMemberPopulationGuardFactory(); - - public Expression GetPopulationGuardOrNull(IMemberMapperData mapperData) => null; - } -} \ No newline at end of file diff --git a/AgileMapper/Members/Population/PreserveExistingValueMemberPopulationGuardFactory.cs b/AgileMapper/Members/Population/PreserveExistingValueMemberPopulationGuardFactory.cs deleted file mode 100644 index dc544e92f..000000000 --- a/AgileMapper/Members/Population/PreserveExistingValueMemberPopulationGuardFactory.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace AgileObjects.AgileMapper.Members.Population -{ - using System.Linq.Expressions; - using Members; - - internal class PreserveExistingValueMemberPopulationGuardFactory : IMemberPopulationGuardFactory - { - public static readonly IMemberPopulationGuardFactory Instance = new PreserveExistingValueMemberPopulationGuardFactory(); - - public Expression GetPopulationGuardOrNull(IMemberMapperData mapperData) - { - if (SkipPopulateCondition(mapperData)) - { - return null; - } - - var existingValueIsDefault = mapperData.TargetMember.GetHasDefaultValueCheck(mapperData); - - return existingValueIsDefault; - } - - private static bool SkipPopulateCondition(IBasicMapperData mapperData) - { - if (!mapperData.TargetMember.IsReadable) - { - return true; - } - - if (mapperData.TargetMember.IsSimple) - { - return false; - } - - if (mapperData.TargetMember.Type != typeof(object)) - { - return true; - } - - var skipObjectValueGuarding = !mapperData.TargetMember.GuardObjectValuePopulations; - - return skipObjectValueGuarding; - } - } -} \ No newline at end of file diff --git a/AgileMapper/Members/SourceMemberMatcher.cs b/AgileMapper/Members/SourceMemberMatcher.cs index 8372046be..5905e5d05 100644 --- a/AgileMapper/Members/SourceMemberMatcher.cs +++ b/AgileMapper/Members/SourceMemberMatcher.cs @@ -70,8 +70,9 @@ private static bool ExactMatchingSourceMemberExists( { var sourceMember = QuerySourceMembers( parentSourceMember, - m => targetData.MapperData.TargetMember.LeafMember.Equals(m) || - targetData.MapperData.TargetMember.JoinedNames.Match(new[] { m.Name })) + targetData, + (m, data) => data.MapperData.TargetMember.LeafMember.Equals(m) || + data.MapperData.TargetMember.JoinedNames.Match(new[] { m.Name })) .FirstOrDefault(); if ((sourceMember == null) || @@ -87,13 +88,18 @@ private static bool ExactMatchingSourceMemberExists( private static IEnumerable QuerySourceMembers( IQualifiedMember parentMember, - Func filter) + IChildMemberMappingData mappingData, + Func filter) { - return GlobalContext + var members = GlobalContext .Instance .MemberCache .GetSourceMembers(parentMember.Type) - .Where(filter); + .Where(m => filter.Invoke(m, mappingData)); + + return mappingData.RuleSet.Settings.AllowGetMethods + ? members + : members.Where(m => m.MemberType != MemberType.GetMethod); } private static IQualifiedMember GetFinalSourceMember( @@ -133,7 +139,8 @@ private static IEnumerable EnumerateSourceMembers( var relevantSourceMembers = QuerySourceMembers( parentMember, - sourceMember => MembersHaveCompatibleTypes(sourceMember, rootData)); + rootData, + MembersHaveCompatibleTypes); foreach (var sourceMember in relevantSourceMembers) { @@ -170,13 +177,9 @@ private static bool MembersHaveCompatibleTypes(Member sourceMember, IChildMember } private static bool IsMatchingMember(IQualifiedMember sourceMember, IMemberMapperData mapperData) - { - return mapperData.TargetMember.Matches(sourceMember) && TypesAreCompatible(sourceMember.Type, mapperData); - } + => mapperData.TargetMember.Matches(sourceMember) && TypesAreCompatible(sourceMember.Type, mapperData); private static bool TypesAreCompatible(Type sourceType, IMemberMapperData mapperData) - { - return mapperData.MapperContext.ValueConverters.CanConvert(sourceType, mapperData.TargetMember.Type); - } + => mapperData.MapperContext.ValueConverters.CanConvert(sourceType, mapperData.TargetMember.Type); } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs index 9d0543d70..3c24e12e6 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeConstructionFactory.cs @@ -167,7 +167,7 @@ private static ConstructorData CreateConstructorData(ConstructorInfo ctor, Const #region Helper Classes - private class ConstructionKey : SourceMemberTypeDependentKeyBase + private class ConstructionKey : SourceMemberTypeDependentKeyBase, IMappingDataOwner { private readonly MappingRuleSet _ruleSet; private readonly IQualifiedMember _sourceMember; diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs index 0926918e9..76c6f07b1 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/ComplexTypeMappingExpressionFactory.cs @@ -13,14 +13,14 @@ internal class ComplexTypeMappingExpressionFactory : MappingExpressionFactoryBas { public static readonly MappingExpressionFactoryBase Instance = new ComplexTypeMappingExpressionFactory(); - private readonly PopulationExpressionFactoryBase _structPopulationFactory; - private readonly PopulationExpressionFactoryBase _classPopulationFactory; + private readonly PopulationExpressionFactoryBase _memberInitPopulationFactory; + private readonly PopulationExpressionFactoryBase _multiStatementPopulationFactory; private readonly IEnumerable _shortCircuitFactories; private ComplexTypeMappingExpressionFactory() { - _structPopulationFactory = new StructPopulationExpressionFactory(); - _classPopulationFactory = new ClassPopulationExpressionFactory(); + _memberInitPopulationFactory = new MemberInitPopulationExpressionFactory(); + _multiStatementPopulationFactory = new MultiStatementPopulationExpressionFactory(); _shortCircuitFactories = new[] { @@ -36,22 +36,19 @@ protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out { // If a target complex type is readonly or unconstructable // we still try to map to it using an existing non-null value: - nullMappingBlock = null; - return false; + return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); } if (mappingData.MapperData.MapperContext.ConstructionFactory.GetNewObjectCreation(mappingData) != null) { - nullMappingBlock = null; - return false; + return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); } var targetType = mappingData.MapperData.TargetType; if (targetType.IsAbstract() && mappingData.MapperData.GetDerivedTargetTypes().Any()) { - nullMappingBlock = null; - return false; + return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); } nullMappingBlock = Expression.Block( @@ -73,6 +70,7 @@ protected override IEnumerable GetShortCircuitReturns(GotoExpression } var alreadyMappedShortCircuit = GetAlreadyMappedObjectShortCircuitOrNull(mapperData); + if (alreadyMappedShortCircuit != null) { yield return alreadyMappedShortCircuit; @@ -96,7 +94,7 @@ private static bool SourceObjectCouldBeNull(IMemberMapperData mapperData) return false; } - if (mapperData.TargetMemberIsEnumerableElement()) + if (mapperData.RuleSet.Settings.SourceElementsCouldBeNull && mapperData.TargetMemberIsEnumerableElement()) { return !mapperData.HasSameSourceAsParent(); } @@ -106,13 +104,15 @@ private static bool SourceObjectCouldBeNull(IMemberMapperData mapperData) private static Expression GetAlreadyMappedObjectShortCircuitOrNull(ObjectMapperData mapperData) { - if (!mapperData.CacheMappedObjects || mapperData.TargetTypeHasNotYetBeenMapped) + if (!mapperData.RuleSet.Settings.AllowObjectTracking || + !mapperData.CacheMappedObjects || + mapperData.TargetTypeHasNotYetBeenMapped) { return null; } - // ReSharper disable once PossibleNullReferenceException - var tryGetMethod = typeof(IObjectMappingDataUntyped).GetPublicInstanceMethod("TryGet") + var tryGetMethod = typeof(IObjectMappingDataUntyped) + .GetPublicInstanceMethod("TryGet") .MakeGenericMethod(mapperData.SourceType, mapperData.TargetType); var tryGetCall = Expression.Call( @@ -141,13 +141,11 @@ protected override Expression GetDerivedTypeMappings(IObjectMappingData mappingD protected override IEnumerable GetObjectPopulation(IObjectMappingData mappingData) { - var expressionFactory = mappingData.MapperData.TargetMemberIsUserStruct() - ? _structPopulationFactory - : _classPopulationFactory; + var expressionFactory = mappingData.MapperData.UseMemberInitialisations() + ? _memberInitPopulationFactory + : _multiStatementPopulationFactory; return expressionFactory.GetPopulation(mappingData); } - - protected override Expression GetReturnValue(ObjectMapperData mapperData) => mapperData.TargetInstance; } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/StructPopulationExpressionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/MemberInitPopulationExpressionFactory.cs similarity index 89% rename from AgileMapper/ObjectPopulation/ComplexTypes/StructPopulationExpressionFactory.cs rename to AgileMapper/ObjectPopulation/ComplexTypes/MemberInitPopulationExpressionFactory.cs index 26f408b0a..3656ed29c 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/StructPopulationExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/MemberInitPopulationExpressionFactory.cs @@ -5,19 +5,24 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.ComplexTypes using Extensions.Internal; using Members.Population; - internal class StructPopulationExpressionFactory : PopulationExpressionFactoryBase + internal class MemberInitPopulationExpressionFactory : PopulationExpressionFactoryBase { protected override IEnumerable GetPopulationExpressionsFor( - IMemberPopulation memberPopulation, + IMemberPopulator memberPopulator, IObjectMappingData mappingData) { - yield return memberPopulation.GetPopulation(); + yield return memberPopulator.GetPopulation(); } protected override Expression GetNewObjectCreation(IObjectMappingData mappingData, IList memberPopulations) { var objectCreation = base.GetNewObjectCreation(mappingData, memberPopulations); + if (objectCreation == null) + { + memberPopulations.Clear(); + } + if (memberPopulations.None()) { return objectCreation; @@ -62,7 +67,7 @@ private static ICollection GetMemberBindingsFrom(IList GetPopulationExpressionsFor( - IMemberPopulation memberPopulation, + IMemberPopulator memberPopulator, IObjectMappingData mappingData) { - var prePopulationCallback = GetPopulationCallbackOrNull(Before, memberPopulation, mappingData); + var prePopulationCallback = GetPopulationCallbackOrNull(Before, memberPopulator, mappingData); if (prePopulationCallback != null) { yield return prePopulationCallback; } - yield return memberPopulation.GetPopulation(); + yield return memberPopulator.GetPopulation(); - var postPopulationCallback = GetPopulationCallbackOrNull(After, memberPopulation, mappingData); + var postPopulationCallback = GetPopulationCallbackOrNull(After, memberPopulator, mappingData); if (postPopulationCallback != null) { @@ -31,10 +31,10 @@ protected override IEnumerable GetPopulationExpressionsFor( private static Expression GetPopulationCallbackOrNull( CallbackPosition position, - IMemberPopulation memberPopulation, + IMemberPopulator memberPopulator, IObjectMappingData mappingData) { - return memberPopulation.MapperData.GetMappingCallbackOrNull(position, mappingData.MapperData); + return memberPopulator.MapperData.GetMappingCallbackOrNull(position, mappingData.MapperData); } } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs index 411742d82..b2169f0a9 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/PopulationExpressionFactoryBase.cs @@ -45,7 +45,8 @@ private static void GetCreationCallbacks( out Expression preCreationCallback, out Expression postCreationCallback) { - if (mapperData.TargetIsDefinitelyPopulated()) + if (mapperData.RuleSet.Settings.UseSingleRootMappingExpression || + mapperData.TargetIsDefinitelyPopulated()) { preCreationCallback = postCreationCallback = null; return; @@ -60,15 +61,15 @@ private static Expression GetCreationCallbackOrNull(CallbackPosition callbackPos private IEnumerable GetPopulationsAndCallbacks(IObjectMappingData mappingData) { - foreach (var memberPopulation in MemberPopulationFactory.Default.Create(mappingData)) + foreach (var memberPopulator in MemberPopulatorFactory.Default.Create(mappingData)) { - if (!memberPopulation.IsSuccessful) + if (!memberPopulator.CanPopulate) { - yield return memberPopulation.GetPopulation(); + yield return memberPopulator.GetPopulation(); continue; } - foreach (var expression in GetPopulationExpressionsFor(memberPopulation, mappingData)) + foreach (var expression in GetPopulationExpressionsFor(memberPopulator, mappingData)) { yield return expression; } @@ -76,7 +77,7 @@ private IEnumerable GetPopulationsAndCallbacks(IObjectMappingData ma } protected abstract IEnumerable GetPopulationExpressionsFor( - IMemberPopulation memberPopulation, + IMemberPopulator memberPopulator, IObjectMappingData mappingData); private Expression GetLocalVariableInstantiation( @@ -110,7 +111,9 @@ protected virtual Expression GetNewObjectCreation( private static Expression GetObjectRegistrationCallOrNull(ObjectMapperData mapperData) { - if (!mapperData.CacheMappedObjects || mapperData.TargetTypeWillNotBeMappedAgain) + if (!mapperData.RuleSet.Settings.AllowObjectTracking || + !mapperData.CacheMappedObjects || + mapperData.TargetTypeWillNotBeMappedAgain) { return null; } diff --git a/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs b/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs index 8c3062433..60ee1191b 100644 --- a/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs +++ b/AgileMapper/ObjectPopulation/ComplexTypes/TargetObjectResolutionFactory.cs @@ -101,6 +101,7 @@ private static bool MemberPopulationsExist(IEnumerable populationsAn private static Expression AddExistingTargetCheckIfAppropriate(Expression value, IObjectMappingData mappingData) { if ((value.NodeType == ExpressionType.Default) || + mappingData.MapperData.RuleSet.Settings.UseSingleRootMappingExpression || mappingData.MapperData.TargetMemberIsUserStruct() || mappingData.MapperData.TargetIsDefinitelyUnpopulated()) { diff --git a/AgileMapper/ObjectPopulation/DerivedComplexTypeMappingsFactory.cs b/AgileMapper/ObjectPopulation/DerivedComplexTypeMappingsFactory.cs index e055ec3ee..7cc8eb4ba 100644 --- a/AgileMapper/ObjectPopulation/DerivedComplexTypeMappingsFactory.cs +++ b/AgileMapper/ObjectPopulation/DerivedComplexTypeMappingsFactory.cs @@ -19,7 +19,10 @@ public static Expression CreateFor(IObjectMappingData declaredTypeMappingData) return Constants.EmptyExpression; } - var derivedSourceTypes = declaredTypeMapperData.GetDerivedSourceTypes(); + var derivedSourceTypes = declaredTypeMapperData.RuleSet.Settings.CheckDerivedSourceTypes + ? declaredTypeMapperData.GetDerivedSourceTypes() + : Enumerable.EmptyArray; + var derivedTargetTypes = GetDerivedTargetTypesIfNecessary(declaredTypeMappingData); var derivedTypePairs = GetTypePairsFor(declaredTypeMapperData, declaredTypeMapperData); @@ -61,7 +64,9 @@ public static Expression CreateFor(IObjectMappingData declaredTypeMappingData) return typedObjectVariables.Any() ? Expression.Block(typedObjectVariables, derivedTypeMappingExpressions) - : Expression.Block(derivedTypeMappingExpressions); + : derivedTypeMappingExpressions.HasOne() + ? derivedTypeMappingExpressions.First() + : Expression.Block(derivedTypeMappingExpressions); } private static ICollection GetDerivedTargetTypesIfNecessary(IObjectMappingData mappingData) @@ -126,18 +131,23 @@ private static Expression GetTypePairCondition(DerivedTypePair derivedTypePair, } private static void AddDerivedSourceTypeMappings( - IEnumerable derivedSourceTypes, + ICollection derivedSourceTypes, IObjectMappingData declaredTypeMappingData, ICollection typedObjectVariables, IList derivedTypeMappingExpressions) { + if (derivedSourceTypes.None()) + { + return; + } + var declaredTypeMapperData = declaredTypeMappingData.MapperData; var insertionOffset = derivedTypeMappingExpressions.Count; - derivedSourceTypes = derivedSourceTypes + var orderedDerivedSourceTypes = derivedSourceTypes .OrderBy(t => t, TypeComparer.MostToLeastDerived); - foreach (var derivedSourceType in derivedSourceTypes) + foreach (var derivedSourceType in orderedDerivedSourceTypes) { var derivedSourceCheck = new DerivedSourceTypeCheck(derivedSourceType); var typedVariableAssignment = derivedSourceCheck.GetTypedVariableAssignment(declaredTypeMapperData); diff --git a/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs index 551e27166..a7558292d 100644 --- a/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/DictionaryMappingExpressionFactory.cs @@ -11,6 +11,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using Extensions.Internal; using Members; using Members.Dictionaries; + using Members.Population; using NetStandardPolyfills; using ReadableExpressions; @@ -18,11 +19,11 @@ internal class DictionaryMappingExpressionFactory : MappingExpressionFactoryBase { public static readonly MappingExpressionFactoryBase Instance = new DictionaryMappingExpressionFactory(); - private readonly MemberPopulationFactory _memberPopulationFactory; + private readonly MemberPopulatorFactory _memberPopulatorFactory; private DictionaryMappingExpressionFactory() { - _memberPopulationFactory = new MemberPopulationFactory(GetAllTargetMembers); + _memberPopulatorFactory = new MemberPopulatorFactory(GetAllTargetMembers); } #region Target Member Generation @@ -252,16 +253,14 @@ protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out { if (mappingData.MapperKey.MappingTypes.SourceType.IsDictionary()) { - nullMappingBlock = null; - return false; + return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); } var targetMember = (DictionaryTargetMember)mappingData.MapperData.TargetMember; if ((targetMember.KeyType == typeof(string)) || (targetMember.KeyType == typeof(object))) { - nullMappingBlock = null; - return false; + return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); } nullMappingBlock = Expression.Block( @@ -271,12 +270,6 @@ protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out return true; } - protected override IEnumerable GetShortCircuitReturns(GotoExpression returnNull, IObjectMappingData mappingData) - => Enumerable.Empty; - - protected override Expression GetDerivedTypeMappings(IObjectMappingData mappingData) - => Constants.EmptyExpression; - protected override IEnumerable GetObjectPopulation(IObjectMappingData mappingData) { var mapperData = mappingData.MapperData; @@ -428,8 +421,8 @@ private static bool UseParameterlessConstructor( private static Expression GetParameterlessDictionaryAssignment(IObjectMappingData mappingData) { - var valueType = mappingData.MapperData.EnumerablePopulationBuilder.TargetTypeHelper.ElementType; - var newDictionary = mappingData.MapperData.TargetType.GetEmptyInstanceCreation(valueType); + var helper = mappingData.MapperData.EnumerablePopulationBuilder.TargetTypeHelper; + var newDictionary = helper.GetEmptyInstanceCreation(); return GetDictionaryAssignment(newDictionary, mappingData); } @@ -465,11 +458,16 @@ private Expression GetDictionaryPopulation(IObjectMappingData mappingData) return GetEnumerableToDictionaryMapping(mappingData); } - var memberPopulations = _memberPopulationFactory + var memberPopulations = _memberPopulatorFactory .Create(mappingData) .Select(memberPopulation => memberPopulation.GetPopulation()) .ToArray(); + if (memberPopulations.None()) + { + return null; + } + if (memberPopulations.HasOne()) { return memberPopulations[0]; @@ -493,8 +491,5 @@ private static Expression GetEnumerableToDictionaryMapping(IObjectMappingData ma return builder; } - - protected override Expression GetReturnValue(ObjectMapperData mapperData) - => mapperData.TargetInstance; } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/Enumerables/CopySourceEnumerablePopulationStrategy.cs b/AgileMapper/ObjectPopulation/Enumerables/CopySourceEnumerablePopulationStrategy.cs index a0f0a849c..ec5ae7172 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/CopySourceEnumerablePopulationStrategy.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/CopySourceEnumerablePopulationStrategy.cs @@ -4,8 +4,6 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables internal class CopySourceEnumerablePopulationStrategy : EnumerablePopulationStrategyBase { - public static readonly IEnumerablePopulationStrategy Instance = new CopySourceEnumerablePopulationStrategy(); - protected override Expression GetEnumerablePopulation( EnumerablePopulationBuilder builder, IObjectMappingData mappingData) diff --git a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs index 3c60413f1..ceb47013a 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/DictionaryPopulationBuilder.cs @@ -221,9 +221,9 @@ private Expression GetPopulation( var sourceMember = mappingData.MapperData.SourceMember; var mappingDataSource = new AdHocDataSource(sourceMember, elementMapping); - var mappingDataSources = new DataSourceSet(mappingDataSource); + var mappingDataSources = new DataSourceSet(elementMapperData, mappingDataSource); - var memberPopulation = MemberPopulation.WithoutRegistration(elementMappingData, mappingDataSources); + var memberPopulation = MemberPopulator.WithoutRegistration(elementMappingData, mappingDataSources); var populationExpression = memberPopulation.GetPopulation(); return populationExpression; diff --git a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryAdapter.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryAdapter.cs index efa2b5702..1257e1a17 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryAdapter.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceElementsDictionaryAdapter.cs @@ -64,9 +64,8 @@ public override Expression GetSourceValues() var filteredEntries = Expression.Call(linqWhereMethod, dictionaryAccess, keyMatchesLambda); - var linqSelectMethod = typeof(Enumerable) - .GetPublicStaticMethods("Select") - .First(m => m.GetParameters()[1].ParameterType.GetGenericTypeArguments().Length == 2) + var linqSelectMethod = EnumerablePopulationBuilder + .EnumerableSelectWithoutIndexMethod .MakeGenericMethod(kvpType, SourceMember.ValueType); var kvpValueLambda = Expression.Lambda( diff --git a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryAdapter.cs b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryAdapter.cs index 88e425207..efec1e291 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryAdapter.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/Dictionaries/SourceObjectDictionaryAdapter.cs @@ -18,9 +18,7 @@ public SourceObjectDictionaryAdapter( : base(builder) { _instanceDictionaryAdapter = new SourceInstanceDictionaryAdapter(sourceMember, builder); - - var targetEnumerableType = TargetTypeHelper.EnumerableInterfaceType; - _emptyTarget = targetEnumerableType.GetEmptyInstanceCreation(TargetTypeHelper.ElementType); + _emptyTarget = TargetTypeHelper.GetEmptyInstanceCreation(TargetTypeHelper.EnumerableInterfaceType); } public override Expression GetSourceValues() @@ -106,7 +104,8 @@ public Expression GetMappingShortCircuitOrNull() .GetSourceEnumerableFoundTest(_emptyTarget, Builder); var projectionAsTargetType = Expression.TypeAs(Builder.SourceValue, Builder.MapperData.TargetType); - var convertedProjection = TargetTypeHelper.GetEnumerableConversion(Builder.SourceValue); + var allowEnumerableAssignment = Builder.MapperData.RuleSet.Settings.AllowEnumerableAssignment; + var convertedProjection = TargetTypeHelper.GetEnumerableConversion(Builder.SourceValue, allowEnumerableAssignment); var projectionResult = Expression.Coalesce(projectionAsTargetType, convertedProjection); var returnConvertedProjection = Expression.Return(Builder.MapperData.ReturnLabelTarget, projectionResult); var ifProjectedReturn = Expression.IfThen(sourceEnumerableFoundTest, returnConvertedProjection); diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs index 04f2dc54e..27dbb977a 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableMappingExpressionFactory.cs @@ -16,8 +16,7 @@ protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out { if (mappingData.MapperData.SourceMember.IsEnumerable) { - nullMappingBlock = null; - return false; + return base.TargetCannotBeMapped(mappingData, out nullMappingBlock); } nullMappingBlock = Expression.Block( @@ -27,12 +26,6 @@ protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out return true; } - protected override IEnumerable GetShortCircuitReturns(GotoExpression returnNull, IObjectMappingData mappingData) - => Enumerable.Empty; - - protected override Expression GetDerivedTypeMappings(IObjectMappingData mappingData) - => Constants.EmptyExpression; - protected override IEnumerable GetObjectPopulation(IObjectMappingData mappingData) { yield return mappingData.MappingContext.RuleSet.EnumerablePopulationStrategy.GetPopulation(mappingData); diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs index 423f3791a..a0337fe3c 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerablePopulationBuilder.cs @@ -14,19 +14,11 @@ internal class EnumerablePopulationBuilder { #region Untyped MethodInfos - private static readonly MethodInfo _selectWithoutIndexMethod = typeof(Enumerable) - .GetPublicStaticMethods("Select") - .Last(m => - (m.GetParameters().Length == 2) && - (m.GetParameters()[1].ParameterType.GetGenericTypeArguments().Length == 2)); - - private static readonly MethodInfo _forEachMethod = typeof(EnumerableExtensions) - .GetPublicStaticMethods("ForEach") - .First(); - - private static readonly MethodInfo _forEachTupleMethod = typeof(EnumerableExtensions) - .GetPublicStaticMethods("ForEach") - .Last(); + public static readonly MethodInfo EnumerableSelectWithoutIndexMethod; + private static readonly MethodInfo _enumerableSelectWithIndexMethod; + private static readonly MethodInfo _queryableSelectMethod; + private static readonly MethodInfo _forEachMethod; + private static readonly MethodInfo _forEachTupleMethod; #endregion @@ -47,12 +39,46 @@ public EnumerablePopulationBuilder(ObjectMapperData mapperData) Context = new EnumerablePopulationContext(mapperData); _sourceItemsSelector = new SourceItemsSelector(this); _sourceElementParameter = Context.SourceElementType.GetOrCreateParameter(); - TargetTypeHelper = new EnumerableTypeHelper(mapperData.TargetType, mapperData.TargetMember.ElementType); + TargetTypeHelper = new EnumerableTypeHelper(mapperData.TargetMember); _sourceAdapter = SourceEnumerableAdapterFactory.GetAdapterFor(this); _populationExpressions = new List(); } + static EnumerablePopulationBuilder() + { + var linqSelectMethods = typeof(Enumerable) + .GetPublicStaticMethods("Select") + .Select(m => new + { + Method = m, + Parameters = m.GetParameters() + }) + .Where(m => m.Parameters.Length == 2) + .Select(m => new + { + m.Method, + ProjectionLambdaParameterCount = m.Parameters[1].ParameterType.GetGenericTypeArguments().Length + }) + .ToArray(); + + EnumerableSelectWithoutIndexMethod = linqSelectMethods + .First(m => m.ProjectionLambdaParameterCount == 2).Method; + + _enumerableSelectWithIndexMethod = linqSelectMethods + .First(m => m.ProjectionLambdaParameterCount == 3).Method; + + _queryableSelectMethod = typeof(Queryable) + .GetPublicStaticMethods("Select") + .First(m => + (m.GetParameters().Length == 2) && + (m.GetParameters()[1].ParameterType.GetGenericTypeArguments()[0].GetGenericTypeArguments().Length == 2)); + + var forEachMethods = typeof(EnumerableExtensions).GetPublicStaticMethods("ForEach").ToArray(); + _forEachMethod = forEachMethods.First(); + _forEachTupleMethod = forEachMethods.Last(); + } + #region Operator public static implicit operator BlockExpression(EnumerablePopulationBuilder builder) @@ -258,12 +284,12 @@ private void CreateSourceTypeHelper(Expression sourceValue) #region Target Variable Population - public void PopulateTargetVariableFromSourceObjectOnly() - => AssignTargetVariableTo(GetSourceOnlyReturnValue()); + public void PopulateTargetVariableFromSourceObjectOnly(IObjectMappingData mappingData = null) + => AssignTargetVariableTo(GetSourceOnlyReturnValue(mappingData)); - private Expression GetSourceOnlyReturnValue() + private Expression GetSourceOnlyReturnValue(IObjectMappingData mappingData) { - var convertedSourceItems = _sourceItemsSelector.SourceItemsProjectedToTargetType().GetResult(); + var convertedSourceItems = _sourceItemsSelector.SourceItemsProjectedToTargetType(mappingData).GetResult(); var returnValue = ConvertForReturnValue(convertedSourceItems); return returnValue; @@ -294,7 +320,7 @@ private Expression GetTargetVariableValue() { if (!MapperData.TargetMemberHasInitAccessibleValue()) { - return TargetTypeHelper.GetEmptyInstanceCreation(); + return TargetTypeHelper.GetNewInstanceCreation(); } if (MapperData.TargetIsDefinitelyUnpopulated()) @@ -532,33 +558,62 @@ private static Expression GetElementMapping( public Expression GetSourceItemsProjection( Expression sourceEnumerableValue, - Func projectionFuncFactory) + Func projectionLambdaFactory) { - return GetSourceItemsProjection( + return CreateSourceItemsProjection( sourceEnumerableValue, - _selectWithoutIndexMethod, - (sourceParameter, counter) => projectionFuncFactory.Invoke(sourceParameter), - _sourceElementParameter); + (sourceParameter, counter) => projectionLambdaFactory.Invoke(sourceParameter), + counterRequired: false); } - private Expression GetSourceItemsProjection( + public Expression GetSourceItemsProjection( Expression sourceEnumerableValue, - MethodInfo linqSelectOverload, - Func projectionFuncFactory, - params ParameterExpression[] projectionFuncParameters) + Func projectionLambdaFactory) { - var funcTypes = projectionFuncParameters - .Select(p => p.Type) - .ToArray() - .Append(Context.TargetElementType); + return CreateSourceItemsProjection(sourceEnumerableValue, projectionLambdaFactory, counterRequired: true); + } - var projectionFunc = Expression.Lambda( - Expression.GetFuncType(funcTypes), - projectionFuncFactory.Invoke(_sourceElementParameter, Counter), - projectionFuncParameters); + private Expression CreateSourceItemsProjection( + Expression sourceEnumerableValue, + Func projectionLambdaFactory, + bool counterRequired) + { + var isRootQueryableMapping = MapperData.SourceType.IsQueryable(); + + if (isRootQueryableMapping || MapperData.Context.IsPartOfQueryableMapping()) + { + counterRequired = false; + } + + var linqSelectOverload = isRootQueryableMapping + ? _queryableSelectMethod + : counterRequired + ? _enumerableSelectWithIndexMethod + : EnumerableSelectWithoutIndexMethod; + + ParameterExpression[] projectionLambdaParameters; + Type[] funcTypes; + + if (counterRequired) + { + projectionLambdaParameters = new[] { _sourceElementParameter, Counter }; + funcTypes = new[] { Context.SourceElementType, Counter.Type, Context.TargetElementType }; + } + else + { + projectionLambdaParameters = new[] { _sourceElementParameter }; + funcTypes = new[] { Context.SourceElementType, Context.TargetElementType }; + } + + var projectionFuncType = Expression.GetFuncType(funcTypes); + + Expression projectionLambda = Expression.Lambda( + projectionFuncType, + projectionLambdaFactory.Invoke(_sourceElementParameter, Counter), + projectionLambdaParameters); var typedSelectMethod = linqSelectOverload.MakeGenericMethod(Context.ElementTypes); - var typedSelectCall = Expression.Call(typedSelectMethod, sourceEnumerableValue, projectionFunc); + var typedSelectCall = Expression.Call(typedSelectMethod, sourceEnumerableValue, projectionLambda); return typedSelectCall; } @@ -633,7 +688,7 @@ private Expression ConvertForReturnValue(Expression value) if (!allowSameValue) { - return TargetTypeHelper.GetEnumerableConversion(value); + return GetEnumerableConversion(value); } if (value.Type.IsAssignableTo(MapperData.TargetType)) @@ -641,7 +696,7 @@ private Expression ConvertForReturnValue(Expression value) return value; } - var conversion = TargetTypeHelper.GetEnumerableConversion(value); + var conversion = GetEnumerableConversion(value); if (MapperData.TargetType.IsInterface()) { @@ -652,6 +707,9 @@ private Expression ConvertForReturnValue(Expression value) } + public Expression GetEnumerableConversion(Expression value) + => TargetTypeHelper.GetEnumerableConversion(value, MapperData.RuleSet.Settings.AllowEnumerableAssignment); + private Expression GetTargetMethodCall(string methodName, Expression argument = null) { var method = TargetTypeHelper.CollectionInterfaceType.GetPublicInstanceMethod(methodName) @@ -685,7 +743,7 @@ internal SourceItemsSelector(EnumerablePopulationBuilder builder) _builder = builder; } - public SourceItemsSelector SourceItemsProjectedToTargetType() + public SourceItemsSelector SourceItemsProjectedToTargetType(IObjectMappingData mappingData = null) { var context = _builder.Context; var sourceEnumerableValue = _builder._sourceAdapter.GetSourceValues(); @@ -699,7 +757,7 @@ public SourceItemsSelector SourceItemsProjectedToTargetType() _result = _builder.GetSourceItemsProjection( sourceEnumerableValue, - _builder.GetSimpleElementConversion); + (sourceElement, counter) => _builder.GetElementConversion(sourceElement, mappingData)); return this; } @@ -731,7 +789,7 @@ public Expression GetResult() return _result; } - _result = _builder.TargetTypeHelper.GetEnumerableConversion(_result); + _result = _builder.GetEnumerableConversion(_result); return _result; } diff --git a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs index ceac09f66..cab520877 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/EnumerableTypeHelper.cs @@ -5,6 +5,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables using System.Collections.ObjectModel; using System.Linq.Expressions; using Extensions.Internal; + using Members; using NetStandardPolyfills; internal class EnumerableTypeHelper @@ -17,6 +18,11 @@ internal class EnumerableTypeHelper private Type _collectionInterfaceType; private Type _enumerableInterfaceType; + public EnumerableTypeHelper(QualifiedMember member) + : this(member.Type, member.ElementType) + { + } + public EnumerableTypeHelper(Type enumerableType, Type elementType) { EnumerableType = enumerableType; @@ -77,27 +83,39 @@ private Type GetEnumerableType(ref Type typeField, Type openGenericEnumerableTyp public Type WrapperType => typeof(ReadOnlyCollectionWrapper<>).MakeGenericType(ElementType); - public Expression GetEmptyInstanceCreation() + public Expression GetNewInstanceCreation() + { + return IsReadOnly || EnumerableType.IsInterface() + ? Expression.New(ListType) + : GetEmptyInstanceCreation(); + } + + public Expression GetEmptyInstanceCreation(Type enumerableType = null) { - if (IsReadOnly || EnumerableType.IsInterface()) + if ((enumerableType == EnumerableType) || (enumerableType == null)) { - return Expression.New(ListType); + return EnumerableType.GetEmptyInstanceCreation(ElementType, this); } - return EnumerableType.GetEmptyInstanceCreation(ElementType); + return enumerableType.GetEmptyInstanceCreation(ElementType); } public Expression GetWrapperConstruction(Expression existingItems, Expression newItemsCount) { - // ReSharper disable once AssignNullToNotNullAttribute return Expression.New( WrapperType.GetPublicInstanceConstructor(ListInterfaceType, typeof(int)), existingItems, newItemsCount); } - public Expression GetEnumerableConversion(Expression instance) + public Expression GetEnumerableConversion(Expression instance, bool allowEnumerableAssignment) { + if (instance.Type.IsAssignableTo(EnumerableType) && + (allowEnumerableAssignment || ValueIsNotEnumerableInterface(instance))) + { + return instance; + } + if (IsArray) { return instance.WithToArrayCall(ElementType); @@ -113,7 +131,12 @@ public Expression GetEnumerableConversion(Expression instance) return instance.WithToCollectionCall(ElementType); } - return instance.WithToListCall(ElementType); + return instance.WithToListLinqCall(ElementType); + } + + private static bool ValueIsNotEnumerableInterface(Expression instance) + { + return instance.Type != typeof(IEnumerable<>).MakeGenericType(instance.Type.GetEnumerableElementType()); } } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/Enumerables/MergeEnumerablePopulationStrategy.cs b/AgileMapper/ObjectPopulation/Enumerables/MergeEnumerablePopulationStrategy.cs index 9de18430f..5af7a48c5 100644 --- a/AgileMapper/ObjectPopulation/Enumerables/MergeEnumerablePopulationStrategy.cs +++ b/AgileMapper/ObjectPopulation/Enumerables/MergeEnumerablePopulationStrategy.cs @@ -4,8 +4,6 @@ namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables internal class MergeEnumerablePopulationStrategy : EnumerablePopulationStrategyBase { - public static readonly IEnumerablePopulationStrategy Instance = new MergeEnumerablePopulationStrategy(); - protected override Expression GetEnumerablePopulation( EnumerablePopulationBuilder builder, IObjectMappingData mappingData) diff --git a/AgileMapper/ObjectPopulation/Enumerables/ProjectSourceEnumerablePopulationStrategy.cs b/AgileMapper/ObjectPopulation/Enumerables/ProjectSourceEnumerablePopulationStrategy.cs new file mode 100644 index 000000000..ebc021d83 --- /dev/null +++ b/AgileMapper/ObjectPopulation/Enumerables/ProjectSourceEnumerablePopulationStrategy.cs @@ -0,0 +1,16 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation.Enumerables +{ + using System.Linq.Expressions; + + internal class ProjectSourceEnumerablePopulationStrategy : EnumerablePopulationStrategyBase + { + protected override Expression GetEnumerablePopulation( + EnumerablePopulationBuilder builder, + IObjectMappingData mappingData) + { + builder.PopulateTargetVariableFromSourceObjectOnly(mappingData); + + return builder; + } + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs b/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs index 764f92815..068f44938 100644 --- a/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs +++ b/AgileMapper/ObjectPopulation/ExistingOrDefaultValueDataSourceFactory.cs @@ -28,7 +28,7 @@ private static Expression GetValue(IMemberMapperData mapperData) : mapperData.GetFallbackCollectionValue(); } - if (mapperData.TargetMember.IsReadable && !mapperData.TargetMemberIsUserStruct()) + if (mapperData.TargetMember.IsReadable && !mapperData.UseMemberInitialisations()) { return mapperData.GetTargetMemberAccess(); } diff --git a/AgileMapper/ObjectPopulation/IMappingDataOwner.cs b/AgileMapper/ObjectPopulation/IMappingDataOwner.cs new file mode 100644 index 000000000..e282dadce --- /dev/null +++ b/AgileMapper/ObjectPopulation/IMappingDataOwner.cs @@ -0,0 +1,7 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation +{ + internal interface IMappingDataOwner + { + IObjectMappingData MappingData { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/IObjectMapper.cs b/AgileMapper/ObjectPopulation/IObjectMapper.cs index 1ef3d9548..4d36f4d51 100644 --- a/AgileMapper/ObjectPopulation/IObjectMapper.cs +++ b/AgileMapper/ObjectPopulation/IObjectMapper.cs @@ -2,6 +2,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { using System.Collections.Generic; using System.Linq.Expressions; + using Recursion; internal interface IObjectMapper : IObjectMapperFunc { diff --git a/AgileMapper/ObjectPopulation/IRootMapperKey.cs b/AgileMapper/ObjectPopulation/IRootMapperKey.cs new file mode 100644 index 000000000..87ceb2921 --- /dev/null +++ b/AgileMapper/ObjectPopulation/IRootMapperKey.cs @@ -0,0 +1,7 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation +{ + internal interface IRootMapperKey : ITypedMapperKey + { + MappingRuleSet RuleSet { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/ITypedMapperKey.cs b/AgileMapper/ObjectPopulation/ITypedMapperKey.cs new file mode 100644 index 000000000..aba8d6233 --- /dev/null +++ b/AgileMapper/ObjectPopulation/ITypedMapperKey.cs @@ -0,0 +1,9 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation +{ + using Members; + + internal interface ITypedMapperKey : IMappingDataOwner + { + MappingTypes MappingTypes { get; } + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/MapperDataContext.cs b/AgileMapper/ObjectPopulation/MapperDataContext.cs index 899b6780d..f49243f45 100644 --- a/AgileMapper/ObjectPopulation/MapperDataContext.cs +++ b/AgileMapper/ObjectPopulation/MapperDataContext.cs @@ -1,5 +1,6 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { + using System; using System.Linq; using Extensions.Internal; using Members; @@ -46,6 +47,11 @@ private static bool ShouldUseLocalVariable(IBasicMapperData mapperData) return false; } + if (mapperData.UseSingleMappingExpression()) + { + return false; + } + if (mapperData.TargetMember.IsComplex && (mapperData.TargetMember.IsReadOnly || mapperData.TargetIsDefinitelyPopulated()) && !mapperData.TargetMemberIsUserStruct()) @@ -85,26 +91,30 @@ private void BubbleMappingNeededToParent() public bool UseLocalVariable { get; } - public bool UseMappingTryCatch => _mapperData.IsRoot || !IsPartOfUserStructMapping; + public bool UseMappingTryCatch + => _mapperData.RuleSet.Settings.UseTryCatch && (_mapperData.IsRoot || !IsPartOfUserStructMapping()); + + public bool IsPartOfUserStructMapping() + => CheckHierarchy(mapperData => mapperData.TargetMemberIsUserStruct()); + + public bool IsPartOfQueryableMapping() + => CheckHierarchy(mapperData => mapperData.SourceType.IsQueryable()); - public bool IsPartOfUserStructMapping + private bool CheckHierarchy(Func predicate) { - get - { - var mapperData = _mapperData; + var mapperData = _mapperData; - while (mapperData != null) + while (mapperData != null) + { + if (predicate.Invoke(mapperData)) { - if (mapperData.TargetMemberIsUserStruct()) - { - return true; - } - - mapperData = mapperData.Parent; + return true; } - return false; + mapperData = mapperData.Parent; } + + return false; } public bool UsesMappingDataObjectAsParameter diff --git a/AgileMapper/ObjectPopulation/MappingDataCreationFactory.cs b/AgileMapper/ObjectPopulation/MappingDataCreationFactory.cs index f14ebd795..28bd05396 100644 --- a/AgileMapper/ObjectPopulation/MappingDataCreationFactory.cs +++ b/AgileMapper/ObjectPopulation/MappingDataCreationFactory.cs @@ -1,11 +1,13 @@ namespace AgileObjects.AgileMapper.ObjectPopulation { + using System.Diagnostics; using System.Linq.Expressions; using Extensions.Internal; using Members; internal static class MappingDataCreationFactory { + [DebuggerStepThrough] public static Expression ForDerivedType(ObjectMapperData childMapperData) { UseAsConversion(childMapperData, out var asConversion); @@ -13,6 +15,7 @@ public static Expression ForDerivedType(ObjectMapperData childMapperData) return asConversion; } + [DebuggerStepThrough] private static bool UseAsConversion(ObjectMapperData childMapperData, out Expression conversion) { if (childMapperData.Context.IsStandalone) @@ -27,6 +30,7 @@ private static bool UseAsConversion(ObjectMapperData childMapperData, out Expres return false; } + [DebuggerStepThrough] public static Expression ForChild( MappingValues mappingValues, int dataSourceIndex, @@ -56,6 +60,7 @@ public static Expression ForChild( return createCall; } + [DebuggerStepThrough] public static Expression ForElement( MappingValues mappingValues, Expression enumerableMappingDataObject, diff --git a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs index 72bb87adf..5dd6f1f44 100644 --- a/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs +++ b/AgileMapper/ObjectPopulation/MappingExpressionFactoryBase.cs @@ -7,8 +7,8 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using Extensions.Internal; using Members; using NetStandardPolyfills; - using static CallbackPosition; using static System.Linq.Expressions.ExpressionType; + using static CallbackPosition; internal abstract class MappingExpressionFactoryBase { @@ -51,7 +51,7 @@ public Expression Create(IObjectMappingData mappingData) mappingExpressions.InsertRange(0, GetShortCircuitReturns(returnNull, mappingData)); - var mappingBlock = GetMappingBlock(mappingExpressions, mappingExtras); + var mappingBlock = GetMappingBlock(mappingExpressions, mappingExtras, mapperData); if (mapperData.Context.UseMappingTryCatch) { @@ -61,9 +61,14 @@ public Expression Create(IObjectMappingData mappingData) return mappingBlock; } - protected abstract bool TargetCannotBeMapped(IObjectMappingData mappingData, out Expression nullMappingBlock); + protected virtual bool TargetCannotBeMapped(IObjectMappingData mappingData, out Expression nullMappingBlock) + { + nullMappingBlock = null; + return false; + } - protected abstract IEnumerable GetShortCircuitReturns(GotoExpression returnNull, IObjectMappingData mappingData); + protected virtual IEnumerable GetShortCircuitReturns(GotoExpression returnNull, IObjectMappingData mappingData) + => Enumerable.Empty; private bool MappingAlwaysBranchesToDerivedType(IObjectMappingData mappingData, out Expression derivedTypeMappings) { @@ -79,20 +84,24 @@ private bool MappingAlwaysBranchesToDerivedType(IObjectMappingData mappingData, return true; } - protected abstract Expression GetDerivedTypeMappings(IObjectMappingData mappingData); + protected virtual Expression GetDerivedTypeMappings(IObjectMappingData mappingData) => Constants.EmptyExpression; private static MappingExtras GetMappingExtras(ObjectMapperData mapperData) { + var mapToNullCondition = GetMapToNullConditionOrNull(mapperData); + + if (mapperData.RuleSet.Settings.UseSingleRootMappingExpression) + { + return (mapToNullCondition != null) + ? new MappingExtras(mapToNullCondition) + : MappingExtras.Empty; + } + var basicMapperData = mapperData.WithNoTargetMember(); var preMappingCallback = basicMapperData.GetMappingCallbackOrNull(Before, mapperData); var postMappingCallback = basicMapperData.GetMappingCallbackOrNull(After, mapperData); - var mapToNullCondition = GetMapToNullConditionOrNull(mapperData); - return new MappingExtras( - mapperData, - preMappingCallback, - postMappingCallback, - mapToNullCondition); + return new MappingExtras(preMappingCallback, postMappingCallback, mapToNullCondition); } private static Expression GetMapToNullConditionOrNull(IMemberMapperData mapperData) @@ -183,19 +192,25 @@ private static bool IsMemberMapping(Expression expression) private static bool IsCallTo(string methodName, Expression methodCall) => ((MethodCallExpression)methodCall).Method.Name == methodName; - private Expression GetMappingBlock(IList mappingExpressions, MappingExtras mappingExtras) + private Expression GetMappingBlock( + IList mappingExpressions, + MappingExtras mappingExtras, + ObjectMapperData mapperData) { - var mapperData = mappingExtras.MapperData; - - Expression returnExpression; - AdjustForSingleExpressionBlockIfApplicable(ref mappingExpressions); + if (mapperData.UseSingleMappingExpression()) + { + return mappingExpressions.First(); + } + if (mappingExpressions.HasOne() && (mappingExpressions[0].NodeType == Constant)) { goto CreateFullMappingBlock; } + Expression returnExpression; + if (mappingExpressions[0].NodeType != Block) { if (mappingExpressions[0].NodeType == MemberAccess) @@ -203,28 +218,23 @@ private Expression GetMappingBlock(IList mappingExpressions, Mapping return GetReturnExpression(mappingExpressions[0], mappingExtras); } - if (!mapperData.Context.UseLocalVariable) - { - goto CreateFullMappingBlock; - } - - var firstAssignment = (BinaryExpression)mappingExpressions.First(exp => exp.NodeType == Assign); - - if ((firstAssignment.Left.NodeType == Parameter) && - (mappingExpressions.Last() == firstAssignment)) + if (TryAdjustForUnusedLocalVariableIfApplicable( + mappingExpressions, + mappingExtras, + mapperData, + out returnExpression)) { - returnExpression = GetReturnExpression(firstAssignment.Right, mappingExtras); - - if (mappingExpressions.HasOne()) - { - return returnExpression; - } - - mappingExpressions[mappingExpressions.Count - 1] = mapperData.GetReturnLabel(returnExpression); - - return Expression.Block(mappingExpressions); + return returnExpression; } } + else if (TryAdjustForUnusedLocalVariableIfApplicable( + mappingExpressions, + mappingExtras, + mapperData, + out returnExpression)) + { + return returnExpression; + } CreateFullMappingBlock: @@ -254,6 +264,56 @@ private static void AdjustForSingleExpressionBlockIfApplicable(ref IList mappingExpressions, + MappingExtras mappingExtras, + ObjectMapperData mapperData, + out Expression returnExpression) + { + if (!mapperData.Context.UseLocalVariable) + { + returnExpression = null; + return false; + } + + if (!TryGetVariableAssignment(mappingExpressions, out var localVariableAssignment)) + { + returnExpression = null; + return false; + } + + if ((localVariableAssignment.Left.NodeType != Parameter) || + (localVariableAssignment != mappingExpressions.Last())) + { + returnExpression = null; + return false; + } + + var assignedValue = localVariableAssignment.Right; + + returnExpression = (assignedValue.NodeType == Invoke) + ? Expression.Block( + new[] { (ParameterExpression)localVariableAssignment.Left }, + GetReturnExpression(localVariableAssignment, mappingExtras)) + : GetReturnExpression(assignedValue, mappingExtras); + + if (mappingExpressions.HasOne()) + { + return true; + } + + mappingExpressions[mappingExpressions.Count - 1] = mapperData.GetReturnLabel(returnExpression); + returnExpression = Expression.Block(mappingExpressions); + return true; + } + + private static bool TryGetVariableAssignment(IEnumerable mappingExpressions, out BinaryExpression binaryExpression) + { + binaryExpression = mappingExpressions.FirstOrDefault(exp => exp.NodeType == Assign) as BinaryExpression; + + return binaryExpression != null; + } + private static Expression GetReturnExpression(Expression returnValue, MappingExtras mappingExtras) { return (mappingExtras.MapToNullCondition != null) @@ -264,7 +324,7 @@ private static Expression GetReturnExpression(Expression returnValue, MappingExt : returnValue; } - protected abstract Expression GetReturnValue(ObjectMapperData mapperData); + protected virtual Expression GetReturnValue(ObjectMapperData mapperData) => mapperData.TargetInstance; private static Expression WrapInTryCatch(Expression mappingBlock, IMemberMapperData mapperData) { @@ -330,20 +390,23 @@ public virtual void Reset() internal class MappingExtras { + public static readonly MappingExtras Empty = new MappingExtras(null, null, null); + + public MappingExtras(Expression mapToNullCondition) + : this(null, null, mapToNullCondition) + { + } + public MappingExtras( - ObjectMapperData mapperData, Expression preMappingCallback, Expression postMappingCallback, Expression mapToNullCondition) { - MapperData = mapperData; PreMappingCallback = preMappingCallback; PostMappingCallback = postMappingCallback; MapToNullCondition = mapToNullCondition; } - public ObjectMapperData MapperData { get; } - public Expression PreMappingCallback { get; } public Expression PostMappingCallback { get; } diff --git a/AgileMapper/ObjectPopulation/MappingFactory.cs b/AgileMapper/ObjectPopulation/MappingFactory.cs index 0571ab19a..d86979534 100644 --- a/AgileMapper/ObjectPopulation/MappingFactory.cs +++ b/AgileMapper/ObjectPopulation/MappingFactory.cs @@ -54,18 +54,14 @@ public static Expression GetChildMapping( if (childMapperData.TargetMemberEverRecurses()) { - if (childMapperData.DoNotMapRecursion()) - { - return Constants.EmptyExpression; - } - - childMapperData.CacheMappedObjects = true; - - var mapRecursionCall = GetMapRecursionCallFor( - childMappingData, - mappingValues.SourceValue, - dataSourceIndex, - declaredTypeMapperData); + var mapRecursionCall = childMapperData + .RuleSet + .RecursiveMemberMappingStrategy + .GetMapRecursionCallFor( + childMappingData, + mappingValues.SourceValue, + dataSourceIndex, + declaredTypeMapperData); return mapRecursionCall; } @@ -78,24 +74,6 @@ public static Expression GetChildMapping( return inlineMappingBlock; } - private static Expression GetMapRecursionCallFor( - IObjectMappingData childMappingData, - Expression sourceValue, - int dataSourceIndex, - ObjectMapperData declaredTypeMapperData) - { - var childMapperData = childMappingData.MapperData; - - childMapperData.RegisterRequiredMapperFunc(childMappingData); - - var mapRecursionCall = declaredTypeMapperData.GetMapRecursionCall( - sourceValue, - childMapperData.TargetMember, - dataSourceIndex); - - return mapRecursionCall; - } - public static Expression GetElementMapping( Expression sourceElementValue, Expression targetElementValue, @@ -194,7 +172,7 @@ public static Expression GetInlineMappingBlock( createMappingDataCall); } - public static Expression GetDirectAccessMapping( + private static Expression GetDirectAccessMapping( Expression mapping, ObjectMapperData mapperData, MappingValues mappingValues, @@ -241,7 +219,8 @@ private static bool ShouldUseLocalSourceValueVariable( IMemberMapperData mapperData) { return (sourceValue.NodeType != ExpressionType.Parameter) && - SourceAccessFinder.MultipleAccessesExist(mapperData, mapping); + !mapperData.RuleSet.Settings.UseMemberInitialisation && + SourceAccessFinder.MultipleAccessesExist(mapperData, mapping); } private static string GetSourceValueVariableName(IMemberMapperData mapperData, Type sourceType = null) @@ -267,7 +246,9 @@ public static Expression UseLocalSourceValueVariableIfAppropriate( Expression mappingExpression, ObjectMapperData mapperData) { - if (mapperData.Context.IsForDerivedType || !mapperData.Context.IsStandalone) + if (mapperData.Context.IsForDerivedType || + !mapperData.Context.IsStandalone || + mapperData.UseSingleMappingExpression()) { return mappingExpression; } diff --git a/AgileMapper/ObjectPopulation/ObjectMapper.cs b/AgileMapper/ObjectPopulation/ObjectMapper.cs index 16d65135f..7af3eb565 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapper.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapper.cs @@ -5,6 +5,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using System.Linq; using System.Linq.Expressions; using Caching; + using Recursion; using NetStandardPolyfills; internal class ObjectMapper : IObjectMapper diff --git a/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs b/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs index 6fe927f28..0dec4c9ff 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperFactory.cs @@ -7,6 +7,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using ComplexTypes; using Enumerables; using Extensions.Internal; + using Queryables; using Validation; internal class ObjectMapperFactory @@ -19,6 +20,7 @@ public ObjectMapperFactory(CacheSet mapperScopedCacheSet) { _mappingExpressionFactories = new[] { + QueryProjectionExpressionFactory.Instance, DictionaryMappingExpressionFactory.Instance, SimpleTypeMappingExpressionFactory.Instance, EnumerableMappingExpressionFactory.Instance, diff --git a/AgileMapper/ObjectPopulation/ObjectMapperKeyBase.cs b/AgileMapper/ObjectPopulation/ObjectMapperKeyBase.cs index 2a50f0f2e..1cf85776d 100644 --- a/AgileMapper/ObjectPopulation/ObjectMapperKeyBase.cs +++ b/AgileMapper/ObjectPopulation/ObjectMapperKeyBase.cs @@ -3,7 +3,7 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using Members; using Members.Sources; - internal abstract class ObjectMapperKeyBase : SourceMemberTypeDependentKeyBase + internal abstract class ObjectMapperKeyBase : SourceMemberTypeDependentKeyBase, ITypedMapperKey { protected ObjectMapperKeyBase(MappingTypes mappingTypes) { @@ -12,7 +12,7 @@ protected ObjectMapperKeyBase(MappingTypes mappingTypes) public MappingTypes MappingTypes { get; } - protected bool TypesMatch(ObjectMapperKeyBase otherKey) => otherKey.MappingTypes.Equals(MappingTypes); + protected bool TypesMatch(ITypedMapperKey otherKey) => otherKey.MappingTypes.Equals(MappingTypes); public abstract IMembersSource GetMembersSource(IObjectMappingData parentMappingData); diff --git a/AgileMapper/ObjectPopulation/ObjectMappingDataFactory.cs b/AgileMapper/ObjectPopulation/ObjectMappingDataFactory.cs index ebae431b7..50db22b0c 100644 --- a/AgileMapper/ObjectPopulation/ObjectMappingDataFactory.cs +++ b/AgileMapper/ObjectPopulation/ObjectMappingDataFactory.cs @@ -9,21 +9,57 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using Members; using Members.Sources; using NetStandardPolyfills; + using Queryables; internal class ObjectMappingDataFactory : IObjectMappingDataFactoryBridge { private static readonly IObjectMappingDataFactoryBridge _bridge = new ObjectMappingDataFactory(); + public static ObjectMappingData ForRootFixedTypes( + IMappingContext mappingContext) + { + return ForRootFixedTypes(default(TSource), default(TTarget), mappingContext); + } + + public static ObjectMappingData, IQueryable> ForProjection( + IQueryable sourceQueryable, + IMappingContext mappingContext) + { + var projectorKey = new QueryProjectorKey( + MappingTypes.Fixed, + sourceQueryable.Provider.GetType(), + mappingContext.MapperContext); + + return ForRootFixedTypes( + sourceQueryable, + default(IQueryable), + projectorKey, + mappingContext); + } + public static ObjectMappingData ForRootFixedTypes( TSource source, TTarget target, IMappingContext mappingContext) + { + return ForRootFixedTypes( + source, + target, + new RootObjectMapperKey(MappingTypes.Fixed, mappingContext), + mappingContext); + } + + private static ObjectMappingData ForRootFixedTypes( + TSource source, + TTarget target, + ObjectMapperKeyBase mapperKey, + IMappingContext mappingContext) { return new ObjectMappingData( source, target, null, // <- No enumerable index because we're at the root - new RootObjectMapperKey(MappingTypes.Fixed, mappingContext), + mapperKey, mappingContext, parent: null); } diff --git a/AgileMapper/ObjectPopulation/IRecursionMapperFunc.cs b/AgileMapper/ObjectPopulation/Recursion/IRecursionMapperFunc.cs similarity index 72% rename from AgileMapper/ObjectPopulation/IRecursionMapperFunc.cs rename to AgileMapper/ObjectPopulation/Recursion/IRecursionMapperFunc.cs index f0f0b2554..92fd4d0b5 100644 --- a/AgileMapper/ObjectPopulation/IRecursionMapperFunc.cs +++ b/AgileMapper/ObjectPopulation/Recursion/IRecursionMapperFunc.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.ObjectPopulation +namespace AgileObjects.AgileMapper.ObjectPopulation.Recursion { using System; diff --git a/AgileMapper/ObjectPopulation/Recursion/IRecursiveMemberMappingStrategy.cs b/AgileMapper/ObjectPopulation/Recursion/IRecursiveMemberMappingStrategy.cs new file mode 100644 index 000000000..53f62cd54 --- /dev/null +++ b/AgileMapper/ObjectPopulation/Recursion/IRecursiveMemberMappingStrategy.cs @@ -0,0 +1,13 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation.Recursion +{ + using System.Linq.Expressions; + + internal interface IRecursiveMemberMappingStrategy + { + Expression GetMapRecursionCallFor( + IObjectMappingData childMappingData, + Expression sourceValue, + int dataSourceIndex, + ObjectMapperData declaredTypeMapperData); + } +} diff --git a/AgileMapper/ObjectPopulation/Recursion/MapRecursionCallRecursiveMemberMappingStrategy.cs b/AgileMapper/ObjectPopulation/Recursion/MapRecursionCallRecursiveMemberMappingStrategy.cs new file mode 100644 index 000000000..79b59d444 --- /dev/null +++ b/AgileMapper/ObjectPopulation/Recursion/MapRecursionCallRecursiveMemberMappingStrategy.cs @@ -0,0 +1,58 @@ +namespace AgileObjects.AgileMapper.ObjectPopulation.Recursion +{ + using System; + using System.Linq.Expressions; + using Extensions.Internal; + using Members; + + internal class MapRecursionCallRecursiveMemberMappingStrategy : IRecursiveMemberMappingStrategy + { + public static IRecursiveMemberMappingStrategy Instance = new MapRecursionCallRecursiveMemberMappingStrategy(); + + public Expression GetMapRecursionCallFor( + IObjectMappingData childMappingData, + Expression sourceValue, + int dataSourceIndex, + ObjectMapperData declaredTypeMapperData) + { + var childMapperData = childMappingData.MapperData; + + if (DoNotMapRecursion(childMapperData)) + { + return Constants.EmptyExpression; + } + + childMapperData.CacheMappedObjects = true; + + childMapperData.RegisterRequiredMapperFunc(childMappingData); + + var mapRecursionCall = declaredTypeMapperData.GetMapRecursionCall( + sourceValue, + childMapperData.TargetMember, + dataSourceIndex); + + return mapRecursionCall; + } + + private static bool DoNotMapRecursion(IMemberMapperData mapperData) + { + if (mapperData.SourceType.IsDictionary()) + { + return true; + } + + while (mapperData != null) + { + if (mapperData.TargetType.Name.EndsWith("Dto", StringComparison.Ordinal) || + mapperData.TargetType.Name.EndsWith("DataTransferObject", StringComparison.Ordinal)) + { + return true; + } + + mapperData = mapperData.Parent; + } + + return false; + } + } +} \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/RecursionMapperFunc.cs b/AgileMapper/ObjectPopulation/Recursion/RecursionMapperFunc.cs similarity index 93% rename from AgileMapper/ObjectPopulation/RecursionMapperFunc.cs rename to AgileMapper/ObjectPopulation/Recursion/RecursionMapperFunc.cs index 11b9366e5..6fc249b36 100644 --- a/AgileMapper/ObjectPopulation/RecursionMapperFunc.cs +++ b/AgileMapper/ObjectPopulation/Recursion/RecursionMapperFunc.cs @@ -1,4 +1,4 @@ -namespace AgileObjects.AgileMapper.ObjectPopulation +namespace AgileObjects.AgileMapper.ObjectPopulation.Recursion { using System; using System.Linq.Expressions; diff --git a/AgileMapper/ObjectPopulation/RootObjectMapperKey.cs b/AgileMapper/ObjectPopulation/RootObjectMapperKey.cs index 43d1ff5d2..d16f8eadd 100644 --- a/AgileMapper/ObjectPopulation/RootObjectMapperKey.cs +++ b/AgileMapper/ObjectPopulation/RootObjectMapperKey.cs @@ -6,10 +6,9 @@ namespace AgileObjects.AgileMapper.ObjectPopulation using ReadableExpressions.Extensions; #endif - internal class RootObjectMapperKey : ObjectMapperKeyBase + internal class RootObjectMapperKey : ObjectMapperKeyBase, IRootMapperKey { private readonly MapperContext _mapperContext; - private readonly MappingRuleSet _ruleSet; public RootObjectMapperKey(MappingTypes mappingTypes, IMappingContext mappingContext) : this(mappingContext.RuleSet, mappingTypes, mappingContext.MapperContext) @@ -20,23 +19,25 @@ private RootObjectMapperKey(MappingRuleSet ruleSet, MappingTypes mappingTypes, M : base(mappingTypes) { _mapperContext = mapperContext; - _ruleSet = ruleSet; + RuleSet = ruleSet; } + public MappingRuleSet RuleSet { get; } + public override IMembersSource GetMembersSource(IObjectMappingData parentMappingData) => _mapperContext.RootMembersSource; protected override ObjectMapperKeyBase CreateInstance(MappingTypes newMappingTypes) - => new RootObjectMapperKey(_ruleSet, newMappingTypes, _mapperContext); + => new RootObjectMapperKey(RuleSet, newMappingTypes, _mapperContext); public override bool Equals(object obj) { - var otherKey = (RootObjectMapperKey)obj; + var otherKey = (IRootMapperKey)obj; // ReSharper disable once PossibleNullReferenceException - return TypesMatch(otherKey) && - (otherKey._ruleSet == _ruleSet) && - SourceHasRequiredTypes(otherKey); + return (otherKey.RuleSet == RuleSet) && + TypesMatch(otherKey) && + SourceHasRequiredTypes(otherKey); } #region ExcludeFromCodeCoverage @@ -54,7 +55,7 @@ public override string ToString() var sourceTypeName = MappingTypes.SourceType.GetFriendlyName(); var targetTypeName = MappingTypes.TargetType.GetFriendlyName(); - return $"{_ruleSet.Name}: {sourceTypeName} -> {targetTypeName}"; + return $"{RuleSet.Name}: {sourceTypeName} -> {targetTypeName}"; } #endif #endregion diff --git a/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs b/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs index 15c9d5e44..10ddafa87 100644 --- a/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs +++ b/AgileMapper/ObjectPopulation/SimpleTypeMappingExpressionFactory.cs @@ -10,21 +10,7 @@ internal class SimpleTypeMappingExpressionFactory : MappingExpressionFactoryBase public static readonly MappingExpressionFactoryBase Instance = new SimpleTypeMappingExpressionFactory(); public override bool IsFor(IObjectMappingData mappingData) - { - return mappingData.MapperKey.MappingTypes.TargetType.IsSimple(); - } - - protected override bool TargetCannotBeMapped(IObjectMappingData mappingData, out Expression nullMappingBlock) - { - nullMappingBlock = null; - return false; - } - - protected override IEnumerable GetShortCircuitReturns(GotoExpression returnNull, IObjectMappingData mappingData) - => Enumerable.Empty; - - protected override Expression GetDerivedTypeMappings(IObjectMappingData mappingData) - => Constants.EmptyExpression; + => mappingData.MapperKey.MappingTypes.TargetType.IsSimple(); protected override IEnumerable GetObjectPopulation(IObjectMappingData mappingData) { @@ -38,7 +24,5 @@ protected override IEnumerable GetObjectPopulation(IObjectMappingDat yield return mapperData.TargetObject; } - - protected override Expression GetReturnValue(ObjectMapperData mapperData) => mapperData.TargetInstance; } } \ No newline at end of file diff --git a/AgileMapper/ObjectPopulation/SourceMemberTypeDependentKeyBase.cs b/AgileMapper/ObjectPopulation/SourceMemberTypeDependentKeyBase.cs index f6aa4588f..b7b7eaa21 100644 --- a/AgileMapper/ObjectPopulation/SourceMemberTypeDependentKeyBase.cs +++ b/AgileMapper/ObjectPopulation/SourceMemberTypeDependentKeyBase.cs @@ -33,7 +33,7 @@ public void AddSourceMemberTypeTesterIfRequired(IObjectMappingData mappingData = _sourceMemberTypeTester = typeTestLambda.Compile(); } - protected bool SourceHasRequiredTypes(SourceMemberTypeDependentKeyBase otherKey) + protected bool SourceHasRequiredTypes(IMappingDataOwner otherKey) => (_sourceMemberTypeTester == null) || _sourceMemberTypeTester.Invoke(otherKey.MappingData); } } \ No newline at end of file diff --git a/AgileMapper/Parameters.cs b/AgileMapper/Parameters.cs index 2f2f4856b..43bd75ede 100644 --- a/AgileMapper/Parameters.cs +++ b/AgileMapper/Parameters.cs @@ -1,6 +1,7 @@ namespace AgileObjects.AgileMapper { using System; + using System.Linq; using System.Linq.Expressions; using Extensions.Internal; using Members; @@ -11,6 +12,8 @@ internal static class Parameters public static readonly ParameterExpression MappingContext = Create(); public static readonly ParameterExpression MappingData = Create(); public static readonly ParameterExpression ObjectMappingData = Create(); + public static readonly ParameterExpression Queryable = Create(); + public static readonly ParameterExpression Mapper = Create(); public static ParameterExpression Create(string name = null) => Create(typeof(T), name); diff --git a/AgileMapper/Plans/MappingPlan.cs b/AgileMapper/Plans/MappingPlan.cs index 95daac9f9..20389ee08 100644 --- a/AgileMapper/Plans/MappingPlan.cs +++ b/AgileMapper/Plans/MappingPlan.cs @@ -28,13 +28,7 @@ internal MappingPlan(IObjectMapper cachedMapper) } } - internal static MappingPlan For(IMappingContext mappingContext) - { - var mappingData = ObjectMappingDataFactory - .ForRootFixedTypes(default(TSource), default(TTarget), mappingContext); - - return new MappingPlan(mappingData.Mapper); - } + internal static MappingPlan For(IObjectMappingData mappingData) => new MappingPlan(mappingData.Mapper); /// /// Converts the given to its string representation. diff --git a/AgileMapper/Plans/RecursionMapperMappingPlanFunction.cs b/AgileMapper/Plans/RecursionMapperMappingPlanFunction.cs index 1b25e0c8c..0976e5f82 100644 --- a/AgileMapper/Plans/RecursionMapperMappingPlanFunction.cs +++ b/AgileMapper/Plans/RecursionMapperMappingPlanFunction.cs @@ -2,7 +2,7 @@ { using System; using System.Linq.Expressions; - using ObjectPopulation; + using ObjectPopulation.Recursion; using ReadableExpressions; using ReadableExpressions.Extensions; diff --git a/AgileMapper/ProjectionExecutor.cs b/AgileMapper/ProjectionExecutor.cs new file mode 100644 index 000000000..c3cb7026c --- /dev/null +++ b/AgileMapper/ProjectionExecutor.cs @@ -0,0 +1,53 @@ +namespace AgileObjects.AgileMapper +{ + using System; + using System.Linq; + using System.Linq.Expressions; + using Api; + using Api.Configuration.Projection; + using ObjectPopulation; + + internal class ProjectionExecutor : IProjectionResultSpecifier + { + private readonly MapperContext _mapperContext; + private readonly IQueryable _sourceQueryable; + + public ProjectionExecutor(MapperContext mapperContext, IQueryable sourceQueryable) + { + _mapperContext = mapperContext; + _sourceQueryable = sourceQueryable; + } + + #region To Overloads + + IQueryable IProjectionResultSpecifier.To() + => PerformProjection(_mapperContext); + + IQueryable IProjectionResultSpecifier.To( + Expression>> configuration) + { + return PerformProjection(new[] { configuration }); + } + + #endregion + + private IQueryable PerformProjection( + Expression>>[] configurations) + { + var inlineMapperContext = _mapperContext.InlineContexts.GetContextFor(configurations, this); + + return PerformProjection(inlineMapperContext); + } + + private IQueryable PerformProjection(MapperContext mapperContext) + { + var rootMappingData = ObjectMappingDataFactory.ForProjection( + _sourceQueryable, + mapperContext.QueryProjectionMappingContext); + + var queryProjection = rootMappingData.MapStart(); + + return queryProjection; + } + } +} diff --git a/AgileMapper/ProjectionExtensions.cs b/AgileMapper/ProjectionExtensions.cs new file mode 100644 index 000000000..4e9352473 --- /dev/null +++ b/AgileMapper/ProjectionExtensions.cs @@ -0,0 +1,40 @@ +namespace AgileObjects.AgileMapper +{ + using System.Linq; + using Api; + + /// + /// Provides extension methods to support projecting an IQueryable to an IQueryable of a different type. + /// + public static class ProjectionExtensions + { + /// + /// Project the elements of the given to instances of a specified + /// result Type, using the default mapper. The projection operation is performed entirely on the data source. + /// + /// The Type of the elements to project to a new result Type. + /// The source IQueryable{T} on which to perform the projection. + /// An IProjectionResultSpecifier with which to specify the type of query projection to perform. + public static IProjectionResultSpecifier Project( + this IQueryable sourceQueryable) + { + return new ProjectionExecutor(Mapper.Default.Context, sourceQueryable); + } + + /// + /// Project the elements of the given to instances of a specified + /// result Type, using the given . The projection operation is performed + /// entirely on the data source. + /// + /// The Type of the elements to project to a new result Type. + /// The source IQueryable{T} on which to perform the projection. + /// The mapper with which the projection should be performed. + /// An IProjectionResultSpecifier with which to specify the type of query projection to perform. + public static IProjectionResultSpecifier ProjectUsing( + this IQueryable sourceQueryable, + IMapper mapper) + { + return new ProjectionExecutor(((IMapperInternal)mapper).Context, sourceQueryable); + } + } +} diff --git a/AgileMapper/Properties/AssemblyInfo.cs b/AgileMapper/Properties/AssemblyInfo.cs index ae87ac23b..e663e9e51 100644 --- a/AgileMapper/Properties/AssemblyInfo.cs +++ b/AgileMapper/Properties/AssemblyInfo.cs @@ -11,4 +11,6 @@ [assembly: InternalsVisibleTo("AgileObjects.AgileMapper.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100570b21a39fbc3df774a5e60b41cd41078ebae1c9210a3ae4355d518a0abecab27f9346fbfe941618dc835e99ab21b75ff38e5815dceebdd8480dc14c0ee14f5cdcd3ace7f980173238c9d827f95a6f46100ff19a7dcf912c9291bf95dcd64694692a193428f2a35023bbed186f3c8f9769e01e5077a8ea5cabafe5d7948024af")] [assembly: InternalsVisibleTo("AgileObjects.AgileMapper.UnitTests.NetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100570b21a39fbc3df774a5e60b41cd41078ebae1c9210a3ae4355d518a0abecab27f9346fbfe941618dc835e99ab21b75ff38e5815dceebdd8480dc14c0ee14f5cdcd3ace7f980173238c9d827f95a6f46100ff19a7dcf912c9291bf95dcd64694692a193428f2a35023bbed186f3c8f9769e01e5077a8ea5cabafe5d7948024af")] [assembly: InternalsVisibleTo("AgileObjects.AgileMapper.UnitTests.NetCore2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100570b21a39fbc3df774a5e60b41cd41078ebae1c9210a3ae4355d518a0abecab27f9346fbfe941618dc835e99ab21b75ff38e5815dceebdd8480dc14c0ee14f5cdcd3ace7f980173238c9d827f95a6f46100ff19a7dcf912c9291bf95dcd64694692a193428f2a35023bbed186f3c8f9769e01e5077a8ea5cabafe5d7948024af")] -[assembly: InternalsVisibleTo("AgileObjects.AgileMapper.UnitTests.NonParallel, PublicKey=0024000004800000940000000602000000240000525341310004000001000100570b21a39fbc3df774a5e60b41cd41078ebae1c9210a3ae4355d518a0abecab27f9346fbfe941618dc835e99ab21b75ff38e5815dceebdd8480dc14c0ee14f5cdcd3ace7f980173238c9d827f95a6f46100ff19a7dcf912c9291bf95dcd64694692a193428f2a35023bbed186f3c8f9769e01e5077a8ea5cabafe5d7948024af")] \ No newline at end of file +[assembly: InternalsVisibleTo("AgileObjects.AgileMapper.UnitTests.NonParallel, PublicKey=0024000004800000940000000602000000240000525341310004000001000100570b21a39fbc3df774a5e60b41cd41078ebae1c9210a3ae4355d518a0abecab27f9346fbfe941618dc835e99ab21b75ff38e5815dceebdd8480dc14c0ee14f5cdcd3ace7f980173238c9d827f95a6f46100ff19a7dcf912c9291bf95dcd64694692a193428f2a35023bbed186f3c8f9769e01e5077a8ea5cabafe5d7948024af")] +[assembly: InternalsVisibleTo("AgileObjects.AgileMapper.UnitTests.Orms, PublicKey=0024000004800000940000000602000000240000525341310004000001000100570b21a39fbc3df774a5e60b41cd41078ebae1c9210a3ae4355d518a0abecab27f9346fbfe941618dc835e99ab21b75ff38e5815dceebdd8480dc14c0ee14f5cdcd3ace7f980173238c9d827f95a6f46100ff19a7dcf912c9291bf95dcd64694692a193428f2a35023bbed186f3c8f9769e01e5077a8ea5cabafe5d7948024af")] +[assembly: InternalsVisibleTo("AgileObjects.AgileMapper.UnitTests.MoreTestClasses, PublicKey=0024000004800000940000000602000000240000525341310004000001000100570b21a39fbc3df774a5e60b41cd41078ebae1c9210a3ae4355d518a0abecab27f9346fbfe941618dc835e99ab21b75ff38e5815dceebdd8480dc14c0ee14f5cdcd3ace7f980173238c9d827f95a6f46100ff19a7dcf912c9291bf95dcd64694692a193428f2a35023bbed186f3c8f9769e01e5077a8ea5cabafe5d7948024af")] diff --git a/AgileMapper/Queryables/Api/IProjectionPlanTargetSelector.cs b/AgileMapper/Queryables/Api/IProjectionPlanTargetSelector.cs new file mode 100644 index 000000000..c1c761e6c --- /dev/null +++ b/AgileMapper/Queryables/Api/IProjectionPlanTargetSelector.cs @@ -0,0 +1,27 @@ +namespace AgileObjects.AgileMapper.Queryables.Api +{ + using Plans; + + /// + /// Provides options to create and compile a query projection function from the source type being configured + /// to a specified target type. + /// + /// + /// The type of element contained in the source IQueryable from which the projection function to be created will project. + /// + // ReSharper disable once UnusedTypeParameter + public interface IProjectionPlanTargetSelector + { + /// + /// Create and compile a query projection function from the source type being configured to the type specified + /// by the type argument. + /// + /// The type of target object for which to create the query projection mapping plan. + /// + /// A object detailing the function to be executed during a query projection, using + /// the given source IQueryable. To see a string representation of the function assign the result to a string + /// variable, or call .ToString(). + /// + MappingPlan To(); + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Converters/ComplexTypeConditionalConverter.cs b/AgileMapper/Queryables/Converters/ComplexTypeConditionalConverter.cs new file mode 100644 index 000000000..92ab026fc --- /dev/null +++ b/AgileMapper/Queryables/Converters/ComplexTypeConditionalConverter.cs @@ -0,0 +1,59 @@ +namespace AgileObjects.AgileMapper.Queryables.Converters +{ + using System.Linq.Expressions; + using Extensions.Internal; + using ReadableExpressions.Extensions; + + internal static class ComplexTypeConditionalConverter + { + public static bool TryConvert( + ConditionalExpression conditional, + IQueryProjectionModifier modifier, + out Expression converted) + { + if (modifier.Settings.SupportsNonEntityNullConstants || !conditional.Type.IsComplex()) + { + converted = null; + return false; + } + + converted = new NonNullableMemberBinder(conditional).GuardMemberAccesses(); + return true; + } + + private class NonNullableMemberBinder : ExpressionVisitor + { + private readonly ConditionalExpression _conditional; + + public NonNullableMemberBinder(ConditionalExpression conditional) + { + _conditional = conditional; + } + + public Expression GuardMemberAccesses() + => VisitAndConvert(_conditional.IfTrue, nameof(GuardMemberAccesses)); + + protected override MemberBinding VisitMemberBinding(MemberBinding binding) + { + if (binding.BindingType != MemberBindingType.Assignment) + { + return base.VisitMemberBinding(binding); + } + + var memberBinding = (MemberAssignment)binding; + + if (memberBinding.Expression.Type.CanBeNull()) + { + return base.VisitMemberBinding(binding); + } + + var bindingValueOrNull = Expression.Condition( + _conditional.Test, + memberBinding.Expression, + DefaultValueConstantExpressionFactory.CreateFor(memberBinding.Expression)); + + return memberBinding.Update(bindingValueOrNull); + } + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Converters/ComplexTypeToNullComparisonConverter.cs b/AgileMapper/Queryables/Converters/ComplexTypeToNullComparisonConverter.cs new file mode 100644 index 000000000..50dd6296c --- /dev/null +++ b/AgileMapper/Queryables/Converters/ComplexTypeToNullComparisonConverter.cs @@ -0,0 +1,98 @@ +namespace AgileObjects.AgileMapper.Queryables.Converters +{ + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using Extensions.Internal; + using Members; + using ReadableExpressions.Extensions; + + internal static class ComplexTypeToNullComparisonConverter + { + public static bool TryConvert( + BinaryExpression comparison, + IQueryProjectionModifier context, + out Expression converted) + { + if (context.Settings.SupportsComplexTypeToNullComparison || + (comparison.Left.Type == typeof(object)) || + !comparison.Left.Type.IsComplex()) + { + converted = null; + return false; + } + + converted = Convert(comparison, context); + return converted != null; + } + + private static Expression Convert(BinaryExpression comparison, IQueryProjectionModifier context) + { + if (!context.MapperData.IsEntity(comparison.Left.Type, out var idMember)) + { + return null; + } + + var entityMemberAccess = comparison.Left; + var entityParentAccess = entityMemberAccess.GetParentOrNull(); + + if (!TryGetEntityMemberIdMemberOrNull( + entityParentAccess, + entityMemberAccess, + idMember, + out var entityMemberIdMember)) + { + return null; + } + + var entityMemberIdMemberAccess = entityMemberIdMember.GetAccess(entityParentAccess); + + return entityMemberIdMemberAccess.GetIsNotDefaultComparison(); + } + + private static bool TryGetEntityMemberIdMemberOrNull( + Expression entityParentAccess, + Expression entityMemberAccess, + Member entityIdMember, + out Member entityMemberIdMember) + { + var sourceMembers = GlobalContext + .Instance + .MemberCache + .GetSourceMembers(entityParentAccess.Type) + .Where(m => m.IsSimple) + .ToArray(); + + var entityMemberName = entityMemberAccess.GetMemberName(); + + if (TryGetEntityMemberIdMember(entityMemberName + entityIdMember.Name, sourceMembers, out entityMemberIdMember)) + { + return true; + } + + if (!entityIdMember.Name.EqualsIgnoreCase("Id") && + TryGetEntityMemberIdMember(entityMemberName + "Id", sourceMembers, out entityMemberIdMember)) + { + return true; + } + + if (!entityIdMember.Name.EqualsIgnoreCase("Identifer") && + TryGetEntityMemberIdMember(entityMemberName + "Identifer", sourceMembers, out entityMemberIdMember)) + { + return true; + } + + entityMemberIdMember = null; + return false; + } + + private static bool TryGetEntityMemberIdMember( + string idMemberName, + IEnumerable sourceMembers, + out Member member) + { + member = sourceMembers.FirstOrDefault(m => m.Name.EqualsIgnoreCase(idMemberName)); + return member != null; + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Converters/DefaultValueConstantExpressionFactory.cs b/AgileMapper/Queryables/Converters/DefaultValueConstantExpressionFactory.cs new file mode 100644 index 000000000..0829df5c5 --- /dev/null +++ b/AgileMapper/Queryables/Converters/DefaultValueConstantExpressionFactory.cs @@ -0,0 +1,41 @@ +namespace AgileObjects.AgileMapper.Queryables.Converters +{ + using System; + using System.Linq.Expressions; + using System.Reflection; + using Extensions.Internal; + using NetStandardPolyfills; + + internal static class DefaultValueConstantExpressionFactory + { + private static readonly MethodInfo _getDefaultValueMethod = typeof(DefaultValueConstantExpressionFactory) + .GetNonPublicStaticMethod("GetDefaultValue"); + + public static Expression CreateFor(Expression expression) + => GetDefaultValueFor(expression.Type).ToConstantExpression(expression.Type); + + private static object GetDefaultValueFor(Type type) + { + if (!type.IsValueType()) + { + return null; + } + + var getDefaultValueCaller = GlobalContext.Instance.Cache.GetOrAdd(type, t => + { + var getDefaultValueCall = Expression + .Call(_getDefaultValueMethod.MakeGenericMethod(t)) + .GetConversionToObject(); + + var getDefaultValueLambda = Expression.Lambda>(getDefaultValueCall); + + return getDefaultValueLambda.Compile(); + }); + + return getDefaultValueCaller.Invoke(); + } + + // ReSharper disable once UnusedMember.Local + private static T GetDefaultValue() => default(T); + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Converters/DerivedTypeMappingConverter.cs b/AgileMapper/Queryables/Converters/DerivedTypeMappingConverter.cs new file mode 100644 index 000000000..11cb6c259 --- /dev/null +++ b/AgileMapper/Queryables/Converters/DerivedTypeMappingConverter.cs @@ -0,0 +1,85 @@ +namespace AgileObjects.AgileMapper.Queryables.Converters +{ + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using Extensions.Internal; + using static System.Linq.Expressions.ExpressionType; + + internal static class DerivedTypeMappingConverter + { + public static bool TryConvert( + BlockExpression mappingBlock, + IQueryProjectionModifier modifier, + out Expression converted) + { + if (IsNotDerivedTypeMappingsBlock(mappingBlock, out var derivedTypeMappings)) + { + converted = null; + return false; + } + + var fallbackValue = GetReturnedValue(mappingBlock.Expressions.Last(), modifier); + + converted = derivedTypeMappings.Reverse().Aggregate( + fallbackValue, + (mappingSoFar, derivedTypeMapping) => Expression.Condition( + derivedTypeMapping.Test, + GetReturnedValue(derivedTypeMapping.IfTrue, modifier).GetConversionTo(fallbackValue.Type), + mappingSoFar)); + + return true; + } + + private static bool IsNotDerivedTypeMappingsBlock( + BlockExpression mappingBlock, + out ICollection derivedTypeMappings) + { + derivedTypeMappings = new List(); + + var firstExpression = mappingBlock.Expressions.First(); + + switch (firstExpression.NodeType) + { + case Conditional: + derivedTypeMappings.Add((ConditionalExpression)firstExpression); + return false; + + case Block: + var nestedMappingBlock = (BlockExpression)firstExpression; + + if (nestedMappingBlock.Expressions.All(exp => exp.NodeType == Conditional)) + { + foreach (var derivedTypeMapping in nestedMappingBlock.Expressions) + { + derivedTypeMappings.Add((ConditionalExpression)derivedTypeMapping); + } + + return false; + } + + break; + } + + return true; + } + + private static Expression GetReturnedValue( + Expression returnExpression, + IQueryProjectionModifier modifier) + { + switch (returnExpression.NodeType) + { + case Label: + returnExpression = ((LabelExpression)returnExpression).DefaultValue; + break; + + case Goto: + returnExpression = ((GotoExpression)returnExpression).Value; + break; + } + + return modifier.Modify(returnExpression); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Converters/GetValueOrDefaultConverter.cs b/AgileMapper/Queryables/Converters/GetValueOrDefaultConverter.cs new file mode 100644 index 000000000..977db8d82 --- /dev/null +++ b/AgileMapper/Queryables/Converters/GetValueOrDefaultConverter.cs @@ -0,0 +1,39 @@ +namespace AgileObjects.AgileMapper.Queryables.Converters +{ + using System; + using System.Linq.Expressions; + using Extensions.Internal; + using ReadableExpressions.Extensions; + + internal static class GetValueOrDefaultConverter + { + public static bool TryConvert( + MethodCallExpression methodCall, + IQueryProjectionModifier context, + out Expression converted) + { + if (context.Settings.SupportsGetValueOrDefault || IsNotGetValueOrDefaultCall(methodCall)) + { + converted = null; + return false; + } + + // ReSharper disable once AssignNullToNotNullAttribute + converted = Expression.Condition( + methodCall.Object.GetIsNotDefaultComparison(), + Expression.Convert(methodCall.Object, methodCall.Type), + DefaultValueConstantExpressionFactory.CreateFor(methodCall)); + + return true; + } + + private static bool IsNotGetValueOrDefaultCall(MethodCallExpression methodCall) + { + // ReSharper disable once PossibleNullReferenceException + return methodCall.Arguments.Any() || + methodCall.Method.IsStatic || + !methodCall.Object.Type.IsNullableType() || + (methodCall.Method.Name != nameof(Nullable.GetValueOrDefault)); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Converters/IQueryProjectionModifier.cs b/AgileMapper/Queryables/Converters/IQueryProjectionModifier.cs new file mode 100644 index 000000000..af95a4561 --- /dev/null +++ b/AgileMapper/Queryables/Converters/IQueryProjectionModifier.cs @@ -0,0 +1,15 @@ +namespace AgileObjects.AgileMapper.Queryables.Converters +{ + using System.Linq.Expressions; + using Members; + using Settings; + + internal interface IQueryProjectionModifier + { + IQueryProviderSettings Settings { get; } + + IMemberMapperData MapperData { get; } + + Expression Modify(Expression queryProjection); + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Converters/NestedProjectionAssignmentConverter.cs b/AgileMapper/Queryables/Converters/NestedProjectionAssignmentConverter.cs new file mode 100644 index 000000000..a8322c4d2 --- /dev/null +++ b/AgileMapper/Queryables/Converters/NestedProjectionAssignmentConverter.cs @@ -0,0 +1,44 @@ +namespace AgileObjects.AgileMapper.Queryables.Converters +{ + using System.Linq.Expressions; + using Extensions.Internal; + + internal static class NestedProjectionAssignmentConverter + { + public static bool TryConvert( + MemberAssignment assignment, + IQueryProjectionModifier modifier, + out MemberAssignment converted) + { + if (modifier.Settings.SupportsEnumerableMaterialisation && + (assignment.Expression.NodeType == ExpressionType.Call) && + assignment.Expression.Type.IsEnumerable()) + { + converted = ConvertToMaterialisation(assignment, modifier); + return true; + } + + converted = null; + return false; + } + + private static MemberAssignment ConvertToMaterialisation( + MemberAssignment assignment, + IQueryProjectionModifier context) + { + var materialisedNestedProjection = GetMaterialisedNestedProjection(assignment.Expression); + var modifiedProjection = context.Modify(materialisedNestedProjection); + + return assignment.Update(modifiedProjection); + } + + private static Expression GetMaterialisedNestedProjection(Expression nestedProjection) + { + var elementType = nestedProjection.Type.GetEnumerableElementType(); + + return nestedProjection.Type.IsArray + ? nestedProjection.WithToArrayLinqCall(elementType) + : nestedProjection.WithToListLinqCall(elementType); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Converters/StringConcatConverter.cs b/AgileMapper/Queryables/Converters/StringConcatConverter.cs new file mode 100644 index 000000000..6a41dc8ae --- /dev/null +++ b/AgileMapper/Queryables/Converters/StringConcatConverter.cs @@ -0,0 +1,80 @@ +namespace AgileObjects.AgileMapper.Queryables.Converters +{ + using System.Linq.Expressions; + using System.Reflection; + using Extensions.Internal; + using NetStandardPolyfills; + + internal static class StringConcatConverter + { + private static readonly MethodInfo _stringConcatObjectsMethod = typeof(string) + .GetPublicStaticMethod("Concat", typeof(object), typeof(object)); + + private static readonly MethodInfo _stringConcatStringsMethod = + StringExpressionExtensions.GetConcatMethod(parameterCount: 2); + + public static bool TryConvert( + BinaryExpression binary, + IQueryProjectionModifier context, + out Expression converted) + { + if ((binary.NodeType != ExpressionType.Add) || + !ReferenceEquals(binary.Method, _stringConcatObjectsMethod) || + ((binary.Left.NodeType != ExpressionType.Convert) && (binary.Right.NodeType != ExpressionType.Convert))) + { + converted = null; + return false; + } + + var convertedLeft = ConvertOperand(binary.Left); + var convertedRight = ConvertOperand(binary.Right); + + if ((convertedLeft == binary.Left) && (convertedRight == binary.Right)) + { + converted = null; + return false; + } + + if ((convertedLeft == null) || (convertedRight == null)) + { + converted = convertedLeft ?? convertedRight; + return true; + } + + converted = Expression.Add(convertedLeft, convertedRight, _stringConcatStringsMethod); + return true; + } + + private static Expression ConvertOperand(Expression value) + { + while (true) + { + switch (value.NodeType) + { + case ExpressionType.Constant: + var constant = ((ConstantExpression)value).Value; + + return (constant == null) + ? null + : (value.Type != typeof(string)) + ? constant.ToString().ToConstantExpression() + : ((string)constant != string.Empty) ? value : null; + + case ExpressionType.Convert: + var conversion = (UnaryExpression)value; + + if ((conversion.Operand.NodeType == ExpressionType.Constant)) + { + value = conversion.Operand; + continue; + } + + return value; + + default: + return value; + } + } + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Converters/StringEqualsIgnoreCaseConverter.cs b/AgileMapper/Queryables/Converters/StringEqualsIgnoreCaseConverter.cs new file mode 100644 index 000000000..904ea923f --- /dev/null +++ b/AgileMapper/Queryables/Converters/StringEqualsIgnoreCaseConverter.cs @@ -0,0 +1,30 @@ +namespace AgileObjects.AgileMapper.Queryables.Converters +{ + using System.Linq.Expressions; + + internal static class StringEqualsIgnoreCaseConverter + { + public static bool TryConvert( + MethodCallExpression methodCall, + IQueryProjectionModifier modifier, + out Expression converted) + { + if (modifier.Settings.SupportsStringEqualsIgnoreCase || IsNotEqualsIgnoreCaseCall(methodCall)) + { + converted = null; + return false; + } + + converted = modifier.Settings.ConvertStringEqualsIgnoreCase(methodCall); + return true; + } + + private static bool IsNotEqualsIgnoreCaseCall(MethodCallExpression methodCall) + { + return !methodCall.Method.IsStatic || + (methodCall.Arguments.Count != 3) || + (methodCall.Method.DeclaringType != typeof(string)) || + (methodCall.Method.Name != nameof(string.Equals)); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Converters/StringToEnumConversionConverter.cs b/AgileMapper/Queryables/Converters/StringToEnumConversionConverter.cs new file mode 100644 index 000000000..c57ffa224 --- /dev/null +++ b/AgileMapper/Queryables/Converters/StringToEnumConversionConverter.cs @@ -0,0 +1,38 @@ +namespace AgileObjects.AgileMapper.Queryables.Converters +{ + using System.Linq.Expressions; + using NetStandardPolyfills; + + internal static class StringToEnumConversionConverter + { + public static bool TryConvert( + ConditionalExpression conditional, + IQueryProjectionModifier modifier, + out Expression converted) + { + if (modifier.Settings.SupportsStringToEnumConversion || IsNotStringToEnumConversion(conditional)) + { + converted = null; + return false; + } + + converted = modifier.Settings.ConvertStringToEnumConversion(conditional); + return true; + } + + private static bool IsNotStringToEnumConversion(ConditionalExpression conditional) + { + if (!conditional.Type.IsEnum() || + (conditional.Test.NodeType != ExpressionType.Call)) + { + return true; + } + + var testMethodCall = (MethodCallExpression)conditional.Test; + + return !testMethodCall.Method.IsStatic || + (testMethodCall.Method.DeclaringType != typeof(string)) || + testMethodCall.Method.Name != nameof(string.IsNullOrWhiteSpace); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Converters/ToStringConverter.cs b/AgileMapper/Queryables/Converters/ToStringConverter.cs new file mode 100644 index 000000000..50ba76189 --- /dev/null +++ b/AgileMapper/Queryables/Converters/ToStringConverter.cs @@ -0,0 +1,45 @@ +namespace AgileObjects.AgileMapper.Queryables.Converters +{ + using System.Linq.Expressions; + using Extensions.Internal; + + internal static class ToStringConverter + { + public static bool TryConvert( + MethodCallExpression methodCall, + IQueryProjectionModifier modifier, + out Expression converted) + { + if (IsNotToStringCall(methodCall)) + { + converted = null; + return false; + } + + if (modifier.Settings.SupportsToString) + { + converted = AdjustForFormatStringIfNecessary(methodCall, modifier); + return converted != methodCall; + } + + methodCall = AdjustForFormatStringIfNecessary(methodCall, modifier); + converted = modifier.Settings.ConvertToStringCall(methodCall); + return true; + } + + private static bool IsNotToStringCall(MethodCallExpression methodCall) + => methodCall.Method.IsStatic || (methodCall.Method.Name != nameof(ToString)); + + private static MethodCallExpression AdjustForFormatStringIfNecessary( + MethodCallExpression methodCall, + IQueryProjectionModifier context) + { + if (context.Settings.SupportsToStringWithFormat || methodCall.Arguments.None()) + { + return methodCall; + } + + return methodCall.Object.WithToStringCall(); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Converters/TryParseAssignmentConverter.cs b/AgileMapper/Queryables/Converters/TryParseAssignmentConverter.cs new file mode 100644 index 000000000..1fa630608 --- /dev/null +++ b/AgileMapper/Queryables/Converters/TryParseAssignmentConverter.cs @@ -0,0 +1,51 @@ +namespace AgileObjects.AgileMapper.Queryables.Converters +{ + using System.Linq.Expressions; + using Extensions.Internal; + + internal static class TryParseAssignmentConverter + { + public static bool TryConvert( + MemberAssignment assignment, + IQueryProjectionModifier modifier, + out MemberAssignment converted) + { + if (assignment.Expression.NodeType != ExpressionType.Block) + { + converted = null; + return false; + } + + var valueBlock = (BlockExpression)assignment.Expression; + var finalExpression = valueBlock.Expressions.Last(); + + if (finalExpression.NodeType != ExpressionType.Conditional) + { + converted = null; + return false; + } + + var tryParseOrDefault = (ConditionalExpression)finalExpression; + + if ((tryParseOrDefault.Test.NodeType != ExpressionType.Call) || + (tryParseOrDefault.IfTrue.NodeType != ExpressionType.Parameter)) + { + converted = null; + return false; + } + + var methodCall = (MethodCallExpression)tryParseOrDefault.Test; + + if (!methodCall.Method.IsStatic || (methodCall.Method.Name != nameof(int.TryParse))) + { + converted = null; + return false; + } + + var convertedValue = modifier.Settings.ConvertTryParseCall(methodCall, tryParseOrDefault.IfFalse); + + converted = assignment.Update(convertedValue); + return true; + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/QueryProjectionExpressionFactory.cs b/AgileMapper/Queryables/QueryProjectionExpressionFactory.cs new file mode 100644 index 000000000..46e58e261 --- /dev/null +++ b/AgileMapper/Queryables/QueryProjectionExpressionFactory.cs @@ -0,0 +1,37 @@ +namespace AgileObjects.AgileMapper.Queryables +{ + using System.Collections.Generic; + using System.Linq.Expressions; + using Extensions.Internal; + using ObjectPopulation; + + internal class QueryProjectionExpressionFactory : MappingExpressionFactoryBase + { + public static readonly MappingExpressionFactoryBase Instance = new QueryProjectionExpressionFactory(); + + public override bool IsFor(IObjectMappingData mappingData) + { + var mapperData = mappingData.MapperData; + + return mapperData.TargetMember.IsEnumerable && mapperData.SourceType.IsQueryable(); + } + + protected override IEnumerable GetObjectPopulation(IObjectMappingData mappingData) + { + var mapperData = mappingData.MapperData; + + var queryProjection = mapperData + .EnumerablePopulationBuilder + .GetSourceItemsProjection( + mapperData.SourceObject, + sourceParameter => MappingFactory.GetElementMapping( + sourceParameter, + mapperData.TargetMember.ElementType.ToDefaultExpression(), + mappingData)); + + queryProjection = QueryProjectionModifier.Modify(queryProjection, mappingData); + + yield return queryProjection; + } + } +} diff --git a/AgileMapper/Queryables/QueryProjectionModifier.cs b/AgileMapper/Queryables/QueryProjectionModifier.cs new file mode 100644 index 000000000..72c5cb099 --- /dev/null +++ b/AgileMapper/Queryables/QueryProjectionModifier.cs @@ -0,0 +1,105 @@ +namespace AgileObjects.AgileMapper.Queryables +{ + using System.Linq.Expressions; + using Converters; + using Members; + using ObjectPopulation; + using Settings; + + internal class QueryProjectionModifier : ExpressionVisitor, IQueryProjectionModifier + { + private QueryProjectionModifier(IObjectMappingData mappingData) + { + MapperData = mappingData.MapperData; + Settings = mappingData.GetQueryProviderSettings(); + } + + public IQueryProviderSettings Settings { get; } + + public IMemberMapperData MapperData { get; } + + public static Expression Modify(Expression queryProjection, IObjectMappingData mappingData) + => new QueryProjectionModifier(mappingData).Modify(queryProjection); + + public Expression Modify(Expression queryProjection) + => VisitAndConvert(queryProjection, "Modify"); + + protected override Expression VisitBinary(BinaryExpression binary) + { + if (ComplexTypeToNullComparisonConverter.TryConvert(binary, this, out var converted)) + { + return converted; + } + + if (StringConcatConverter.TryConvert(binary, this, out converted)) + { + return converted; + } + + return base.VisitBinary(binary); + } + + protected override Expression VisitBlock(BlockExpression block) + { + if (DerivedTypeMappingConverter.TryConvert(block, this, out var converted)) + { + return converted; + } + + return base.VisitBlock(block); + } + + protected override Expression VisitConditional(ConditionalExpression conditional) + { + if (ComplexTypeConditionalConverter.TryConvert(conditional, this, out var converted)) + { + return Modify(converted); + } + + if (StringToEnumConversionConverter.TryConvert(conditional, this, out converted)) + { + return converted; + } + + return base.VisitConditional(conditional); + } + + protected override Expression VisitDefault(DefaultExpression defaultExpression) + => DefaultValueConstantExpressionFactory.CreateFor(defaultExpression); + + protected override MemberAssignment VisitMemberAssignment(MemberAssignment assignment) + { + if (TryParseAssignmentConverter.TryConvert(assignment, this, out var converted)) + { + return converted; + } + + if (NestedProjectionAssignmentConverter.TryConvert(assignment, this, out converted)) + { + return converted; + } + + return base.VisitMemberAssignment(assignment); + } + + protected override Expression VisitMethodCall(MethodCallExpression methodCall) + { + if (ToStringConverter.TryConvert(methodCall, this, out var converted)) + { + return converted; + } + + if (GetValueOrDefaultConverter.TryConvert(methodCall, this, out converted)) + { + return converted; + } + + if (StringEqualsIgnoreCaseConverter.TryConvert(methodCall, this, out converted)) + { + return converted; + } + + return base.VisitMethodCall(methodCall); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/QueryProjectorKey.cs b/AgileMapper/Queryables/QueryProjectorKey.cs new file mode 100644 index 000000000..061825687 --- /dev/null +++ b/AgileMapper/Queryables/QueryProjectorKey.cs @@ -0,0 +1,49 @@ +namespace AgileObjects.AgileMapper.Queryables +{ + using System; + using Members; + using Members.Sources; + using ObjectPopulation; + + internal class QueryProjectorKey : ObjectMapperKeyBase, IRootMapperKey + { + private readonly MapperContext _mapperContext; + + public QueryProjectorKey( + MappingTypes mappingTypes, + Type queryProviderType, + MapperContext mapperContext) + : base(mappingTypes) + { + QueryProviderType = queryProviderType; + _mapperContext = mapperContext; + } + + public MappingRuleSet RuleSet => _mapperContext.RuleSets.Project; + + public Type QueryProviderType { get; } + + public override IMembersSource GetMembersSource(IObjectMappingData parentMappingData) + => _mapperContext.RootMembersSource; + + protected override ObjectMapperKeyBase CreateInstance(MappingTypes newMappingTypes) + => new QueryProjectorKey(newMappingTypes, QueryProviderType, _mapperContext); + + public override bool Equals(object obj) + { + var otherKey = (IRootMapperKey)obj; + + // ReSharper disable once PossibleNullReferenceException + return (otherKey.RuleSet == RuleSet) && + TypesMatch(otherKey) && + (((QueryProjectorKey)otherKey).QueryProviderType == QueryProviderType); + } + + #region ExcludeFromCodeCoverage +#if DEBUG + [ExcludeFromCodeCoverage] +#endif + #endregion + public override int GetHashCode() => 0; + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Recursion/MapToDepthRecursiveMemberMappingStrategy.cs b/AgileMapper/Queryables/Recursion/MapToDepthRecursiveMemberMappingStrategy.cs new file mode 100644 index 000000000..71f36524c --- /dev/null +++ b/AgileMapper/Queryables/Recursion/MapToDepthRecursiveMemberMappingStrategy.cs @@ -0,0 +1,55 @@ +namespace AgileObjects.AgileMapper.Queryables.Recursion +{ + using System.Linq.Expressions; + using Members; + using ObjectPopulation; + using ObjectPopulation.Recursion; + + internal class MapToDepthRecursiveMemberMappingStrategy : IRecursiveMemberMappingStrategy + { + public Expression GetMapRecursionCallFor( + IObjectMappingData childMappingData, + Expression sourceValue, + int dataSourceIndex, + ObjectMapperData declaredTypeMapperData) + { + if (ShortCircuitRecursion(childMappingData)) + { + return GetRecursionShortCircuit(childMappingData); + } + + var mappingValues = new MappingValues( + sourceValue, + childMappingData.MapperData.GetTargetMemberDefault(), + declaredTypeMapperData.EnumerableIndex); + + var inlineMappingBlock = MappingFactory.GetInlineMappingBlock( + childMappingData, + mappingValues, + MappingDataCreationFactory.ForChild(mappingValues, 0, childMappingData.MapperData)); + + return inlineMappingBlock; + } + + private static bool ShortCircuitRecursion(IObjectMappingData childMappingData) + { + return childMappingData + .MappingContext + .MapperContext + .UserConfigurations + .ShortCircuitRecursion(childMappingData.MapperData); + } + + private static Expression GetRecursionShortCircuit(IObjectMappingData childMappingData) + { + if (childMappingData.MapperData.TargetMember.IsComplex) + { + return Constants.EmptyExpression; + } + + var helper = childMappingData.MapperData.EnumerablePopulationBuilder.TargetTypeHelper; + + return helper.GetEmptyInstanceCreation(); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Settings/DefaultQueryProviderSettings.cs b/AgileMapper/Queryables/Settings/DefaultQueryProviderSettings.cs new file mode 100644 index 000000000..6f066dbdc --- /dev/null +++ b/AgileMapper/Queryables/Settings/DefaultQueryProviderSettings.cs @@ -0,0 +1,133 @@ +namespace AgileObjects.AgileMapper.Queryables.Settings +{ + using System; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using Converters; + using Extensions.Internal; + using NetStandardPolyfills; + + internal class DefaultQueryProviderSettings : IQueryProviderSettings + { + private readonly Lazy _canonicalFunctionsTypeLoader; + private readonly Lazy _sqlFunctionsTypeLoader; + + public DefaultQueryProviderSettings() + { + _canonicalFunctionsTypeLoader = new Lazy(LoadCanonicalFunctionsType); + _sqlFunctionsTypeLoader = new Lazy(LoadSqlFunctionsType); + } + + protected virtual Type LoadCanonicalFunctionsType() => null; + + protected virtual Type LoadSqlFunctionsType() => null; + + public Type CanonicalFunctionsType => _canonicalFunctionsTypeLoader.Value; + + public Type SqlFunctionsType => _sqlFunctionsTypeLoader.Value; + + public virtual bool SupportsToString => true; + + public virtual bool SupportsToStringWithFormat => false; + + public virtual bool SupportsStringEqualsIgnoreCase => false; + + public virtual bool SupportsStringToEnumConversion => true; + + public virtual bool SupportsGetValueOrDefault => true; + + public virtual bool SupportsComplexTypeToNullComparison => true; + + public virtual bool SupportsNonEntityNullConstants => true; + + public virtual bool SupportsEnumerableMaterialisation => true; + + public virtual Expression ConvertToStringCall(MethodCallExpression call) + => call.Object.GetConversionTo(); + + public Expression ConvertTryParseCall(MethodCallExpression call, Expression fallbackValue) + { + if (call.Method.DeclaringType == typeof(Guid)) + { + return GetConvertStringToGuid(call, fallbackValue); + } + + Expression conversion; + + if ((call.Method.DeclaringType == typeof(DateTime)) && + (conversion = GetParseStringToDateTimeOrNull(call, fallbackValue)) != null) + { + return conversion; + } + + // ReSharper disable once PossibleNullReferenceException + // Attempt to use Convert.ToInt32 - irretrievably unsupported in non-EDMX EF5 and EF6, + // but it at least gives a decent error message: + var convertMethodName = "To" + call.Method.DeclaringType.Name; + + var convertMethod = typeof(Convert) + .GetPublicStaticMethods(convertMethodName) + .First(m => m.GetParameters().HasOne() && (m.GetParameters()[0].ParameterType == typeof(string))); + + conversion = Expression.Call(convertMethod, call.Arguments.First()); + + return conversion; + } + + public virtual Expression ConvertStringEqualsIgnoreCase(MethodCallExpression call) => call; + + #region ConvertTryParseCall Helpers + + private static Expression GetConvertStringToGuid(MethodCallExpression guidTryParseCall, Expression fallbackValue) + { + var parseMethod = typeof(Guid) + .GetPublicStaticMethod("Parse", parameterCount: 1); + + var sourceValue = guidTryParseCall.Arguments.First(); + var guidConversion = Expression.Call(parseMethod, sourceValue); + fallbackValue = GetDefaultValueFor(fallbackValue); + + var nullString = default(string).ToConstantExpression(); + var sourceIsNotNull = Expression.NotEqual(sourceValue, nullString); + var convertedOrFallback = Expression.Condition(sourceIsNotNull, guidConversion, fallbackValue); + + return convertedOrFallback; + } + + protected static Expression GetDefaultValueFor(Expression value) + => DefaultValueConstantExpressionFactory.CreateFor(value); + + protected virtual Expression GetParseStringToDateTimeOrNull(MethodCallExpression call, Expression fallbackValue) + => null; + + #endregion + + public virtual Expression ConvertStringToEnumConversion(ConditionalExpression conversion) => conversion; + + protected static Type GetTypeOrNull(string loadedAssemblyName, string typeName) + => GetTypeOrNull(GetAssemblyOrNull(loadedAssemblyName), typeName); + + private static Assembly GetAssemblyOrNull(string loadedAssemblyName) + { +#if NET_STANDARD + try + { + return Assembly.Load(new AssemblyName(loadedAssemblyName)); + } + catch + { + return null; + } +#else + var assemblyName = loadedAssemblyName.Substring(0, loadedAssemblyName.IndexOf(',')); + + return AppDomain.CurrentDomain + .GetAssemblies() + .FirstOrDefault(assembly => assembly.GetName().Name == assemblyName); +#endif + } + + protected static Type GetTypeOrNull(Assembly assembly, string typeName) => assembly?.GetType(typeName); + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Settings/EntityFramework/Ef5QueryProviderSettings.cs b/AgileMapper/Queryables/Settings/EntityFramework/Ef5QueryProviderSettings.cs new file mode 100644 index 000000000..5291b44fa --- /dev/null +++ b/AgileMapper/Queryables/Settings/EntityFramework/Ef5QueryProviderSettings.cs @@ -0,0 +1,182 @@ +namespace AgileObjects.AgileMapper.Queryables.Settings.EntityFramework +{ + using System; + using System.Globalization; + using System.Linq.Expressions; + using System.Reflection; + using Extensions.Internal; + using NetStandardPolyfills; + + internal class Ef5QueryProviderSettings : LegacyEfQueryProviderSettings + { + public override bool SupportsToString => false; + + public override bool SupportsNonEntityNullConstants => false; + + protected override Type LoadCanonicalFunctionsType() + { + return GetTypeOrNull( + "System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "System.Data.Objects.EntityFunctions"); + } + + protected override Type LoadSqlFunctionsType() + { + return GetTypeOrNull( + "System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "System.Data.Objects.SqlClient.SqlFunctions"); + } + + public override Expression ConvertToStringCall(MethodCallExpression call) + { + var sqlFunctionsType = SqlFunctionsType; + + if (sqlFunctionsType == null) + { + return base.ConvertToStringCall(call); + } + + return GetStringConvertCall(call.Object, sqlFunctionsType); + } + + private static Expression GetStringConvertCall( + Expression subject, + Type sqlFunctionsType, + bool includeDecimalPlaces = true) + { + var subjectType = subject.Type.GetNonNullableType(); + + if (subjectType == typeof(DateTime)) + { + return GetParseDateTimeToStringOrNull(subject, sqlFunctionsType); + } + + return (subjectType == typeof(decimal)) + ? GetTrimmedStringConvertCall(sqlFunctionsType, subject, subjectType, includeDecimalPlaces) + : GetTrimmedStringConvertCall(sqlFunctionsType, subject, subjectType, includeDecimalPlaces); + } + + private static Expression GetTrimmedStringConvertCall( + Type sqlFunctionsType, + Expression subject, + Type subjectType, + bool includeDecimalPlaces) + { + if (includeDecimalPlaces) + { + includeDecimalPlaces = subjectType.IsNonWholeNumberNumeric(); + } + + subject = subject.GetConversionTo(); + + Expression stringConvertCall; + + if (includeDecimalPlaces) + { + stringConvertCall = Expression.Call( + GetConvertMethod(sqlFunctionsType, typeof(TSubject), typeof(int?), typeof(int?)), + subject.GetConversionTo(), + 20.ToConstantExpression(typeof(int?)), // <-- Total Length + 6.ToConstantExpression(typeof(int?))); // <-- Decimal places + } + else + { + stringConvertCall = Expression.Call( + GetConvertMethod(sqlFunctionsType, typeof(TSubject)), + subject); + } + + var trimMethod = typeof(string).GetPublicInstanceMethod("Trim", parameterCount: 0); + var trimCall = Expression.Call(stringConvertCall, trimMethod); + + return trimCall; + } + + private static MethodInfo GetConvertMethod(Type sqlFunctionsType, params Type[] argumentTypes) + => sqlFunctionsType.GetPublicStaticMethod("StringConvert", argumentTypes); + + private static Expression GetParseDateTimeToStringOrNull(Expression dateValue, Type sqlFunctionsType) + { + if (!TryGetDatePartMethod(sqlFunctionsType, out var datePartMethod)) + { + return null; + } + + dateValue = dateValue.GetConversionTo(); + + var dateTimePattern = CultureInfo + .CurrentCulture + .DateTimeFormat + .SortableDateTimePattern + .Replace("mm", "mi") // Minutes + .Replace('T', ' ') // Date-Time separator + .Replace("'", null) + .ToLowerInvariant(); + + Expression valueConcatenation = null; + var datePartStartIndex = 0; + var stringConcat = StringExpressionExtensions.GetConcatMethod(parameterCount: 2); + + for (var i = 0; i < dateTimePattern.Length; ++i) + { + if (char.IsLetter(dateTimePattern[i])) + { + continue; + } + + var datePartNameCall = GetDatePartCall( + datePartMethod, + dateTimePattern, + datePartStartIndex, + i, + dateValue, + sqlFunctionsType); + + var added = Expression.Call( + stringConcat, + datePartNameCall, + dateTimePattern[i].ToString().ToConstantExpression()); + + valueConcatenation = (valueConcatenation != null) + ? Expression.Call(stringConcat, valueConcatenation, added) : added; + + datePartStartIndex = i + 1; + } + + var finalDatePartNameCall = GetDatePartCall( + datePartMethod, + dateTimePattern, + datePartStartIndex, + dateTimePattern.Length, + dateValue, + sqlFunctionsType); + + // ReSharper disable once AssignNullToNotNullAttribute + return Expression.Call(stringConcat, valueConcatenation, finalDatePartNameCall); + } + + private static Expression GetDatePartCall( + MethodInfo datePartMethod, + string dateTimePattern, + int datePartStartIndex, + int datePartEndIndex, + Expression dateValue, + Type sqlFunctionsType) + { + var datePartNameCall = GetDatePartCall( + datePartMethod, + GetDatePart(dateTimePattern, datePartStartIndex, datePartEndIndex), + dateValue); + + return GetStringConvertCall(datePartNameCall, sqlFunctionsType, false); + } + + private static string GetDatePart( + string dateTimePattern, + int datePartStartIndex, + int datePartEndIndex) + { + return dateTimePattern.Substring(datePartStartIndex, datePartEndIndex - datePartStartIndex); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Settings/EntityFramework/Ef6QueryProviderSettings.cs b/AgileMapper/Queryables/Settings/EntityFramework/Ef6QueryProviderSettings.cs new file mode 100644 index 000000000..77813a0cd --- /dev/null +++ b/AgileMapper/Queryables/Settings/EntityFramework/Ef6QueryProviderSettings.cs @@ -0,0 +1,25 @@ +namespace AgileObjects.AgileMapper.Queryables.Settings.EntityFramework +{ + using System; + using System.Reflection; + + internal class Ef6QueryProviderSettings : LegacyEfQueryProviderSettings + { + private readonly Assembly _entityFrameworkAssembly; + + public Ef6QueryProviderSettings(Assembly entityFrameworkAssembly) + { + _entityFrameworkAssembly = entityFrameworkAssembly; + } + + protected override Type LoadCanonicalFunctionsType() + => GetTypeOrNull(_entityFrameworkAssembly, "System.Data.Entity.DbFunctions"); + + protected override Type LoadSqlFunctionsType() + { + return GetTypeOrNull( + "EntityFramework.SqlServer, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "System.Data.Entity.SqlServer.SqlFunctions"); + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Settings/EntityFramework/EfCore2QueryProviderSettings.cs b/AgileMapper/Queryables/Settings/EntityFramework/EfCore2QueryProviderSettings.cs new file mode 100644 index 000000000..1988fb562 --- /dev/null +++ b/AgileMapper/Queryables/Settings/EntityFramework/EfCore2QueryProviderSettings.cs @@ -0,0 +1,15 @@ +namespace AgileObjects.AgileMapper.Queryables.Settings.EntityFramework +{ + internal class EfCore2QueryProviderSettings : DefaultQueryProviderSettings + { + public override bool SupportsToStringWithFormat => true; + + public override bool SupportsStringEqualsIgnoreCase => true; + + // EF Core translates navigation property-to-null comparisons to compare + // on the navigation property id, and then falls over rewriting the + // comparison binary by trying to compare the complex type to its id value + // This is due to be fixed in 2.1. + public override bool SupportsComplexTypeToNullComparison => false; + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Settings/EntityFramework/LegacyEfQueryProviderSettings.cs b/AgileMapper/Queryables/Settings/EntityFramework/LegacyEfQueryProviderSettings.cs new file mode 100644 index 000000000..0d32af5e2 --- /dev/null +++ b/AgileMapper/Queryables/Settings/EntityFramework/LegacyEfQueryProviderSettings.cs @@ -0,0 +1,214 @@ +namespace AgileObjects.AgileMapper.Queryables.Settings.EntityFramework +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using Extensions.Internal; + using NetStandardPolyfills; + + internal abstract class LegacyEfQueryProviderSettings : DefaultQueryProviderSettings + { + public override bool SupportsStringToEnumConversion => false; + + public override bool SupportsGetValueOrDefault => false; + + public override bool SupportsEnumerableMaterialisation => false; + + protected override Expression GetParseStringToDateTimeOrNull(MethodCallExpression call, Expression fallbackValue) + { + if ((CanonicalFunctionsType == null) || (SqlFunctionsType == null)) + { + return null; + } + + var createDateTimeMethod = CanonicalFunctionsType.GetPublicStaticMethod("CreateDateTime"); + + if (createDateTimeMethod == null) + { + return null; + } + + if (!TryGetDatePartMethod(SqlFunctionsType, out var datePartMethod)) + { + return null; + } + + var sourceValue = call.Arguments[0]; + + var createDateTimeCall = Expression.Call( + createDateTimeMethod, + GetDatePartCall(datePartMethod, "yy", sourceValue), + GetDatePartCall(datePartMethod, "mm", sourceValue), + GetDatePartCall(datePartMethod, "dd", sourceValue), + GetDatePartCall(datePartMethod, "hh", sourceValue), + GetDatePartCall(datePartMethod, "mi", sourceValue), + GetDatePartCall(datePartMethod, "ss", sourceValue).GetConversionTo()); + + fallbackValue = GetDefaultValueFor(fallbackValue); + + var createdDateTime = GetGuardedDateCreation(createDateTimeCall, sourceValue, fallbackValue); + + var nullString = default(string).ToConstantExpression(); + var sourceIsNotNull = Expression.NotEqual(sourceValue, nullString); + var convertedOrFallback = Expression.Condition(sourceIsNotNull, createdDateTime, fallbackValue); + + return convertedOrFallback; + } + + #region GetCreateDateTimeFromStringOrNull Helpers + + protected static bool TryGetDatePartMethod(Type sqlFunctionsType, out MethodInfo datePartMethod) + { + datePartMethod = sqlFunctionsType + .GetPublicStaticMethod("DatePart", typeof(string), typeof(TArgument)); + + return datePartMethod != null; + } + + protected static Expression GetDatePartCall( + MethodInfo datePartMethod, + string datePart, + Expression sourceValue) + { + return Expression.Call(datePartMethod, datePart.ToConstantExpression(), sourceValue); + } + + private Expression GetGuardedDateCreation( + Expression createDateTimeCall, + Expression sourceValue, + Expression fallbackValue) + { + var createdDateTime = Expression.Property(createDateTimeCall, "Value"); + + var isDateMethod = SqlFunctionsType.GetPublicStaticMethod("IsDate"); + + if (isDateMethod == null) + { + return createDateTimeCall; + } + + var isDateCall = Expression.Call(isDateMethod, sourceValue); + var one = 1.ToConstantExpression(typeof(int?)); + var isDateIsTrue = Expression.Equal(isDateCall, one); + var createdDateOrFallback = Expression.Condition(isDateIsTrue, createdDateTime, fallbackValue); + + return createdDateOrFallback; + } + + #endregion + + public override Expression ConvertStringEqualsIgnoreCase(MethodCallExpression call) + { + var subjectToLower = Expression.Call( + call.Arguments[0], + typeof(string).GetPublicInstanceMethod("ToLower", parameterCount: 0)); + + var comparisonValue = GetComparisonValue(call.Arguments[1]); + var comparison = Expression.Equal(subjectToLower, comparisonValue); + + return comparison; + } + + #region GetStringEqualsIgnoreCaseConversion Helpers + + private static Expression GetComparisonValue(Expression value) + { + if (value.NodeType != ExpressionType.Constant) + { + return value; + } + + var stringValue = ((ConstantExpression)value).Value?.ToString().ToLowerInvariant(); + + return stringValue.ToConstantExpression(); + } + + #endregion + + public override Expression ConvertStringToEnumConversion(ConditionalExpression conversion) + { + var enumType = conversion.Type; + var underlyingEnumType = Enum.GetUnderlyingType(enumType); + + var enumNumericValuesByField = enumType.GetPublicStaticFields().ToDictionary( + f => f, + f => Convert.ChangeType(f.GetValue(null), underlyingEnumType).ToString().ToConstantExpression()); + + var fallbackValue = GetDefaultValueFor(conversion.IfTrue); + + while (IsNotStringEqualsCheck(conversion.Test)) + { + conversion = (ConditionalExpression)conversion.IfFalse; + } + + var stringEqualsCheck = (MethodCallExpression)conversion.Test; + var stringValue = stringEqualsCheck.Arguments.First(); + + var stringToEnumConversions = new List(enumNumericValuesByField.Count); + + while (true) + { + if (conversion.IfFalse.NodeType == ExpressionType.Default) + { + break; + } + + var conversionTest = ConvertStringEqualsIgnoreCase((MethodCallExpression)conversion.Test); + + stringToEnumConversions.Insert(0, new StringToEnumConversion(conversionTest, conversion.IfTrue)); + + conversion = (ConditionalExpression)conversion.IfFalse; + } + + var noParseConversion = stringToEnumConversions.Aggregate( + fallbackValue, + (conversionSoFar, stringToEnumConversion) => + { + var enumNumericValue = enumNumericValuesByField[stringToEnumConversion.EnumValueField]; + var stringIsNumericValue = Expression.Equal(stringValue, enumNumericValue); + + return Expression.Condition( + Expression.OrElse(stringIsNumericValue, stringToEnumConversion.Test), + stringToEnumConversion.EnumValue, + conversionSoFar); + }); + + return noParseConversion; + } + + #region GetNoParseStringToEnumConversion Helpers + + private static bool IsNotStringEqualsCheck(Expression test) + { + if (test.NodeType != ExpressionType.Call) + { + return true; + } + + var testMethodCall = (MethodCallExpression)test; + + return !testMethodCall.Method.IsStatic || + (testMethodCall.Method.DeclaringType != typeof(string)) || + (testMethodCall.Method.Name != nameof(string.Equals)); + } + + private class StringToEnumConversion + { + public StringToEnumConversion(Expression test, Expression enumValue) + { + Test = test; + EnumValue = (MemberExpression)enumValue; + } + + public Expression Test { get; } + + public MemberExpression EnumValue { get; } + + public FieldInfo EnumValueField => (FieldInfo)EnumValue.Member; + } + + #endregion + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Settings/IQueryProviderSettings.cs b/AgileMapper/Queryables/Settings/IQueryProviderSettings.cs new file mode 100644 index 000000000..ab1b107e7 --- /dev/null +++ b/AgileMapper/Queryables/Settings/IQueryProviderSettings.cs @@ -0,0 +1,36 @@ +namespace AgileObjects.AgileMapper.Queryables.Settings +{ + using System; + using System.Linq.Expressions; + + internal interface IQueryProviderSettings + { + Type CanonicalFunctionsType { get; } + + Type SqlFunctionsType { get; } + + bool SupportsToString { get; } + + bool SupportsToStringWithFormat { get; } + + bool SupportsStringEqualsIgnoreCase { get; } + + bool SupportsStringToEnumConversion { get; } + + bool SupportsGetValueOrDefault { get; } + + bool SupportsComplexTypeToNullComparison { get; } + + bool SupportsNonEntityNullConstants { get; } + + bool SupportsEnumerableMaterialisation { get; } + + Expression ConvertToStringCall(MethodCallExpression call); + + Expression ConvertTryParseCall(MethodCallExpression call, Expression fallbackValue); + + Expression ConvertStringEqualsIgnoreCase(MethodCallExpression call); + + Expression ConvertStringToEnumConversion(ConditionalExpression conversion); + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Settings/QueryProviderSettings.cs b/AgileMapper/Queryables/Settings/QueryProviderSettings.cs new file mode 100644 index 000000000..2c38f6f83 --- /dev/null +++ b/AgileMapper/Queryables/Settings/QueryProviderSettings.cs @@ -0,0 +1,39 @@ +namespace AgileObjects.AgileMapper.Queryables.Settings +{ + using System; + using EntityFramework; + using NetStandardPolyfills; + + internal static class QueryProviderSettings + { + private static readonly IQueryProviderSettings _ef5Settings = new Ef5QueryProviderSettings(); + private static readonly IQueryProviderSettings _efCore2Settings = new EfCore2QueryProviderSettings(); + private static readonly IQueryProviderSettings _defaultSettings = new DefaultQueryProviderSettings(); + + public static IQueryProviderSettings For(Type queryProviderType) + { + var queryProviderAssembly = queryProviderType.GetAssembly(); + var queryableProviderAssemblyName = queryProviderAssembly.GetName(); + var queryableProviderName = queryableProviderAssemblyName.FullName; + + if (queryableProviderName.Contains("EntityFrameworkCore")) + { + return _efCore2Settings; + } + + if (queryableProviderName.Contains("EntityFramework")) + { + switch (queryableProviderAssemblyName.Version.Major) + { + case 5: + return _ef5Settings; + + case 6: + return new Ef6QueryProviderSettings(queryProviderAssembly); + } + } + + return _defaultSettings; + } + } +} \ No newline at end of file diff --git a/AgileMapper/Queryables/Settings/QueryProviderSettingsExtensions.cs b/AgileMapper/Queryables/Settings/QueryProviderSettingsExtensions.cs new file mode 100644 index 000000000..24ff28ab2 --- /dev/null +++ b/AgileMapper/Queryables/Settings/QueryProviderSettingsExtensions.cs @@ -0,0 +1,20 @@ +namespace AgileObjects.AgileMapper.Queryables.Settings +{ + using ObjectPopulation; + + internal static class QueryProviderSettingsExtensions + { + public static IQueryProviderSettings GetQueryProviderSettings(this IObjectMappingData mappingData) + { + while (!mappingData.IsRoot) + { + mappingData = mappingData.Parent; + } + + var queryProviderType = ((QueryProjectorKey)mappingData.MapperKey).QueryProviderType; + var providerSettings = QueryProviderSettings.For(queryProviderType); + + return providerSettings; + } + } +} \ No newline at end of file diff --git a/AgileMapper/SimpleMappingContext.cs b/AgileMapper/SimpleMappingContext.cs index d675ff661..64850e413 100644 --- a/AgileMapper/SimpleMappingContext.cs +++ b/AgileMapper/SimpleMappingContext.cs @@ -12,5 +12,7 @@ public SimpleMappingContext(MappingRuleSet ruleSet, MapperContext mapperContext) public MapperContext MapperContext { get; } public MappingRuleSet RuleSet { get; } + + public bool AddUnsuccessfulMemberPopulations { get; set; } } } \ No newline at end of file diff --git a/AgileMapper/TypeConversion/ToBoolConverter.cs b/AgileMapper/TypeConversion/ToBoolConverter.cs index 7abee406b..db029147a 100644 --- a/AgileMapper/TypeConversion/ToBoolConverter.cs +++ b/AgileMapper/TypeConversion/ToBoolConverter.cs @@ -12,7 +12,7 @@ internal class ToBoolConverter : ValueConverterBase { private static readonly Type[] _supportedSourceTypes = Constants .NumericTypes - .Append(typeof(bool), typeof(string), typeof(object), typeof(char)); + .Append(typeof(bool)); private readonly ToStringConverter _toStringConverter; @@ -22,7 +22,11 @@ public ToBoolConverter(ToStringConverter toStringConverter) } public override bool CanConvert(Type nonNullableSourceType, Type nonNullableTargetType) - => (nonNullableTargetType == typeof(bool)) && _supportedSourceTypes.Contains(nonNullableSourceType); + { + return (nonNullableTargetType == typeof(bool)) && + ((_supportedSourceTypes.Contains(nonNullableSourceType)) || + _toStringConverter.HasNativeStringRepresentation(nonNullableSourceType)); + } public override Expression GetConversion(Expression sourceValue, Type targetType) { @@ -102,7 +106,7 @@ private Expression GetSourceEqualsTests(Expression sourceValue, IEnumerable nonNullableTargetType == typeof(string); + public override bool CanConvert(Type nonNullableSourceType, Type nonNullableTargetType) + => nonNullableTargetType == typeof(string); + + public bool HasNativeStringRepresentation(Type nonNullableType) + { + return (nonNullableType == typeof(string)) || + (nonNullableType == typeof(object)) || + nonNullableType.IsEnum() || + (nonNullableType == typeof(char)) || + HasToStringOperator(nonNullableType, out var _); + } + + private static bool HasToStringOperator(Type nonNullableSourceType, out MethodInfo operatorMethod) + { + operatorMethod = nonNullableSourceType + .GetOperators(o => o.To()) + .FirstOrDefault(); + + return operatorMethod != null; + } public override Expression GetConversion(Expression sourceValue, Type targetType) { @@ -20,6 +39,11 @@ public override Expression GetConversion(Expression sourceValue, Type targetType public Expression GetConversion(Expression sourceValue) { + if (sourceValue.Type == typeof(string)) + { + return sourceValue; + } + if (sourceValue.Type == typeof(byte[])) { return GetByteArrayToBase64StringConversion(sourceValue); @@ -32,26 +56,17 @@ public Expression GetConversion(Expression sourceValue) return GetDateTimeToStringConversion(sourceValue, nonNullableSourceType); } + if (nonNullableSourceType == typeof(bool)) + { + return GetBoolToStringConversion(sourceValue, nonNullableSourceType); + } + if (HasToStringOperator(nonNullableSourceType, out var operatorMethod)) { return Expression.Call(operatorMethod, sourceValue); } - var toStringMethod = sourceValue.Type - .GetPublicInstanceMethod("ToString", parameterCount: 0); - - var toStringCall = Expression.Call(sourceValue, toStringMethod); - - return toStringCall; - } - - public bool HasToStringOperator(Type nonNullableSourceType, out MethodInfo operatorMethod) - { - operatorMethod = nonNullableSourceType - .GetOperators(o => o.To()) - .FirstOrDefault(); - - return operatorMethod != null; + return sourceValue.WithToStringCall(); } #region Byte[] Conversion @@ -96,5 +111,28 @@ public static MethodInfo GetToStringMethodOrNull(Type sourceType, Type argumentT return toStringMethod; } + + private static Expression GetBoolToStringConversion(Expression sourceValue, Type nonNullableSourceType) + { + if (sourceValue.Type == nonNullableSourceType) + { + return GetTrueOrFalseTernary(sourceValue); + } + + var nullTrueOrFalse = Expression.Condition( + Expression.Property(sourceValue, "HasValue"), + GetTrueOrFalseTernary(Expression.Property(sourceValue, "Value")), + typeof(string).ToDefaultExpression()); + + return nullTrueOrFalse; + } + + private static Expression GetTrueOrFalseTernary(Expression sourceValue) + { + return Expression.Condition( + sourceValue, + "true".ToConstantExpression(), + "false".ToConstantExpression()); + } } } \ No newline at end of file diff --git a/AgileMapper/TypeConversion/TryParseConverter.cs b/AgileMapper/TypeConversion/TryParseConverter.cs index d4e0f8d50..5aef9833f 100644 --- a/AgileMapper/TypeConversion/TryParseConverter.cs +++ b/AgileMapper/TypeConversion/TryParseConverter.cs @@ -34,9 +34,7 @@ public override bool CanConvert(Type nonNullableSourceType, Type nonNullableTarg protected virtual bool CanConvert(Type nonNullableSourceType) { return (nonNullableSourceType == _nonNullableTargetType) || - (nonNullableSourceType == typeof(string)) || - (nonNullableSourceType == typeof(object)) || - _toStringConverter.HasToStringOperator(nonNullableSourceType, out var _); + _toStringConverter.HasNativeStringRepresentation(nonNullableSourceType); } public override Expression GetConversion(Expression sourceValue, Type targetType) diff --git a/CommonAssemblyInfo.cs b/CommonAssemblyInfo.cs index 5ebe21f74..fced7121b 100644 --- a/CommonAssemblyInfo.cs +++ b/CommonAssemblyInfo.cs @@ -1,10 +1,7 @@ -using System; -using System.Reflection; +using System.Reflection; using System.Resources; [assembly: AssemblyCompany("AgileObjects Ltd")] [assembly: AssemblyProduct("AgileObjects.AgileMapper")] [assembly: AssemblyCopyright("Copyright © AgileObjects Ltd 2018")] -[assembly: NeutralResourcesLanguage("en")] - -[assembly: CLSCompliant(true)] \ No newline at end of file +[assembly: NeutralResourcesLanguage("en")] \ No newline at end of file