ForAllMembers overriding Ignore rules already defined - c#

I am using AutoMapper and I need to ignore members where an Attribute is not defined. Then, if the Member is not being ignored, I need to map only where values are defined. I have managed to achieve these two separately, but ForAllMembers/ForAllOtherMembers seems to be overriding the first rule.
Let's say I have this class:
public class Foo
{
[MyCustomAttribute]
public string Name { get; set; }
public string IgnoreMe { get; set; }
[MyCustomAttribute]
public int? DontIgnoreNumber { get; set; }
}
I want to ignore IgnoreMe regardless. Then, for Name and DontIgnoreNumber, I want to map them only if they have a value. How can I achieve this?
I have tried this:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Foo, Foo>()
.IgnoreAllNonAttributedEntities()
.ForAllOtherMembers(opts =>
{
opts.Condition((src, dest, srcMember) =>
{
// Check if source is a default value
return srcMember != null;
});
});
});
I have checked that the ForAllOtherMembers rule is working. And I, separately, have checked that the IgnoreAllNonAttributedEntities is working. When I try to combine them, the ForAllOtherMembers seems to be taking priority.
IgnoreAllNonAttributedEntities is defined as:
public static IMappingExpression<TSource, TDestination> IgnoreAllNonAttributedEntities<TSource, TDestination>
(this IMappingExpression<TSource, TDestination> expression)
{
var flags = BindingFlags.Public | BindingFlags.Instance;
//var sourceType = typeof(TSource);
var destinationType = typeof(TDestination);
foreach(var prop in destinationType.GetProperties(flags))
{
var attr = ReflectionHelpers.GetAttribute<MyCustomAttribute>(prop);
if (attr == null)
{
expression.ForMember(prop.Name, opt => opt.Ignore());
}
}
return expression;
}

I've just run your code and it works as expected. However, maybe what bothers you is the default value of value types in c# (cuz you only check for nulls). Here is my fix for value types:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Foo, Foo>()
.IgnoreAllNonAttributedEntities()
.ForAllOtherMembers(opts =>
{
opts.Condition((src, dest, srcMember) =>
{
var srcType = srcMember?.GetType();
if (srcType is null)
{
return false;
}
return (srcType.IsClass && srcMember != null)
|| (srcType.IsValueType
&& !srcMember.Equals(Activator.CreateInstance(srcType)));
});
});
});
I've recreated your scenerio using latest version of automapper available on NuGet (8.0.0.0).

Related

Why do I have to Ignore all other properties in AutoMapper when using records?

