Skip to content

Commit

Permalink
Merge pull request MapsterMapper#18 from chaowlert/explicit-mapping
Browse files Browse the repository at this point in the history
Explicit mapping
  • Loading branch information
eswann committed Jan 21, 2016
2 parents 9fa6133 + 77193c9 commit 61faf22
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 36 deletions.
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ or just

var destObject = TypeAdapter.Adapt<TDestination>(sourceObject);

or using extension methods

var destObject = sourceObject.Adapt<TDestination>();

#####Mapping to an existing object <a name="MappingToTarget"></a>
You make the object, Mapster maps to the object.

Expand Down Expand Up @@ -173,9 +177,9 @@ This includes mapping among lists, arrays, collections, dictionary including var

####Setting <a name="Setting"></a>
#####Setting per type <a name="SettingPerType"></a>
You can easily create setting for type mapping by `TypeAdapterConfig<TSource, TDestination>().NewConfig()`
You can easily create setting for type mapping by `TypeAdapterConfig<TSource, TDestination>.NewConfig()`

TypeAdapterConfig<TSource, TDestination>()
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.Ignore(dest => dest.Age)
.Map(dest => dest.FullName,
Expand Down Expand Up @@ -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<TSource, TDestination>()
config.ForType<TSource, TDestination>()
.Map(dest => dest.FullName,
src => string.Format("{0} {1}", src.FirstName, src.LastName));

Expand All @@ -254,56 +258,56 @@ When the default convention mappings aren't enough to do the job, you can specif
#####Ignore Members & Attributes <a name="Ignore"></a>
Mapster will automatically map properties with the same names. You can ignore members by using `Ignore` method.

TypeAdapterConfig<TSource, TDestination>()
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.Ignore(dest => dest.Id);

You can ignore members annotated with specific attribute by using `IgnoreAttribute` method.

TypeAdapterConfig<TSource, TDestination>()
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.IgnoreAttribute(typeof(JsonIgnoreAttribute));

#####Property mapping <a name="Map"></a>
You can customize how Mapster maps value to property.

TypeAdapterConfig<TSource, TDestination>()
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.Map(dest => dest.FullName,
src => string.Format("{0} {1}", src.FirstName, src.LastName));

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<TSource, TDestination>()
TypeAdapterConfig<TSource, TDestination>
.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<TSource, TDestination>()
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.Map(dest => dest.Gender, //Genders.Male or Genders.Female
src => src.GenderString); //"Male" or "Female"

#####Merge object <a name="Merge"></a>
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<TSource, TDestination>()
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.IgnoreNullValues(true);

#####Shallow copy <a name="ShallowCopy"></a>
By default, Mapster will recursively map nested objects. You can do shallow copying by setting `ShallowCopyForSameType` to `true`.

TypeAdapterConfig<TSource, TDestination>()
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.ShallowCopyForSameType(true);

#####Preserve reference (preventing circular reference stackoverflow) <a name="PreserveReference"></a>
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<TSource, TDestination>()
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.PreserveReference(true);

Expand Down
1 change: 1 addition & 0 deletions src/Mapster.Tests/Mapster.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
<Compile Include="Classes\Customer.cs" />
<Compile Include="Classes\Product.cs" />
<Compile Include="Classes\TypeTestClass.cs" />
<Compile Include="WhenMappingWithExtensionMethods.cs" />
<Compile Include="WhenPreserveReferences.cs" />
<Compile Include="WhenAddingCustomMappings.cs" />
<Compile Include="WhenAddingPrimitiveTypes.cs" />
Expand Down
27 changes: 22 additions & 5 deletions src/Mapster.Tests/WhenExplicitMappingRequired.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CollectionPoco, CollectionDto>.NewConfig();
TypeAdapterConfig<SimplePoco, SimpleDto>.NewConfig();

var collectionPoco = new CollectionPoco { Id = Guid.NewGuid(), Name = "TestName", Children = new List<ChildPoco>() };
var simplePocos = new[]
{
new SimplePoco {Id = Guid.NewGuid(), Name = "TestName"}
};

var collectionDto = TypeAdapter.Adapt<CollectionPoco, CollectionDto>(collectionPoco);
var simpleDtos = TypeAdapter.Adapt<SimplePoco[], List<SimpleDto>>(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<CollectionPoco, CollectionDto>.NewConfig();

// var collectionPoco = new CollectionPoco { Id = Guid.NewGuid(), Name = "TestName", Children = new List<ChildPoco>() };

// var collectionDto = TypeAdapter.Adapt<CollectionPoco, CollectionDto>(collectionPoco);

// collectionDto.Name.ShouldEqual(collectionPoco.Name);
//}


#region TestClasses

Expand Down
73 changes: 73 additions & 0 deletions src/Mapster.Tests/WhenMappingWithExtensionMethods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using Mapster.Tests.Classes;
using NUnit.Framework;
using Should;

namespace Mapster.Tests
{
/// <summary>
/// Not trying to test core testing here...just a few tests to make sure the extension method approach doesn't hose anything
/// </summary>
[TestFixture]
public class WhenMappingWithExtensionMethods
{

[Test]
public void Adapt_With_Source_And_Destination_Type_Succeeds()
{
TypeAdapterConfig<Product, ProductDTO>.NewConfig()
.Compile();

var product = new Product { Id = Guid.NewGuid(), Title = "ProductA", CreatedUser = new User { Name = "UserA" } };

var dto = product.Adapt<Product, ProductDTO>();

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<Product, ProductDTO>();


var product = new Product { Id = Guid.NewGuid(), Title = "ProductA", CreatedUser = new User { Name = "UserA" } };

var dto = product.Adapt<Product, ProductDTO>(config);

dto.ShouldNotBeNull();
dto.Id.ShouldEqual(product.Id);
}

[Test]
public void Adapt_With_Destination_Type_Succeeds()
{
TypeAdapterConfig<Product, ProductDTO>.NewConfig()
.Compile();

var product = new Product { Id = Guid.NewGuid(), Title = "ProductA", CreatedUser = new User { Name = "UserA" } };

var dto = product.Adapt<ProductDTO>();

dto.ShouldNotBeNull();
dto.Id.ShouldEqual(product.Id);
}

[Test]
public void Adapt_With_Destination_Type_And_Config_Succeeds()
{
var config = new TypeAdapterConfig();
config.ForType<Product, ProductDTO>();


var product = new Product { Id = Guid.NewGuid(), Title = "ProductA", CreatedUser = new User { Name = "UserA" } };

var dto = product.Adapt<ProductDTO>(config);

dto.ShouldNotBeNull();
dto.Id.ShouldEqual(product.Id);
}
}
}
9 changes: 6 additions & 3 deletions src/Mapster.Tests/WhenMappingWithImplicitInheritance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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();
Expand Down
11 changes: 11 additions & 0 deletions src/Mapster/Adapters/ClassAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/Mapster/MapContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public int GetHashCode(object obj)
return RuntimeHelpers.GetHashCode(obj);
}
}
public class MapContext
internal class MapContext
{
[ThreadStatic]
private static MapContext _context;
Expand Down
12 changes: 6 additions & 6 deletions src/Mapster/TypeAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static class TypeAdapter
/// <param name="source">Source object to adapt.</param>
/// <param name="config">Configuration</param>
/// <returns>Adapted destination type.</returns>
public static TDestination Adapt<TDestination>(object source, TypeAdapterConfig config = null)
public static TDestination Adapt<TDestination>(this object source, TypeAdapterConfig config = null)
{
config = config ?? TypeAdapterConfig.GlobalSettings;
dynamic fn = config.GetMapFunction(source.GetType(), typeof(TDestination));
Expand All @@ -32,7 +32,7 @@ public static TDestination Adapt<TDestination>(object source, TypeAdapterConfig
/// <typeparam name="TDestination">Destination type.</typeparam>
/// <param name="source">Source object to adapt.</param>
/// <returns>Adapted destination type.</returns>
public static TDestination Adapt<TSource, TDestination>(TSource source)
public static TDestination Adapt<TSource, TDestination>(this TSource source)
{
try
{
Expand All @@ -52,7 +52,7 @@ public static TDestination Adapt<TDestination>(object source, TypeAdapterConfig
/// <param name="source">Source object to adapt.</param>
/// <param name="config">Configuration</param>
/// <returns>Adapted destination type.</returns>
public static TDestination Adapt<TSource, TDestination>(TSource source, TypeAdapterConfig config)
public static TDestination Adapt<TSource, TDestination>(this TSource source, TypeAdapterConfig config)
{
var fn = config.GetMapFunction<TSource, TDestination>();
try
Expand All @@ -74,7 +74,7 @@ public static TDestination Adapt<TDestination>(object source, TypeAdapterConfig
/// <param name="destination">The destination object to populate.</param>
/// <param name="config">Configuration</param>
/// <returns>Adapted destination type.</returns>
public static TDestination Adapt<TSource, TDestination>(TSource source, TDestination destination, TypeAdapterConfig config = null)
public static TDestination Adapt<TSource, TDestination>(this TSource source, TDestination destination, TypeAdapterConfig config = null)
{
config = config ?? TypeAdapterConfig.GlobalSettings;
var fn = config.GetMapToTargetFunction<TSource, TDestination>();
Expand All @@ -97,7 +97,7 @@ public static TDestination Adapt<TDestination>(object source, TypeAdapterConfig
/// <param name="destinationType">The type of the destination object.</param>
/// <param name="config">Configuration</param>
/// <returns>Adapted destination type.</returns>
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);
Expand All @@ -120,7 +120,7 @@ public static object Adapt(object source, Type sourceType, Type destinationType,
/// <param name="destinationType">The type of the destination object.</param>
/// <param name="config">Configuration</param>
/// <returns>Adapted destination type.</returns>
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);
Expand Down
16 changes: 7 additions & 9 deletions src/Mapster/TypeAdapterConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -255,6 +256,7 @@ private LambdaExpression CreateMapExpression(Type sourceType, Type destinationTy
MapType = mapType,
Context = context,
Settings = setting,
IsInvalidRequiredExplicitMapping = invalidRequireExclicitMapping,
};
return fn(arg);
}
Expand Down Expand Up @@ -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)
{
Expand All @@ -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<TypeAdapterRule>()
Expand Down
1 change: 1 addition & 0 deletions src/Mapster/TypeAdapterSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public class CompileArgument
public MapType MapType;
public TypeAdapterSettings Settings;
public CompileContext Context;
public bool IsInvalidRequiredExplicitMapping;
}

public class CompileContext
Expand Down

0 comments on commit 61faf22

Please sign in to comment.