Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Value transformers in global, profiles, type maps, and members #2261

Merged
merged 10 commits into from Sep 5, 2017
1 change: 1 addition & 0 deletions src/AutoMapper/Configuration/IProfileConfiguration.cs
Expand Up @@ -44,5 +44,6 @@ public interface IProfileConfiguration
INamingConvention DestinationMemberNamingConvention { get; }
IEnumerable<ITypeMapConfiguration> TypeMapConfigs { get; }
IEnumerable<ITypeMapConfiguration> OpenTypeMapConfigs { get; }
IEnumerable<ValueTransformerConfiguration> ValueTransformers { get; }
}
}
15 changes: 15 additions & 0 deletions src/AutoMapper/Configuration/MappingExpression.cs
Expand Up @@ -134,6 +134,7 @@ public class MappingExpression<TSource, TDestination> : IMappingExpression<TSour
private readonly List<IPropertyMapConfiguration> _memberConfigurations = new List<IPropertyMapConfiguration>();
private readonly List<SourceMappingExpression> _sourceMemberConfigurations = new List<SourceMappingExpression>();
private readonly List<CtorParamConfigurationExpression<TSource>> _ctorParamConfigurations = new List<CtorParamConfigurationExpression<TSource>>();
private readonly List<ValueTransformerConfiguration> _valueTransformers = new List<ValueTransformerConfiguration>();
private MappingExpression<TDestination, TSource> _reverseMap;
private Action<IMemberConfigurationExpression<TSource, TDestination, object>> _allMemberOptions;
private Func<MemberInfo, bool> _memberFilter;
Expand All @@ -156,6 +157,7 @@ public MappingExpression(MemberList memberList, Type sourceType, Type destinatio
public Type DestinationType => Types.DestinationType;
public bool IsOpenGeneric { get; }
public ITypeMapConfiguration ReverseTypeMap => _reverseMap;
public IList<ValueTransformerConfiguration> ValueTransformers => _valueTransformers;
protected List<Action<TypeMap>> TypeMapActions { get; } = new List<Action<TypeMap>>();

public IMappingExpression<TSource, TDestination> PreserveReferences()
Expand Down Expand Up @@ -529,6 +531,15 @@ private void CheckIsDerived(Type derivedType, Type baseType)

return this;
}

public IMappingExpression<TSource, TDestination> AddTransform<TValue>(Expression<Func<TValue, TValue>> transformer)
{
var config = new ValueTransformerConfiguration(typeof(TValue), transformer);

_valueTransformers.Add(config);

return this;
}

private IPropertyMapConfiguration GetDestinationMemberConfiguration(MemberInfo destinationMember) =>
_memberConfigurations.FirstOrDefault(m => m.DestinationMember == destinationMember);
Expand Down Expand Up @@ -573,6 +584,10 @@ public void Configure(TypeMap typeMap)
{
paramConfig.Configure(typeMap);
}
foreach (var valueTransformer in _valueTransformers)
{
typeMap.AddValueTransformation(valueTransformer);
}

if (_reverseMap != null)
{
Expand Down
10 changes: 10 additions & 0 deletions src/AutoMapper/Configuration/MemberConfigurationExpression.cs
Expand Up @@ -233,6 +233,16 @@ public void PreCondition(Func<TSource, ResolutionContext, bool> condition)
});
}

public void AddTransform<TValue>(Expression<Func<TValue, TValue>> transformer)
{
PropertyMapActions.Add(pm =>
{
var config = new ValueTransformerConfiguration(typeof(TValue), transformer);

pm.AddValueTransformation(config);
});
}

