diff --git a/README.md b/README.md index f8e6fe97..a946e930 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,10 @@ or just var destObject = TypeAdapter.Adapt(sourceObject); +or using extension methods + + var destObject = sourceObject.Adapt(); + #####Mapping to an existing object You make the object, Mapster maps to the object. @@ -173,9 +177,9 @@ This includes mapping among lists, arrays, collections, dictionary including var ####Setting #####Setting per type -You can easily create setting for type mapping by `TypeAdapterConfig().NewConfig()` +You can easily create setting for type mapping by `TypeAdapterConfig.NewConfig()` - TypeAdapterConfig() + TypeAdapterConfig .NewConfig() .Ignore(dest => dest.Age) .Map(dest => dest.FullName, @@ -233,9 +237,9 @@ You may wish to have different settings in different scenarios. If you would not var config = new TypeAdapterConfig(); config.Default.Ignore("Id"); -For type mapping, you can use `OfType` method. +For type mapping, you can use `ForType` method. - config.OfType() + config.ForType() .Map(dest => dest.FullName, src => string.Format("{0} {1}", src.FirstName, src.LastName)); @@ -254,20 +258,20 @@ When the default convention mappings aren't enough to do the job, you can specif #####Ignore Members & Attributes Mapster will automatically map properties with the same names. You can ignore members by using `Ignore` method. - TypeAdapterConfig() + TypeAdapterConfig .NewConfig() .Ignore(dest => dest.Id); You can ignore members annotated with specific attribute by using `IgnoreAttribute` method. - TypeAdapterConfig() + TypeAdapterConfig .NewConfig() .IgnoreAttribute(typeof(JsonIgnoreAttribute)); #####Property mapping You can customize how Mapster maps value to property. - TypeAdapterConfig() + TypeAdapterConfig .NewConfig() .Map(dest => dest.FullName, src => string.Format("{0} {1}", src.FirstName, src.LastName)); @@ -275,13 +279,13 @@ You can customize how Mapster maps value to property. The Map configuration can accept a third parameter that provides a condition based on the source. If the condition is not met, the mapping is skipped altogether. - TypeAdapterConfig() + TypeAdapterConfig .NewConfig() .Map(dest => dest.FullName, src => src.FullName, srcCond => srcCond.City == "Victoria"); In Mapster 2.0, you can map even type of source and destination properties are different. - TypeAdapterConfig() + TypeAdapterConfig .NewConfig() .Map(dest => dest.Gender, //Genders.Male or Genders.Female src => src.GenderString); //"Male" or "Female" @@ -289,21 +293,21 @@ In Mapster 2.0, you can map even type of source and destination properties are d #####Merge object By default, Mapster will map all properties, even source properties contains null value. You can copy only properties that have value by using `IgnoreNullValues` method. - TypeAdapterConfig() + TypeAdapterConfig .NewConfig() .IgnoreNullValues(true); #####Shallow copy By default, Mapster will recursively map nested objects. You can do shallow copying by setting `ShallowCopyForSameType` to `true`. - TypeAdapterConfig() + TypeAdapterConfig .NewConfig() .ShallowCopyForSameType(true); #####Preserve reference (preventing circular reference stackoverflow) When you map circular reference objects, there will be stackoverflow exception. This is because Mapster will try to recursively map all objects in circular. If you would like to map circular reference objects, or preserve references (such as 2 properties point to the same object), you can do it by setting `PreserveReference` to `true` - TypeAdapterConfig() + TypeAdapterConfig .NewConfig() .PreserveReference(true); diff --git a/src/Mapster.Tests/Mapster.Tests.csproj b/src/Mapster.Tests/Mapster.Tests.csproj index 11541e23..624dddc9 100644 --- a/src/Mapster.Tests/Mapster.Tests.csproj +++ b/src/Mapster.Tests/Mapster.Tests.csproj @@ -66,6 +66,7 @@ + diff --git a/src/Mapster.Tests/WhenExplicitMappingRequired.cs b/src/Mapster.Tests/WhenExplicitMappingRequired.cs index e91860b2..281bc4cb 100644 --- a/src/Mapster.Tests/WhenExplicitMappingRequired.cs +++ b/src/Mapster.Tests/WhenExplicitMappingRequired.cs @@ -55,19 +55,36 @@ public void Mapped_Classes_Succeed() } [Test] - public void Mapped_Classes_Succeed_With_Child_Mapping() + public void Mapped_List_Of_Classes_Succeed() { TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = true; - TypeAdapterConfig.NewConfig(); + TypeAdapterConfig.NewConfig(); - var collectionPoco = new CollectionPoco { Id = Guid.NewGuid(), Name = "TestName", Children = new List() }; + var simplePocos = new[] + { + new SimplePoco {Id = Guid.NewGuid(), Name = "TestName"} + }; - var collectionDto = TypeAdapter.Adapt(collectionPoco); + var simpleDtos = TypeAdapter.Adapt>(simplePocos); - collectionDto.Name.ShouldEqual(collectionPoco.Name); + simpleDtos[0].Name.ShouldEqual(simplePocos[0].Name); } + //[Test] + //public void Mapped_Classes_Succeed_With_Child_Mapping() + //{ + // TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = true; + + // TypeAdapterConfig.NewConfig(); + + // var collectionPoco = new CollectionPoco { Id = Guid.NewGuid(), Name = "TestName", Children = new List() }; + + // var collectionDto = TypeAdapter.Adapt(collectionPoco); + + // collectionDto.Name.ShouldEqual(collectionPoco.Name); + //} + #region TestClasses diff --git a/src/Mapster.Tests/WhenMappingWithExtensionMethods.cs b/src/Mapster.Tests/WhenMappingWithExtensionMethods.cs new file mode 100644 index 00000000..f5d34c0a --- /dev/null +++ b/src/Mapster.Tests/WhenMappingWithExtensionMethods.cs @@ -0,0 +1,73 @@ +using System; +using Mapster.Tests.Classes; +using NUnit.Framework; +using Should; + +namespace Mapster.Tests +{ + /// + /// Not trying to test core testing here...just a few tests to make sure the extension method approach doesn't hose anything + /// + [TestFixture] + public class WhenMappingWithExtensionMethods + { + + [Test] + public void Adapt_With_Source_And_Destination_Type_Succeeds() + { + TypeAdapterConfig.NewConfig() + .Compile(); + + var product = new Product { Id = Guid.NewGuid(), Title = "ProductA", CreatedUser = new User { Name = "UserA" } }; + + var dto = product.Adapt(); + + dto.ShouldNotBeNull(); + dto.Id.ShouldEqual(product.Id); + } + + [Test] + public void Adapt_With_Source_And_Destination_Types_And_Config_Succeeds() + { + var config = new TypeAdapterConfig(); + config.ForType(); + + + var product = new Product { Id = Guid.NewGuid(), Title = "ProductA", CreatedUser = new User { Name = "UserA" } }; + + var dto = product.Adapt(config); + + dto.ShouldNotBeNull(); + dto.Id.ShouldEqual(product.Id); + } + + [Test] + public void Adapt_With_Destination_Type_Succeeds() + { + TypeAdapterConfig.NewConfig() + .Compile(); + + var product = new Product { Id = Guid.NewGuid(), Title = "ProductA", CreatedUser = new User { Name = "UserA" } }; + + var dto = product.Adapt(); + + dto.ShouldNotBeNull(); + dto.Id.ShouldEqual(product.Id); + } + + [Test] + public void Adapt_With_Destination_Type_And_Config_Succeeds() + { + var config = new TypeAdapterConfig(); + config.ForType(); + + + var product = new Product { Id = Guid.NewGuid(), Title = "ProductA", CreatedUser = new User { Name = "UserA" } }; + + var dto = product.Adapt(config); + + dto.ShouldNotBeNull(); + dto.Id.ShouldEqual(product.Id); + } + } +} diff --git a/src/Mapster.Tests/WhenMappingWithImplicitInheritance.cs b/src/Mapster.Tests/WhenMappingWithImplicitInheritance.cs index 19b60c23..c0df3bd4 100644 --- a/src/Mapster.Tests/WhenMappingWithImplicitInheritance.cs +++ b/src/Mapster.Tests/WhenMappingWithImplicitInheritance.cs @@ -181,7 +181,8 @@ public void Derived_Config_Shares_Base_Config_Properties() //.MaxDepth(5) .Compile(); - var derivedConfig = TypeAdapterConfig.GlobalSettings.GetMergedSettings(typeof(DerivedPoco), typeof(SimpleDto), MapType.Map); + bool invalid; + var derivedConfig = TypeAdapterConfig.GlobalSettings.GetMergedSettings(typeof(DerivedPoco), typeof(SimpleDto), MapType.Map, out invalid); derivedConfig.IgnoreNullValues.ShouldEqual(true); derivedConfig.ShallowCopyForSameType.ShouldEqual(true); @@ -198,7 +199,8 @@ public void Derived_Config_Shares_Base_Dest_Config_Properties() //.MaxDepth(5) .Compile(); - var derivedConfig = TypeAdapterConfig.GlobalSettings.GetMergedSettings(typeof(DerivedPoco), typeof(DerivedDto), MapType.Map); + bool invalid; + var derivedConfig = TypeAdapterConfig.GlobalSettings.GetMergedSettings(typeof(DerivedPoco), typeof(DerivedDto), MapType.Map, out invalid); derivedConfig.IgnoreNullValues.ShouldEqual(true); derivedConfig.ShallowCopyForSameType.ShouldEqual(true); @@ -215,7 +217,8 @@ public void Derived_Config_Doesnt_Share_Base_Dest_Config_Properties_If_Disabled( //.MaxDepth(5) .Compile(); - var derivedConfig = TypeAdapterConfig.GlobalSettings.GetMergedSettings(typeof(DerivedPoco), typeof(DerivedDto), MapType.Map); + bool invalid; + var derivedConfig = TypeAdapterConfig.GlobalSettings.GetMergedSettings(typeof(DerivedPoco), typeof(DerivedDto), MapType.Map, out invalid); derivedConfig.IgnoreNullValues.ShouldBeNull(); derivedConfig.ShallowCopyForSameType.ShouldBeNull(); diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 1eb37a17..ff62e170 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -28,6 +28,17 @@ internal class ClassAdapter : BaseAdapter return -150; } + protected override Expression CreateExpressionBody(Expression source, Expression destination, CompileArgument arg) + { + if (arg.IsInvalidRequiredExplicitMapping) + { + throw new InvalidOperationException( + $"Implicit mapping is not allowed (check GlobalSettings.RequireExplicitMapping) and no configuration exists for the following mapping: TSource: {arg.SourceType} TDestination: {arg.DestinationType}"); + } + + return base.CreateExpressionBody(source, destination, arg); + } + protected override Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg) { var properties = CreateAdapterModel(source, destination, arg); diff --git a/src/Mapster/MapContext.cs b/src/Mapster/MapContext.cs index 07e1ee59..9721f075 100644 --- a/src/Mapster/MapContext.cs +++ b/src/Mapster/MapContext.cs @@ -18,7 +18,7 @@ public int GetHashCode(object obj) return RuntimeHelpers.GetHashCode(obj); } } - public class MapContext + internal class MapContext { [ThreadStatic] private static MapContext _context; diff --git a/src/Mapster/TypeAdapter.cs b/src/Mapster/TypeAdapter.cs index 21e9a6c4..2966c17c 100644 --- a/src/Mapster/TypeAdapter.cs +++ b/src/Mapster/TypeAdapter.cs @@ -11,7 +11,7 @@ public static class TypeAdapter /// Source object to adapt. /// Configuration /// Adapted destination type. - public static TDestination Adapt(object source, TypeAdapterConfig config = null) + public static TDestination Adapt(this object source, TypeAdapterConfig config = null) { config = config ?? TypeAdapterConfig.GlobalSettings; dynamic fn = config.GetMapFunction(source.GetType(), typeof(TDestination)); @@ -32,7 +32,7 @@ public static TDestination Adapt(object source, TypeAdapterConfig /// Destination type. /// Source object to adapt. /// Adapted destination type. - public static TDestination Adapt(TSource source) + public static TDestination Adapt(this TSource source) { try { @@ -52,7 +52,7 @@ public static TDestination Adapt(object source, TypeAdapterConfig /// Source object to adapt. /// Configuration /// Adapted destination type. - public static TDestination Adapt(TSource source, TypeAdapterConfig config) + public static TDestination Adapt(this TSource source, TypeAdapterConfig config) { var fn = config.GetMapFunction(); try @@ -74,7 +74,7 @@ public static TDestination Adapt(object source, TypeAdapterConfig /// The destination object to populate. /// Configuration /// Adapted destination type. - public static TDestination Adapt(TSource source, TDestination destination, TypeAdapterConfig config = null) + public static TDestination Adapt(this TSource source, TDestination destination, TypeAdapterConfig config = null) { config = config ?? TypeAdapterConfig.GlobalSettings; var fn = config.GetMapToTargetFunction(); @@ -97,7 +97,7 @@ public static TDestination Adapt(object source, TypeAdapterConfig /// The type of the destination object. /// Configuration /// Adapted destination type. - public static object Adapt(object source, Type sourceType, Type destinationType, TypeAdapterConfig config = null) + public static object Adapt(this object source, Type sourceType, Type destinationType, TypeAdapterConfig config = null) { config = config ?? TypeAdapterConfig.GlobalSettings; dynamic fn = config.GetMapFunction(sourceType, destinationType); @@ -120,7 +120,7 @@ public static object Adapt(object source, Type sourceType, Type destinationType, /// The type of the destination object. /// Configuration /// Adapted destination type. - public static object Adapt(object source, object destination, Type sourceType, Type destinationType, TypeAdapterConfig config = null) + public static object Adapt(this object source, object destination, Type sourceType, Type destinationType, TypeAdapterConfig config = null) { config = config ?? TypeAdapterConfig.GlobalSettings; dynamic fn = config.GetMapToTargetFunction(sourceType, destinationType); diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index ff65111f..cacd0fa3 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -235,7 +235,8 @@ private LambdaExpression CreateProjectionExpression(TypeTuple tuple) private LambdaExpression CreateMapExpression(Type sourceType, Type destinationType, MapType mapType, CompileContext context) { - var setting = GetMergedSettings(sourceType, destinationType, mapType); + bool invalidRequireExclicitMapping; + var setting = GetMergedSettings(sourceType, destinationType, mapType, out invalidRequireExclicitMapping); var fn = mapType == MapType.MapToTarget ? setting.ConverterToTargetFactory : setting.ConverterFactory; @@ -255,6 +256,7 @@ private LambdaExpression CreateMapExpression(Type sourceType, Type destinationTy MapType = mapType, Context = context, Settings = setting, + IsInvalidRequiredExplicitMapping = invalidRequireExclicitMapping, }; return fn(arg); } @@ -286,10 +288,6 @@ internal LambdaExpression CreateInlineMapExpression(Type sourceType, Type destin private LambdaExpression CreateInvokeExpression(Type sourceType, Type destinationType) { - //ensure there is MapContext to prevent error on GetMergedSettings - if (this.RequireExplicitMapping) - MapContext.EnsureContext(); - Expression invoker; if (this == GlobalSettings) { @@ -308,13 +306,13 @@ private LambdaExpression CreateInvokeExpression(Type sourceType, Type destinatio return Expression.Lambda(invoke, p); } - internal TypeAdapterSettings GetMergedSettings(Type sourceType, Type destinationType, MapType mapType) + internal TypeAdapterSettings GetMergedSettings(Type sourceType, Type destinationType, MapType mapType, out bool invalidRequireExplicitMapping) { - if (this.RequireExplicitMapping && mapType != MapType.InlineMap && !MapContext.HasContext) + invalidRequireExplicitMapping = false; + if (this.RequireExplicitMapping) { if (!this.Dict.ContainsKey(new TypeTuple(sourceType, destinationType))) - throw new InvalidOperationException( - $"Implicit mapping is not allowed (check GlobalSettings.RequireExplicitMapping) and no configuration exists for the following mapping: TSource: {sourceType} TDestination: {destinationType}"); + invalidRequireExplicitMapping = true; } var settings = (from rule in this.Rules.Reverse() diff --git a/src/Mapster/TypeAdapterSettings.cs b/src/Mapster/TypeAdapterSettings.cs index e39e8ec1..85e41a01 100644 --- a/src/Mapster/TypeAdapterSettings.cs +++ b/src/Mapster/TypeAdapterSettings.cs @@ -77,6 +77,7 @@ public class CompileArgument public MapType MapType; public TypeAdapterSettings Settings; public CompileContext Context; + public bool IsInvalidRequiredExplicitMapping; } public class CompileContext