public record Destination(double X, double Y);
public struct Source
{
public double X { get; set; }
public Potato Potato { get; set; }
public double Z { get; set; }
}
public struct Potato
{
public double Y { get; set; }
}
public MappingProfile()
{
CreateMap<Source, Destination>();
.ForCtorParam(nameof(Destination.Y), e => e.MapFrom(x => x.Potato.Y))
.ForAllOtherMembers(x => x.Ignore());
}
To map source to destination, I need to manually map one of the children.
However, automapper will then give an extremly confusing message, saying Y property is unmapped.
Unmapped members were found. Review the types and members below.
Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
For no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters
==========================================================================================================
Source -> Destination (Destination member list)
Unmapped properties:
Y
I found by adding the line to ignore all other members, it would 'solve' the issue. Is there a better way of preventing this error occuring?
The error message mentions mapping all the constructor parameters, but even if I add .ForCtorParam(nameof(Destination.X), e => e.MapFrom(x => x.X)) the error still occurs.
This is already solved in 11. Because you cannot upgrade, you'll have to ignore all those properties. The problem is that the properties have setters and in AM 10 the properties are not considered mapped even if they're already mapped through the constructor.
Another solution is to use a struct (or class) without setters instead of record.
I created the following class to help cover my needs, since I can't upgrade to newer AutoMapper versions.
It is naive, and assumes that all properties on a record should be mapped to constructor.
Benefits:
Will automatically map all properties with same name
Can use lambda arguments instead of nameof
Don't need to call ignore other members
Will throw exception if a property is not mapped
To use it, in MappingProfile:
new RecordMapBuilder<TSource, TDestination>(this)
.Map(x => x.Foo, x => x.Offset.Foo)
.Build();
public class RecordMapBuilder<TSource, TDestination>
{
private readonly Profile _profile;
private readonly bool _autoMapMatchingProperties;
private readonly Dictionary<string, Expression<Func<TSource, object>>> _ctorParamActions = new();
public RecordMapBuilder(Profile profile, bool autoMapMatchingProperties = true)
{
_profile = profile;
_autoMapMatchingProperties = autoMapMatchingProperties;
}
public void Build()
{
var map = _profile.CreateMap<TSource, TDestination>();
var unmappedDestinationProperties = new HashSet<string>(
typeof(TDestination)
.GetProperties()
.Where(e => !_ctorParamActions.ContainsKey(e.Name))
.Select(e => e.Name));
var sourceProperties = new HashSet<string>(typeof(TSource)
.GetProperties()
.Select(e => e.Name));
var mappableProperties = unmappedDestinationProperties.Intersect(sourceProperties).ToHashSet();
var unMappableProperties = unmappedDestinationProperties.Except(sourceProperties).ToHashSet();
if (unMappableProperties.Any())
{
var properties = string.Join(", ", unMappableProperties);
throw new InvalidOperationException($"Not all properties mapped for type {typeof(TSource)} -> {typeof(TDestination)}: {properties}");
}
if (_autoMapMatchingProperties)
{
foreach (var name in mappableProperties)
{
map.ForCtorParam(name, x => x.MapFrom(name));
}
}
foreach (var kv in _ctorParamActions)
{
map.ForCtorParam(kv.Key, x => x.MapFrom(kv.Value));
}
map.ForAllOtherMembers(x => x.Ignore());
}
public RecordMapBuilder<TSource, TDestination> Map(Expression<Func<TDestination, object>> destination, Expression<Func<TSource, object>> source)
{
var name = GetName(destination);
_ctorParamActions[name] = source;
return this;
}
private static string GetName(Expression<Func<TDestination, object>> destination)
{
{
if (destination.Body is UnaryExpression ue && ue.Operand is MemberExpression me)
{
return me.Member.Name;
}
}
{
if (destination.Body is MemberExpression me)
{
return me.Member.Name;
}
}
throw new InvalidOperationException($"Unhandled expression of type: {destination.Body.GetType()}");
}

Automapper | Only Map if not default / initilized value

I would like to have a generic way to map properties only if they are changed and not the default value.
So let's say you have these two classes
public class Source
{
public string Foo { get; set; } = "Foo";
public string Bar { get; set; } = "Bar";
}
public class Destination
{
public string Foo { get; set; } = "Foo of Destination";
public string Bar { get; set; } = "Bar of Destination";
}
Now I want to map Source to Destination and if the property of Source.Foo is still "Foo" it should change to "Foo of Destination", but when the value of Source.Foo is different from "Foo" (the default) it should really map the property.
I could not find a way to do this with automapper.
[TestMethod]
public void ChangeAllProperties()
{
var source = new Source();
var destination = _mapper.Map<Destination>(source);
var defaultDestination = new Destination();
Assert.AreEqual(destination.Bar, defaultDestination.Bar); // Should be the Default Value of "Destination"
Assert.AreEqual(destination.Foo, defaultDestination.Foo); // Should be the Default Value of "Destination"
}
[TestMethod]
public void ChangeDefaultProperty()
{
var source = new Source();
source.Bar = "Custom Bar!";
var destination = _mapper.Map<Destination>(source);
var defaultDestination = new Destination();
Assert.AreEqual(destination.Bar, source.Bar); // Should be Value of Source
Assert.AreEqual(destination.Foo, defaultDestination.Foo); // Should be the Default Value of "Destination"
}
[TestMethod]
public void AlLChanged()
{
var source = new Source();
source.Foo = "Custom Foo!";
source.Bar = "Custom Bar!";
var destination = _mapper.Map<Destination>(source);
Assert.AreEqual(destination.Bar, source.Bar); // Should be Value of Source
Assert.AreEqual(destination.Foo, source.Foo); // Should be Value of Source
}
Check out Automapper conditional mapping - link
Basically, you can do something like this (from the samples):
var configuration = new MapperConfiguration(cfg => {
cfg.CreateMap<Foo,Bar>()
.ForMember(dest => dest.baz, opt => opt.Condition(src => (src.baz >= 0)));
});
opt.Condition() makes sure your condition is met before actually doing any mapping of that particular member.
The opt.Condition() idea from Plamen Yordanov was the right path. I was able to create something more generic, so i dont need to define this for every member and checks the default value of the properties.
This is the solution i came up with. It only checks the default value if the destination member also exists in the source with the same name and type.
public class FooBarProfile: Profile
{
public FooBarProfile()
{
CreateMap<Source, Destination>()
.ForAllMembers(opt => opt.Condition(src => OnlyIfChanged(src, opt.DestinationMember)));
// And if you need reversed...
CreateMap<Destination, Source>()
.ForAllMembers(opt => opt.Condition(src => OnlyIfChanged(src, opt.DestinationMember)));
}
private static bool OnlyIfChanged<TSource>(TSource src, MemberInfo destinationMember) where TSource : new()
{
// Check if DestinationMember exists in TSource (same Name & Type)
var sourceMember = typeof(TSource).GetMembers().FirstOrDefault(e => e.Name == destinationMember.Name && e.MemberType == destinationMember.MemberType);
if (sourceMember != null)
{
var defaultOfSource = new TSource();
// Map only if Value of Source is differnent than the default soruce value
return (GetValue(sourceMember,src)?.Equals(GetValue(sourceMember,defaultOfSource)) == false);
}
return true;
}
// Helper-Method to get Value from a MemberInfo
private static object GetValue(MemberInfo memberInfo, object forObject)
{
switch (memberInfo.MemberType)
{
case MemberTypes.Field:
return ((FieldInfo)memberInfo).GetValue(forObject);
case MemberTypes.Property:
return ((PropertyInfo)memberInfo).GetValue(forObject);
default:
throw new ArgumentException("Member is not Field or Property.", nameof(memberInfo));
}
}
}

AutoMapper Mapping one property to two properties by condition

There are two classes
class A
{
public string ProductionDivision { get; set; }
}
class B
{
private object _productionDivision;
public enum ProductionDivisionOneofCase
{
None = 0,
IsNullproductionDivision = 15,
ProductionDivisionValue = 16,
}
private ProductionDivisionOneofCase _productionDivisionCase = ProductionDivisionOneofCase.None;
public bool IsNullProductionDivision
{
get { return _productionDivisionCase == ProductionDivisionOneofCase.IsNullproductionDivision ? (bool)_productionDivision : false; }
set
{
_productionDivision = value;
_productionDivisionCase = ProductionDivisionOneofCase.IsNullproductionDivision;
}
}
public string ProductionDivisionValue
{
get { return _productionDivisionCase == ProductionDivisionOneofCase.ProductionDivisionValue ? (string)_productionDivision : ""; }
set
{
_productionDivision = value;
_productionDivisionCase = ProductionDivisionOneofCase.ProductionDivisionValue;
}
}
}
I would like to map the ProductionDivision property to one of properties of class B depending on the condition - null(map to IsNullProductionDivision) or not null(map to ProductionDivisionValue) of the source property value. I can achieve it like as below.
CreateMap<A, B>()
.ForMember(g => g.IsNullProductionDivision, m =>
{
m.PreCondition(s => s.ProductionDivision == null);
m.MapFrom(b => true);
})
.ForMember(g => g.ProductionDivisionValue, m =>
{
m.PreCondition(s => s.ProductionDivision != null);
m.MapFrom(b => b.ProductionDivision);
});
If the value of {source property name} is null then the value of IsNull{source property name} is true.
Else if the value of {source property name} is not null then the value of {source property name}Value is the value of {source property name}.
I have many properties that respond this mapping rule. So, I don't want to write mapping rule for each properties like above. I want to configurate a rule for such mapping globally.
How can I configure AutoMapper so that it can handle such complex mapping?
I have found solution. The solution is pretty simple and clear. It turns out as follow:
Full code:
public class Program
{
class A
{
public string ProductionDivision { get; set; }
}
class B
{
private object _productionDivision;
public enum ProductionDivisionOneofCase
{
None = 0,
IsNullproductionDivision = 15,
ProductionDivisionValue = 16,
}
private ProductionDivisionOneofCase _productionDivisionCase = ProductionDivisionOneofCase.None;
public bool IsNullProductionDivision
{
get { return _productionDivisionCase == ProductionDivisionOneofCase.IsNullproductionDivision ? (bool)_productionDivision : false; }
set
{
_productionDivision = value;
_productionDivisionCase = ProductionDivisionOneofCase.IsNullproductionDivision;
}
}
public string ProductionDivisionValue
{
get { return _productionDivisionCase == ProductionDivisionOneofCase.ProductionDivisionValue ? (string)_productionDivision : ""; }
set
{
_productionDivision = value;
_productionDivisionCase = ProductionDivisionOneofCase.ProductionDivisionValue;
}
}
}
public class StrinToBoolCustomResolver
: IValueConverter<string, bool>
{
public bool Convert(string sourceMember, ResolutionContext context)
{
return sourceMember == null;
}
}
public class MyAutoMapperProfile
: Profile
{
public MyAutoMapperProfile()
{
// add post and pre prefixes to add corresponding properties in the inner property map
RecognizeDestinationPostfixes("Value");
RecognizeDestinationPrefixes("IsNull");
// add mapping for "value" property
this.ForAllPropertyMaps(map => map.SourceMember.Name + "Value" == map.DestinationName,
(map, expression) =>
{
expression.Condition((source, dest, sMem, destMem) =>
{
return sMem != null;
});
expression.MapFrom(map.SourceMember.Name);
});
// add mapping for "IsNull" property
this.ForAllPropertyMaps(map => "IsNull" + map.SourceMember.Name == map.DestinationName,
(map, expression) =>
{
expression.Condition((source, dest, sMem, destMem) =>
{
return (bool)sMem;
});
//expression.MapFrom(map.SourceMember.Name);
expression.ConvertUsing<string, bool>(new StrinToBoolCustomResolver(), map.SourceMember.Name);
});
CreateMap<A, B>();
//.ForMember(g => g.IsNullProductionDivision, m =>
//{
// m.PreCondition(s => s.ProductionDivision == null);
// m.MapFrom(b => true);
//});
//.ForMember(g => g.ProductionDivisionValue, m =>
//{
// m.PreCondition(s => s.ProductionDivision != null);
// m.MapFrom(b => b.ProductionDivision);
//});
}
}
public static void Main(string[] args)
{
var configuration = new MapperConfiguration(cfg => {
cfg.AddProfile(new MyAutoMapperProfile());
});
var mapper = new Mapper(configuration);
mapper.ConfigurationProvider.AssertConfigurationIsValid();
var a = new A()
{
ProductionDivision = null
};
// b._productionDivisionCase will be equal IsNullproductionDivision
var b = mapper.Map<B>(a);
var c = new A()
{
ProductionDivision = "dsd"
};
// d._productionDivisionCase will be equal ProductionDivisionValue
var d = mapper.Map<B>(c);
}
}
Clarification:
add (post/pre)fixes to add corresponding properties to inner property map. It need to do here because our properties should be catched by AutoMapper. Otherwise properties will be abandoned and mapper configuration will be failed.
After that, we configurate how these properties need to be mapping. We call ForAllPropertyMaps method, filtering all properties and setting a rule to mapping properties suitable with our filter. When the mapper object is creating, the execution plan will be built taking look the specified filters.
For "Value" property we add a condition to check whether the source property is not null. If it is null, then mapping will be missed.
For "IsNull" property First of all, we add a converter to convert string type to bool type. The converter just compares the source string property with null value. If the source property equals null, then the converter returns true. So, the condition receives a true value, returns true, and mapping will be done. Otherwise the converter returns false, so the condition returns false and mapping will be missed.
Thus, the mapping of source property will occur to different destination properties depending on whether the source property is null value. Besides that, corresponding set methods of corresponding destination properties will be not called if it not must to be called.

FluentValidation Recursive list causes stack overflow

I am using FluentValidation.AspNetcore 8.2.2 and have a object model that contains a list of child items of the same type.
I would like to use fluent validation to validate the object.
When trying to set the validator for the child object I either run into a stack overflow exception and/or the collection has changed (typical foreach loop issue).
In order to test and find resolution I have setup a simple .net core class library project with a unit test.
Base Model
using FluentValidation;
public class BaseModelItem
{
public int ItemId { get; set; }
public string Name { get; set; }
private List<BaseModelItem> ChildItems { get; set; }
}
public class BaseModelItemValidator : AbstractValidator<BaseModelItem>
{
public BaseModelItemValidator()
{
RuleFor(i => i.ItemId).GreaterThanOrEqualTo(0).WithMessage("Item id may not be negative.");
RuleFor(i => i.Name).NotNull().NotEmpty().WithMessage("Item name cannot be empty.");
RuleFor(i => i.ChildItems).ForEach(i => i.SetValidator(new BaseModelItemValidator()));
}
}
unit test
public class Tests
{
[Test]
public void Test_Name_Cannot_Null()
{
var item = new BaseModelItem
{
ItemId = 2,
Name = null,
ChildItems = new List<BaseModelItem>()
};
var validator = new BaseModelItemValidator();
validator.ShouldHaveValidationErrorFor(t => t.Name, item);
Assert.Pass();
}
}
This test will cause a stack overflow exception.
I have tried using backing fields, initializing or even changing to an array.
I can successfully negate the stackover flow exception by using a custom validator.
public class BaseModelItemValidator : AbstractValidator<BaseModelItem>
{
public BaseModelItemValidator()
{
RuleFor(i => i.ItemId).GreaterThanOrEqualTo(0).WithMessage("Item id may not be negative.");
RuleFor(i => i.Name).NotNull().NotEmpty().WithMessage("Item name cannot be empty.");
RuleFor(i => i.ChildItems).Must(BeValidChildItemList);
}
private bool BeValidChildItemList(List<BaseModelItem> list)
{
if (list.Count > 0)
{
RuleFor(i => i.ChildItems).ForEach(i => i.SetValidator(new BaseModelItemValidator()));
}
return true;
}
}
Allows it to validate objects without child items.
However if you run a test with populated child objects I get the error "Collection was modified; enumeration operation may not execute".
Stack trace
StackTrace:
at System.ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion()
at System.Collections.Generic.List`1.Enumerator.MoveNextRare()
at System.Linq.Enumerable.SelectManySingleSelectorIterator`2.MoveNext()
at System.Linq.Enumerable.WhereEnumerableIterator`1.MoveNext()
at FluentValidation.AbstractValidator`1.Validate(ValidationContext`1 context) in ****\FluentValidation\src\FluentValidation\AbstractValidator.cs:line 115
at FluentValidation.DefaultValidatorExtensions.Validate[T](IValidator`1 validator, T instance, IValidatorSelector selector, String ruleSet) in ******\FluentValidation\src\FluentValidation\DefaultValidatorExtensions.cs:line 876
at FluentValidation.TestHelper.ValidationTestExtension.TestValidate[T,TValue](IValidator`1 validator, Expression`1 expression, T instanceToValidate, TValue value, String ruleSet, Boolean setProperty) in ******\FluentValidation\src\FluentValidation\TestHelper\ValidatorTestExtensions.cs:line 101
at FluentValidation.TestHelper.ValidationTestExtension.ShouldHaveValidationErrorFor[T,TValue](IValidator`1 validator, Expression`1 expression, T objectToTest, String ruleSet) in *******\FluentValidation\src\FluentValidation\TestHelper\ValidatorTestExtensions.cs:line 40
at Tests.Tests.Test_Name_Cannot_Null_Nested() in \FluentValidationChildern\FluentValidationChildern.Tests\UnitTest1.cs:line 55
I am unable to find a feasible resolution.
Although I have been unable to get Fluent to work using the SetValidator method, I do have a workaround which is working and can be improved on.
on the child list I set use the 'Must' method and then implement a manual function to loop the child items and construct the validator object manually and check the result.
public class BaseModelItemValidator : AbstractValidator<BaseModelItem>
{
public BaseModelItemValidator()
{
RuleFor(i => i.ItemId).GreaterThanOrEqualTo(0).WithMessage("Item id may not be negative.");
RuleFor(i => i.Name).NotNull().NotEmpty().WithMessage("Item name cannot be empty.");
RuleFor(i => i.ChildItems).Must(BeValidChildItemList);
}
private bool BeValidChildItemList(List<BaseModelItem> list)
{
if (list == null || list.Count == 0) return true;
foreach (var child in list)
{
var validator = new BaseModelItemValidator();
var validatorResults = validator.Validate(child);
if (!validatorResults.IsValid)
{
return false;
}
}
return true;
}
}
I had a similar requirement where in I had to validate a object which had a list of objects of same type as children. What I did was I overrode the Validate method of my validator and added logic something like
public override ValidationResult Validate(ValidationContext<BaseModelItem> context)
{
var result = base.Validate(context);
var obj = context.InstanceToValidate;
if (obj.ChildItems != null && obj.ChildItems.Count > 0)
{
foreach (var item in obj.ChildItems)
{
var childResult = base.Validate(item);
foreach (var error in childResult.Errors)
{
result.Errors.Add(error);
}
}
}
return result;
}
I would have loved to use the SetValidator somehow but couldn't find a way to do it. But anyways, this override returns ValidationResult object that I expect
It would seem the solution is to use the same instance of the validator like this:
public class BaseModelItemValidator : AbstractValidator<BaseModelItem>
{
public BaseModelItemValidator()
{
RuleFor(i => i.ItemId).GreaterThanOrEqualTo(0).WithMessage("Item id may not be negative.");
RuleFor(i => i.Name).NotNull().NotEmpty().WithMessage("Item name cannot be empty.");
RuleFor(i => i.ChildItems).ForEach(i => i.SetValidator(this));
}
}
I can confirm this is working for me. Credit to this issue

Custom Mapping with AutoMapper

I have two very simple objects:
public class CategoryDto
{
public string Id { get; set; }
public string MyValueProperty { get; set; }
}
public class Category
{
public string Id { get; set; }
[MapTo("MyValueProperty")]
public string Key { get; set; }
}
When mapping a Category to a CategoryDto with AutoMapper, I would like the following behavior:
The properties should be mapped as usual, except for those that have the MapTo attribute. In this case, I have to read the value of the Attribute to find the target property. The value of the source property is used to find the value to inject in the destination property (with the help of a dictionary). An example is always better that 1000 words...
Example:
Dictionary<string, string> keys =
new Dictionary<string, string> { { "MyKey", "MyValue" } };
Category category = new Category();
category.Id = "3";
category.Key = "MyKey";
CategoryDto result = Map<Category, CategoryDto>(category);
result.Id // Expected : "3"
result.MyValueProperty // Expected : "MyValue"
The Key property is mapped to the MyValueProperty (via the MapTo Attribute), and the assigned value is "MyValue", because the source property value is "MyKey" which is mapped (via dictionary) to "MyValue".
Is this possible using AutoMapper ? I need of course a solution that works on every object, not just on Category/CategoryDto.
I finally (after so many hours !!!!) found a solution.
I share this with the community; hopefully it will help someone else...
Edit: Note that it's now much simpler (AutoMapper 5.0+), you can do like I answered in this post: How to make AutoMapper truncate strings according to MaxLength attribute?
public static class Extensions
{
public static IMappingExpression<TSource, TDestination> MapTo<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
{
Type sourceType = typeof(TSource);
Type destinationType = typeof(TDestination);
TypeMap existingMaps = Mapper.GetAllTypeMaps().First(b => b.SourceType == sourceType && b.DestinationType == destinationType);
string[] missingMappings = existingMaps.GetUnmappedPropertyNames();
if (missingMappings.Any())
{
PropertyInfo[] sourceProperties = sourceType.GetProperties();
foreach (string property in missingMappings)
{
foreach (PropertyInfo propertyInfo in sourceProperties)
{
MapToAttribute attr = propertyInfo.GetCustomAttribute<MapToAttribute>();
if (attr != null && attr.Name == property)
{
expression.ForMember(property, opt => opt.ResolveUsing(new MyValueResolve(propertyInfo)));
}
}
}
}
return expression;
}
}
public class MyValueResolve : IValueResolver
{
private readonly PropertyInfo pInfo = null;
public MyValueResolve(PropertyInfo pInfo)
{
this.pInfo = pInfo;
}
public ResolutionResult Resolve(ResolutionResult source)
{
string key = pInfo.GetValue(source.Value) as string;
string value = dictonary[key];
return source.New(value);
}
}
This should be fairly straightforward using an implementation of IValueResolver and the ResolveUsing() method. You basically just need to have a constructor for the resolver that takes in the property info (or if you wanna be fancy that takes in a lambda expression and resolves the property info similar to How to get the PropertyInfo of a specific property?. Though I haven't tested it myself I imagine the following would work:
public class PropertyBasedResolver : IValueResolver
{
public PropertyInfo Property { get; set; }
public PropertyBasedResolver(PropertyInfo property)
{
this.Property = property;
}
public ResolutionResult Resolve(ResolutionResult source)
{
var result = GetValueFromKey(property, source.Value); // gets from some static cache or dictionary elsewhere in your code by reading the prop info and then using that to look up the value based on the key as appropriate
return source.New(result)
}
}
Then to set up that mapping you need to do:
AutoMapper.Mapper.CreateMap<Category, CategoryDto>()
.ForMember(
dest => dest.Value,
opt => opt.ResolveUsing(
src =>
new PropertyBasedResolver(typeof(Category.Key) as PropertyInfo).Resolve(src)));
Of course that is a pretty gross lamda and I would suggest that you clean it up by having your property resolver determine the property on the source object it should look at based on the attribute/property info so you can just pass a clean new PropertyBasedResolver(property) into the ResolveUsing() but hopefully this explains enough to put you on the right track.
Lets assume I have the following classes
public class foo
{
public string Value;
}
public class bar
{
public string Value1;
public string Value2;
}
You can pass a lambda to ResolveUsing:
.ForMember(f => f.Value, o => o.ResolveUsing(b =>
{
if (b.Value1.StartsWith("A"));)
{
return b.Value1;
}
return b.Value2;
}
));

Categories