public void ExplicitExpansion()
{
PropertyMapActions.Add(pm => pm.ExplicitExpansion = true);
Expand Down
10 changes: 8 additions & 2 deletions src/AutoMapper/Execution/TypeMapPlanBuilder.cs
Expand Up @@ -5,7 +5,7 @@ namespace AutoMapper.Execution
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using AutoMapper.Configuration;
using Configuration;
using static System.Linq.Expressions.Expression;
using static Internal.ExpressionFactory;
using static ExpressionBuilder;
Expand Down Expand Up @@ -468,7 +468,7 @@ private Expression BuildValueResolverFunc(PropertyMap propertyMap, Expression de
var returnType = destinationNullable && destinationPropertyType.GetTypeOfNullable() ==
nullCheckedExpression.Type
? destinationPropertyType
: nullCheckedExpression.Type;
: nullCheckedExpression.Type;
valueResolverFunc =
TryCatch(
ToType(nullCheckedExpression, returnType),
Expand Down Expand Up @@ -522,6 +522,12 @@ private Expression BuildValueResolverFunc(PropertyMap propertyMap, Expression de
);
}

valueResolverFunc = propertyMap.ValueTransformers
.Concat(_typeMap.ValueTransformers)
.Concat(_typeMap.Profile.ValueTransformers)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be a lot of transformers here, so a cache would help.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know much about transformers, but that sounded weird :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few hundred mappings, maybe a few thousand properties, it seems reasonable. For all the existing transformers. This will hurt eventually :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would we cache? The list could be different since you can define at the property map level.

Alternatively we could push the transformers down from the parent levels, kinda like we do for Include.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but not the transformers per member are the problem, all the rest are. And yes, I thought about smth like your second suggestion, but I'm not sure how that would look like, caching by type seemed more obvious.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will only be an issue with large amounts of value transformers. And I'm not sure that would really happen

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can wait :)

.Where(vt => vt.IsMatch(propertyMap))
.Aggregate(valueResolverFunc, (current, vtConfig) => ToType(ReplaceParameters(vtConfig.TransformerExpression, ToType(current, vtConfig.ValueType)), propertyMap.DestinationPropertyType));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this change to having ValueTransformerConfiguration have function likeMapExpression? Kinda like what IObjectMapper does now where it's an interface like IValueTransformerConfiguration.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's a further task on the list

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't going to make it like IObjectMapper though, more like IValueResolver and ITypeConverter. You can go the expression route, which can be used in LINQ etc. or the runtime route that can get access to ResolutionContext. That would also allow a DI-able version.


return valueResolverFunc;
}

Expand Down
14 changes: 14 additions & 0 deletions src/AutoMapper/IMappingExpression.cs
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace AutoMapper
Expand Down Expand Up @@ -181,6 +182,9 @@ IMappingExpression BeforeMap<TMappingAction>()
/// <returns>Itself</returns>
IMappingExpression AfterMap<TMappingAction>()
where TMappingAction : IMappingAction<object, object>;

IList<ValueTransformerConfiguration> ValueTransformers { get; }

}

/// <summary>
Expand Down Expand Up @@ -439,5 +443,15 @@ public interface IMappingExpression<TSource, TDestination>
/// </summary>
/// <returns>Itself</returns>
IMappingExpression<TSource, TDestination> DisableCtorValidation();

IList<ValueTransformerConfiguration> ValueTransformers { get; }

/// <summary>
/// Apply a transformation function after any resolved destination member value with the given type
/// </summary>
/// <typeparam name="TValue">Value type to match and transform</typeparam>
/// <param name="transformer">Transformation expression</param>
/// <returns>Itself</returns>
IMappingExpression<TSource, TDestination> AddTransform<TValue>(Expression<Func<TValue, TValue>> transformer);
}
}
9 changes: 8 additions & 1 deletion src/AutoMapper/IMemberConfigurationExpression.cs
Expand Up @@ -10,7 +10,7 @@ namespace AutoMapper
/// <typeparam name="TSource">Source type for this member</typeparam>
/// <typeparam name="TMember">Type for this member</typeparam>
/// <typeparam name="TDestination">Destination type for this map</typeparam>
public interface IMemberConfigurationExpression<TSource, out TDestination, TMember>
public interface IMemberConfigurationExpression<TSource, TDestination, TMember>
{
/// <summary>
/// Do not precompute the execution plan for this member, just map it at runtime.
Expand Down Expand Up @@ -199,6 +199,13 @@ void ResolveUsing<TValueResolver>()
/// The destination member being configured.
/// </summary>
MemberInfo DestinationMember { get; }

/// <summary>
/// Apply a transformation function after any resolved destination member value with the given type
/// </summary>
/// <typeparam name="TValue">Value type to match and transform</typeparam>
/// <param name="transformer">Transformation expression</param>
void AddTransform<TValue>(Expression<Func<TValue, TValue>> transformer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a non generic overload that just uses TMember.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we need an overload as much as just get rid of this version. I don't remember why I put a transform where you could specify a different type. Transformers get applied after resolving/mapping so it's always going to be of the destination member type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After resolving and before mapping. And I think you meant for TValue to be the type of the resolved value. Which doesn't have to be TMember. Right? :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah then I wonder if I have value transformations in the right place. I think it's really before you assign it, you transform it but I have it as part of resolving.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the value transformations to after mapping and right before setting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it. I've always thought AfterPropertyMap is a good idea :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha well maybe this will take care of people's scenarios.

}

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions src/AutoMapper/IProfileExpression.cs
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using AutoMapper.Configuration.Conventions;
using AutoMapper.Mappers;
Expand Down Expand Up @@ -152,5 +154,10 @@ public interface IProfileExpression
/// </summary>
/// <param name="type">Static type that contains extension methods</param>
void IncludeSourceExtensionMethods(Type type);

/// <summary>
/// Value transformers. Modify the list directly or use <see cref="ValueTransformerConfigurationExtensions.Add{TValue}"/>
/// </summary>
IList<ValueTransformerConfiguration> ValueTransformers { get; }
}
}
9 changes: 8 additions & 1 deletion src/AutoMapper/Profile.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using AutoMapper.Configuration;
Expand All @@ -25,8 +26,8 @@ public abstract class Profile : IProfileExpression, IProfileConfiguration
private readonly List<ITypeMapConfiguration> _openTypeMapConfigs = new List<ITypeMapConfiguration>();
private readonly List<MethodInfo> _sourceExtensionMethods = new List<MethodInfo>();
private readonly IList<ConditionalObjectMapper> _typeConfigurations = new List<ConditionalObjectMapper>();

private readonly List<ITypeMapConfiguration> _typeMapConfigs = new List<ITypeMapConfiguration>();
private readonly List<ValueTransformerConfiguration> _valueTransformerConfigs = new List<ValueTransformerConfiguration>();

protected Profile(string profileName)
: this() => ProfileName = profileName;
Expand Down Expand Up @@ -63,6 +64,7 @@ protected Profile(string profileName, Action<IProfileExpression> configurationAc
IEnumerable<IConditionalObjectMapper> IProfileConfiguration.TypeConfigurations => _typeConfigurations;
IEnumerable<ITypeMapConfiguration> IProfileConfiguration.TypeMapConfigs => _typeMapConfigs;
IEnumerable<ITypeMapConfiguration> IProfileConfiguration.OpenTypeMapConfigs => _openTypeMapConfigs;
IEnumerable<ValueTransformerConfiguration> IProfileConfiguration.ValueTransformers => _valueTransformerConfigs;

public virtual string ProfileName { get; }

Expand All @@ -75,6 +77,7 @@ protected Profile(string profileName, Action<IProfileExpression> configurationAc
public INamingConvention SourceMemberNamingConvention { get; set; }
public INamingConvention DestinationMemberNamingConvention { get; set; }

public IList<ValueTransformerConfiguration> ValueTransformers => _valueTransformerConfigs;

public void DisableConstructorMapping()
{
Expand Down Expand Up @@ -182,6 +185,10 @@ public void IncludeSourceExtensionMethods(Type type)
m.GetParameters().Length == 1));
}

public void ApplyTransform<TValue>(Expression<Func<TValue, TValue>> transformer)
{
}

private IMappingExpression<TSource, TDestination> CreateMappingExpression<TSource, TDestination>(
MemberList memberList)
{
Expand Down
2 changes: 2 additions & 0 deletions src/AutoMapper/ProfileMap.cs
Expand Up @@ -47,6 +47,7 @@ public ProfileMap(IProfileConfiguration profile, IConfiguration configuration)
: Enumerable.Empty<IConditionalObjectMapper>())
.ToArray();

ValueTransformers = profile.ValueTransformers.Concat(configuration?.ValueTransformers ?? Enumerable.Empty<ValueTransformerConfiguration>()).ToArray();

MemberConfigurations = profile.MemberConfigurations.ToArray();

Expand Down Expand Up @@ -95,6 +96,7 @@ public ProfileMap(IProfileConfiguration profile, IConfiguration configuration)
public IEnumerable<IConditionalObjectMapper> TypeConfigurations { get; }
public IEnumerable<string> Prefixes { get; }
public IEnumerable<string> Postfixes { get; }
public IEnumerable<ValueTransformerConfiguration> ValueTransformers { get; }

public TypeDetails CreateTypeDetails(Type type) => _typeDetails.GetOrAdd(type);

Expand Down
7 changes: 7 additions & 0 deletions src/AutoMapper/PropertyMap.cs
Expand Up @@ -17,6 +17,7 @@ namespace AutoMapper
public class PropertyMap
{
private readonly List<MemberInfo> _memberChain = new List<MemberInfo>();
private readonly List<ValueTransformerConfiguration> _valueTransformerConfigs = new List<ValueTransformerConfiguration>();

public PropertyMap(PathMap pathMap)
{
Expand Down Expand Up @@ -68,6 +69,7 @@ public PropertyMap(PropertyMap inheritedMappedProperty, TypeMap typeMap)
public bool ExplicitExpansion { get; set; }
public object NullSubstitute { get; set; }
public ValueResolverConfiguration ValueResolverConfig { get; set; }
public IEnumerable<ValueTransformerConfiguration> ValueTransformers => _valueTransformerConfigs;

public MemberInfo SourceMember
{
Expand Down Expand Up @@ -142,6 +144,11 @@ public void MapFrom(LambdaExpression sourceMember)
Ignored = false;
}

public void AddValueTransformation(ValueTransformerConfiguration valueTransformerConfiguration)
{
_valueTransformerConfigs.Add(valueTransformerConfiguration);
}

private class MemberFinderVisitor : ExpressionVisitor
{
public MemberExpression Member { get; private set; }
Expand Down
9 changes: 8 additions & 1 deletion src/AutoMapper/TypeMap.cs
Expand Up @@ -29,6 +29,7 @@ public class TypeMap
private PropertyMap[] _orderedPropertyMaps;
private bool _sealed;
private readonly IList<TypeMap> _inheritedTypeMaps = new List<TypeMap>();
private readonly List<ValueTransformerConfiguration> _valueTransformerConfigs = new List<ValueTransformerConfiguration>();

public TypeMap(TypeDetails sourceType, TypeDetails destinationType, MemberList memberList, ProfileMap profile)
{
Expand Down Expand Up @@ -79,7 +80,8 @@ public PathMap FindOrCreatePathMapFor(LambdaExpression destinationExpression, Me
public IEnumerable<TypePair> IncludedBaseTypes => _includedBaseTypes;

public IEnumerable<LambdaExpression> BeforeMapActions => _beforeMapActions;
public IEnumerable<LambdaExpression> AfterMapActions => _afterMapActions;
public IEnumerable<LambdaExpression> AfterMapActions => _afterMapActions;
public IEnumerable<ValueTransformerConfiguration> ValueTransformers => _valueTransformerConfigs;

public bool PreserveReferences { get; set; }
public LambdaExpression Condition { get; set; }
Expand Down Expand Up @@ -259,6 +261,11 @@ public void AddAfterMapAction(LambdaExpression afterMap)
}
}

public void AddValueTransformation(ValueTransformerConfiguration valueTransformerConfiguration)
{
_valueTransformerConfigs.Add(valueTransformerConfiguration);
}

public void Seal(IConfigurationProvider configurationProvider, Stack<TypeMap> typeMapsPath = null)
{
if(_sealed)
Expand Down
40 changes: 40 additions & 0 deletions src/AutoMapper/ValueTransformerConfiguration.cs
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace AutoMapper
{
public class ValueTransformerConfiguration
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this could be a struct.

{
public ValueTransformerConfiguration(Type valueType, LambdaExpression transformerExpression)
{
ValueType = valueType;
TransformerExpression = transformerExpression;
}

public Type ValueType { get; }
public LambdaExpression TransformerExpression { get; }

public bool IsMatch(PropertyMap propertyMap)
{
return ValueType.IsAssignableFrom(propertyMap.DestinationPropertyType);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the other way around?

}
}

public static class ValueTransformerConfigurationExtensions
{
/// <summary>
/// Apply a transformation function after any resolved destination member value with the given type
/// </summary>
/// <typeparam name="TValue">Value type to match and transform</typeparam>
/// <param name="valueTransformers">Value transformer list</param>
/// <param name="transformer">Transformation expression</param>
public static void Add<TValue>(this IList<ValueTransformerConfiguration> valueTransformers,
Expression<Func<TValue, TValue>> transformer)
{
var config = new ValueTransformerConfiguration(typeof(TValue), transformer);

valueTransformers.Add(config);
}
}
}
2 changes: 1 addition & 1 deletion src/UnitTests/Bug/MultiThreadingIssues.cs
Expand Up @@ -75,7 +75,7 @@ static void doMapping()
{
var source = createSource();

Console.WriteLine(@"Mapping {0} on thread {1}", source.GetType(), Thread.CurrentThread.ManagedThreadId);
Debug.WriteLine(@"Mapping {0} on thread {1}", source.GetType(), Thread.CurrentThread.ManagedThreadId);

var config = new MapperConfiguration(cfg => cfg.CreateMap(source.GetType(), typeof(DestType)));

Expand Down
12 changes: 1 addition & 11 deletions src/UnitTests/ConfigurationRules.cs
@@ -1,5 +1,4 @@
using System;
using Xunit;
using Xunit;

namespace AutoMapper.UnitTests
{
Expand Down Expand Up @@ -59,15 +58,6 @@ public void Should_throw_for_multiple_create_map_calls_in_different_profiles()
});

typeof(DuplicateTypeMapConfigurationException).ShouldBeThrownBy(() => config.AssertConfigurationIsValid());

try
{
config.AssertConfigurationIsValid();
}
catch (DuplicateTypeMapConfigurationException e)
{
Console.WriteLine(e.ToString());
}
}

[Fact]
Expand Down