I'd like to map a common object to an instance of an abstraction (concrete instance of the abstraction) without saying which concrete instance I want. I want to choose the concrete instance based on a condition which will be set up in mapping configuration.
Like here:
ConstructUsing(y => !string.IsNullOrEmpty(y.Label) ? (X)new XLabel() : new XValue())
and here:
m.Map<X>(i2); //where X is an abstraction
Data
// Source
public class Input
{
public int Id { get; set; }
public string Label { get; set; }
public decimal Amount { get; set; }
}
//Base target
public abstract class X
{
public int P1 { get; set; }
}
//Concrete target 1
public class XLabel : X
{
public string Name { get; set; }
}
//Concrete target 2
public class XValue : X
{
public string Value { get; set; }
}
CONFIGURATION:
//config
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Input, XLabel>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Label));
cfg.CreateMap<Input, XValue>()
.ForMember(dest => dest.Value, opt => opt.MapFrom(src => src.Amount));
cfg.CreateMap<Input, X>()
.Include<Input, XLabel>()
.Include<Input, XValue>()
.ConstructUsing(y => !string.IsNullOrEmpty(y.Label) ? (X)new XLabel() : new XValue())
.ForMember(dest => dest.P1, opt => opt.MapFrom(src => src.Id));
});
MAPPING:
//Test
Input i1 = new Input { Id = 1, Amount = 2, };
Input i2 = new Input { Id = 1, Label = "my label" };
IMapper m = new Mapper(config);
var result1 = m.Map<X>(i1);
var result2 = m.Map<X>(i2);
Notice that I'm doing this:
var result1 = m.Map<X>(i1); not this var result1 = m.Map<XValue>(i1); -And this is what I wanted to do -I wanted to Map to abstraction not to a concrete instance
TESTS:
Assert.IsType<XValue>(result1); //PASSED
Assert.IsType<XLabel>(result2); //PASSED
Assert.Equal("my label", ((XLabel)result1).Name);// Message: Assert.Equal() FAILURE Expected: my label; Actual: (null)
Problem:
Looks like this mapping does not work:
cfg.CreateMap<Input, XLabel>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Label));
cfg.CreateMap<Input, XValue>()
.ForMember(dest => dest.Value, opt => opt.MapFrom(src => src.Amount));
Message: Assert.Equal() FAILURE Expected: my label; Actual: (null)
Related
let me say what i have:
CarDto Class code is like below:
namespace MyNamespace.AppService.Contract.Cars;
public class CarDto : BaseDto
{
public string Name { get; set; }
public string? Description { get; set; }
}
Car Model Class code is like below:
namespace MyNamespace.Model.Cars;
public class Car : BaseModel<Car>
{
public Car(string name)
{
Name = name;
}
private string _name;
private string? _description;
public string Name
{
get => _name;
set
{
if (value.Length is < 2 or > 40)
throw new ArgumentOutOfRangeException();
_name = value;
}
}
public string? Description
{
get => _description;
set
{
if (value is { Length: > 800 })
throw new ArgumentOutOfRangeException("The length of the Description property cannot be greater than 800 characters.");
_description = value;
}
}
protected override Car Edit(Car model)
{
Name = model.Name;
Description = model.Description;
return this;
}
}
and we Use AutoMapper to map this to class, so my CarProfile that have automapper configuration is like below code:
using AutoMapper;
using MyRBV.AppService.Contract.Cars;
using MyRBV.Model.Cars;
namespace MyNamespace.AppService.Profiles;
public class CarProfile: Profile
{
public CarProfile()
{
CreateMap<CarDto, Car>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
.ReverseMap();
}
}
when i run below Unittest Code:
[Fact]
public async Task Add_CheckInvalidCarDescription_ThrowValidationException()
{
var car = new CarDto
{
Name = new string('c',20),
Description = new string('c', 802)
};
var action = new Func<Task<CarDto>>(() => _service.Add(car));
await action.Should().ThrowAsync<ArgumentOutOfRangeException>();
}
i get below error:
found <AutoMapper.AutoMapperMappingException>: "
"AutoMapper.AutoMapperMappingException with message "Error mapping types.
Mapping types:
CarDto -> Car
MyRBV.AppService.Contract.Cars.CarDto -> MyRBV.Model.Cars.Car
Type Map configuration:
CarDto -> Car
MyRBV.AppService.Contract.Cars.CarDto -> MyRBV.Model.Cars.Car
Destination Member:
Description
"
i want ArgumentOutOfRangeException error be triggered in my Car Model, but i have this problem,
i did some search i changed my CarProfile to below code and error solved and my test passed.
here is ne CarProfile that solved the problem
using AutoMapper;
using MyRBV.AppService.Contract.Cars;
using MyRBV.Model.Cars;
namespace MyNamespace.AppService.Profiles;
public class CarProfile: Profile
{
public CarProfile()
{
CreateMap<CarDto, Car>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
//.ForMember(dest => dest.Description, opt => opt.Condition(src => (src.Description?.Length<=800)))
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
.ForMember(dest => dest.Description, opt => opt.Ignore())
.AfterMap((src, dest) => {
if (src.Description?.Length > 800)
throw new ArgumentOutOfRangeException("Description");
})
.ReverseMap();
}
}
but i want ArgumentOutOfRangeException in CarMdel Class throw when i give Description with more than 800 char lenght to my Add_CheckInvalidCarDescription_ThrowValidationException() test function for more clear code,
Given 2 source entities:
class SourceA
{
public string Info1 { get; set; }
public string Info2 { get; set; }
}
class SourceB
{
public A A { get; set; }
public string OptionalExtraInfo { get; set; }
}
and one destination class:
class Dest
{
public string ModifiedInfo1 { get; set; }
public string ModifiedInfo2 { get; set; }
public string ModifiedOptionalExtraInfo { get; set; }
}
I want to have the following code working with EF6:
var destsFromA = dbContext.SourcesA.ProjectTo<Dest>().ToArray();
var destsFromB = dbContext.SourcesB.ProjectTo<Dest>().ToArray();
So I define Automapper.net mappings:
SourceA => Dest
SourceB => Dest
with custom rules on how to project Info1 into ModifiedInfo1, and Info2=>ModifiedInfo2:
CreateMap<SourceA, Dest>()
.ForMember(x => ModifiedInfo1, opt => opt.MapFrom(src => src.Info1 + " something else-1")
.ForMember(x => ModifiedInfo2, opt => opt.MapFrom(src => src.Info1 + " something else-2")
.ForMember(x => ModifiedOptionalExtraInfo, opt => opt.Ignore());
CreateMap<SourceB, Dest>()
.ForMember(x => ModifiedInfo1, opt => opt.MapFrom(src => src.A.Info1 + " something else-1")
.ForMember(x => ModifiedInfo2, opt => opt.MapFrom(src => src.A.Info2 + " something else-2")
.ForMember(x => ModifiedOptionalExtraInfo, opt => opt.MapFrom(src => src.OptionalExtraInfo + " something else-3"));
How do I reuse mapping rules for ModifiedInfo1, ModifiedInfo2 in second mapping as they are the same as in the first case?
UPDATE In my certain case I figured out how to reuse SourceA => Dest mapping in a natural way.
First, I added a reverse-reference (navigation property) SourceA.B as these entities are really in one-to-zero-or-one relationship and EF has to know about that.
Then I changed the aggregation root in my application code and it became:
var destsFromA = dbContext.SourcesA.ProjectTo<Dest>().ToArray();
var destsFromB = dbContext.SourcesB.Select(x => x.A).ProjectTo<Dest>().ToArray();
so I only had to work with the only SourceA => Dest mapping
Finally I changed the mapping itself:
CreateMap<SourceA, Dest>()
.ForMember(x => ModifiedInfo1, opt => opt.MapFrom(src => src.Info1 + " something else-1")
.ForMember(x => ModifiedInfo2, opt => opt.MapFrom(src => src.Info1 + " something else-2")
.ForMember(x => ModifiedOptionalExtraInfo, opt => opt.MapFrom(src => src.B ? src.B.OptionalExtraInfo + " something else-3" : null);
As this is a solution to a problem, but not an answer to the original question, I accepted Ilya Chumakov's answer as a correct one.
Parametrize mappings with expressions:
opt.MapFrom(expression)
.ForMember(x => x.Foo, expression)
It's easy to extract these expression variables with ReSharper, so it could look like:
Expression<Func<SourceA, string>> expression = src => src.Info1 + " something else-1";
var func = expression.Compile();
cfg.CreateMap<SourceA, Dest>()
.ForMember(x => x.ModifiedInfo1,
opt => opt.MapFrom(expression));
cfg.CreateMap<SourceB, Dest>()
.ForMember(x => x.ModifiedInfo1,
opt => opt.MapFrom(src => func(src.A)));
Update: In case of LINQ to SQL translation, the solution becomes much more complicated. expression.Compile() won't work and a new expression should be created:
Expression<Func<SourceA, string>> expression = src => src.Info1 + "foo";
//it should contain `src => src.A.Info1 + "foo"`
var newExpression = ConvertExpression(expression);
Basic implementation with ExpressionVisitor:
private static Expression<Func<SourceB, string>>
ConvertExpression(Expression<Func<SourceA, string>> expression)
{
var newParam = Expression.Parameter(typeof(SourceB), "src");
var newExpression = Expression.Lambda<Func<SourceB, string>>(
new ReplaceVisitor().Modify(expression.Body, newParam), newParam);
return newExpression;
}
class ReplaceVisitor : ExpressionVisitor
{
private ParameterExpression parameter;
public Expression Modify(Expression expression, ParameterExpression parameter)
{
this.parameter = parameter;
return Visit(expression);
}
protected override Expression VisitLambda<T>(Expression<T> node)
{
return Expression.Lambda<Func<SourceB, bool>>(
Visit(node.Body),
Expression.Parameter(typeof(SourceB)));
}
protected override Expression VisitParameter(ParameterExpression node)
{
if (node.Type == typeof(SourceA))
{
return Expression.Property(parameter, nameof(SourceB.A));
}
throw new InvalidOperationException();
}
}
A quick and easy solution would be to use an intermediate class.
First the classes who are used then later on in the posting
public class SourceA
{
public string A { get; set; }
}
public class SourceB
{
public string B { get; set; }
}
public class Dest
{
public string ValueFromSourceA { get; set; }
public string ValueFromSourceB { get; set; }
}
That said here is the intermediate class:
public class Intermediate
{
public SourceA SourceA { get; set; } = new SourceA();
public SourceB SourceB { get; set; } = new SourceB();
}
Now let's start sticking the parts together with Automapper.
Defining profile classes
public class DestinationProfile : Profile
{
public DestinationProfile()
{
this.CreateMap<Intermediate, Dest>()
.ForMember(destination => destination.ValueFromSourceA,
opt => opt.MapFrom(src => src.SourceA.A))
.ForMember(destination => destination.ValueFromSourceB,
opt => opt.MapFrom(src => src.SourceB.B));
}
}
public class IntermediateProfile : Profile
{
public IntermediateProfile()
{
this.CreateMap<Intermediate, Dest>()
.ForMember(destination => destination.ValueFromSourceA, map => map.MapFrom(src => src.SourceA.A))
.ForMember(destination => destination.ValueFromSourceB, map => map.MapFrom(src => src.SourceB.B));
// ----- TODO: Create mapping for source classes.
}
}
And here comes the heavy lifting for the mapping we marked as todo above.
You can use IValueResolver interface from Automapper to define value mapping.
So in our case the resolvers look like
public class SourceAResolver : IValueResolver<SourceA, Intermediate, SourceA>
{
public SourceA Resolve(SourceA source, Intermediate destination, SourceA destMember, ResolutionContext context)
{
return source;
}
}
public class SourceBResolver : IValueResolver<SourceB, Intermediate, SourceB>
{
public SourceB Resolve(SourceB source, Intermediate destination, SourceB destMember, ResolutionContext context)
{
return source;
}
}
With the above now we are able to replace the todo statement
this.CreateMap<SourceA, Intermediate>()
.ForMember(destination => destination.SourceA, map => map.ResolveUsing<SourceAResolver>());
this.CreateMap<SourceB, Intermediate>()
.ForMember(destination => destination.SourceB, map => map.ResolveUsing<SourceBResolver>());
Finally we register our profile classes to the Automapper
public static class AutomapperProfile
{
public static void Configure()
{
Mapper.Initialize(cfg =>
{
cfg.AddProfile<DestinationProfile>();
cfg.AddProfile<IntermediateProfile>();
});
}
}
Firing up a console with the below code snippet helps testing our stuff
AutomapperProfile.Configure();
var a = new SourceA {A = "Value A"};
var b = new SourceB() {B = "Value B"};
var intermediate = new Intermediate() {SourceA = a, SourceB = b};
var destination = AutoMapper.Mapper.Map<Dest>(intermediate);
Console.WriteLine(destination.ValueFromSourceA);
Console.Read();
Done!
Note: The code snippets provided are just coded to demo the usage/meaning of what is meant by "intermediate" class - have not implemented the way back to the source classes.
Have fun with it :)
I'm testing automapper to see if it can do what I need in other project, so I've made some objects in a console project, simplified but that should have what I need:
public class Person
{
public Person(List<Banco> bancos)
{
this._Bancos = bancos;
}
public int Id { get; set; }
public string Nombre { get; set; }
public DNI Dni { get; set; }
private List<Banco> _Bancos;
public List<Banco> Bancos
{
get { return _Bancos; }
}
}
public class DNI
{
private DNI() { }
public DNI(DNI2 letra)
{
this._Letra = letra;
Console.WriteLine("****DNI CONSTRUCTOR****");
}
public int Id { get; set; }
public int PersonId { get; set; }
public string number { get; set; }
private DNI2 _Letra;
public DNI2 Letra
{
get { return _Letra; }
}
}
public class DNI2
{
public int Id { get; set; }
public int DNIId { get; set; }
public string letra { get; set; }
}
public class Banco
{
public int Id { get; set; }
public string number { get; set; }
}
As you can see, the objects Person and DNI have a custom cosntructor, and I setted the last one's default parameterless constructor to private.
Now, I need to map nested objects with custom constructors from dynamic objects, so I made this in the same console project:
class Program
{
static void Main(string[] args)
{
//OJO TODO: ¿Si un objeto dinamico tiene propiedades de varios tipos, es capaz de diferenciar a qué tipo pertenece cada prop?
//¿qué pasa si un valor del dinamico es del tipo equivocado?
var bancoConfig = new MapperConfiguration(cfg => cfg.CreateMissingTypeMaps = true);
var DNI2Config = new MapperConfiguration(cfg => cfg.CreateMissingTypeMaps = true);
DNI2Config.AssertConfigurationIsValid();
var bancoMapper = bancoConfig.CreateMapper();
var dni2Mapper = DNI2Config.CreateMapper();
var dni2DBConfig = new MapperConfiguration(cfg =>
{
cfg.CreateMissingTypeMaps = true;
cfg.RecognizePrefixes("DNI2");
});
var dni2DBMapper = dni2DBConfig.CreateMapper();
var DNIConfig = new MapperConfiguration(cfg =>
{
cfg.CreateMap<dynamic, DNI>()
.ConstructUsing(dyn => new DNI(dni2DBMapper.Map<DNI2>(dyn)));
/*.ForMember(dest => dest.Id, opts => opts.MapFrom<int>(src => (int)(src as IDictionary<string, object>)["Id"]))
.ForMember(dest => dest.number, opts => opts.MapFrom<string>(src => (string)(src as IDictionary<string, object>)["number"]))
.ForMember(dest => dest.PersonId, opts => opts.MapFrom<int>(src => (int)(src as IDictionary<string, object>)["PersonId"]));*/
/*cfg.CreateMap<IDictionary<string, object>, DNI>()
.ConstructUsing(dyn => new DNI(dni2DBMapper.Map<DNI2>(dyn as dynamic)))
.ForMember(dest => dest.Id, opts => opts.MapFrom<int>(src => (int)(src["Id"])))
.ForMember(dest => dest.number, opts => opts.MapFrom<string>(src => (string)(src["number"])))
.ForMember(dest => dest.PersonId, opts => opts.MapFrom<int>(src => (int)(src["PersonId"])));*/
cfg.CreateMissingTypeMaps = true;
});
var dniMapper = DNIConfig.CreateMapper();
dynamic d = new { Id = 0, number = "1234" };
Banco banco = bancoMapper.Map<Banco>(d);
Console.WriteLine("Banco: " + Environment.NewLine);
Console.WriteLine(banco.Id + Environment.NewLine + banco.number);
Console.ReadLine();
d = new { Id = 1, DNIId = 0, letra = "a" };
DNI2 dni2 = dni2Mapper.Map<DNI2>(d);
Console.WriteLine("DNI2: " + Environment.NewLine);
Console.WriteLine(dni2.Id + Environment.NewLine + dni2.DNIId + Environment.NewLine + dni2.letra);
Console.ReadLine();
d = new { Id = 2, PersonId = 5, number = "5678", DNI2Id = 2, DNIId = 2, letra = "b" };
DNI2 d2 = dni2DBMapper.Map<DNI2>(d);
DNI dni = dniMapper.Map<DNI>(d);
Console.WriteLine("DNI: " + Environment.NewLine);
Console.WriteLine(dni.Id + Environment.NewLine + dni.PersonId + Environment.NewLine + dni.number + Environment.NewLine);
Console.WriteLine("dni.dni2: "+dni.Letra.Id + Environment.NewLine + dni.Letra.DNIId + Environment.NewLine + dni.Letra.letra);
Console.ReadLine();
}
}
That gives this result in the console:
Banco:
0
1234
DNI2:
1
0
a
****DNI CONSTRUCTOR****
DNI:
2
5
5678
dni.dni2: 0
0
So, it just creates the DNI object and set the members, it go through the public constructor... but even when it goes through the constructor it doesn't set the DNI2 property.
If I coment the dynamic line in the DNI configuration:
var DNIConfig = new MapperConfiguration(cfg =>
{
cfg.CreateMap<dynamic, DNI>()
.ConstructUsing(dyn => new DNI(dni2DBMapper.Map<DNI2>(dyn)));
/*.ForMember(dest => dest.Id, opts => opts.MapFrom<int>(src => (int)(src as IDictionary<string, object>)["Id"]))
.ForMember(dest => dest.number, opts => opts.MapFrom<string>(src => (string)(src as IDictionary<string, object>)["number"]))
.ForMember(dest => dest.PersonId, opts => opts.MapFrom<int>(src => (int)(src as IDictionary<string, object>)["PersonId"]));*/
/*cfg.CreateMap<IDictionary<string, object>, DNI>()
.ConstructUsing(dyn => new DNI(dni2DBMapper.Map<DNI2>(dyn as dynamic)))
.ForMember(dest => dest.Id, opts => opts.MapFrom<int>(src => (int)(src["Id"])))
.ForMember(dest => dest.number, opts => opts.MapFrom<string>(src => (string)(src["number"])))
.ForMember(dest => dest.PersonId, opts => opts.MapFrom<int>(src => (int)(src["PersonId"])));*/
//cfg.CreateMissingTypeMaps = true;
});
... so it does not have the config to make it dynamic, the result is:
Banco:
0
1234
DNI2:
1
0
a
****DNI CONSTRUCTOR****
DNI:
0
0
dni.dni2: 2
2
b
The commented lines in the configuration is only part of what I've tried, including two days searching here and google.
Seems that the program is telling me: you map it from a dynamic or you use custom constructor, you can not do both, suck it.
The most intriguing part for me is: using the first configuration, it isn't mapping the DNI2 object, but it is mapping the DNI object using the constructor(the line is at the console), but it don't have the object needed by the parameters, so it is using the constructor without the required parameter O_o
Honestly, I know I'm an amateur and all, but I always thought that even using reflection you couldn't use a constructor(neither any other method) without any of the required parameters.
So: Is there a way to map a nested object with a custom constructor from a dynamic object with automapper?
I have found a solution to this:
var DNIDynConfig = new MapperConfiguration(cfg => cfg.CreateMissingTypeMaps = true);
var dniDynMapper = DNIDynConfig.CreateMapper();
var DNIConfig = new MapperConfiguration(cfg =>
{
cfg.CreateMap<dynamic, DNI>()
.ConstructUsing(dyn => new DNI(dni2DBMapper.Map<DNI2>(dyn)))
.AfterMap((src, dest) =>
{
dniDynMapper.Map(src, dest, typeof(object), typeof(DNI));
});
});
I don't really like it since it seems that it have to map the object twice, but it works. I would appreaciate if someone came with something better though.
Mapper.Initialize(cfg =>
{
cfg.CreateMap<ObjectDTO, Object>().ForMember(obj => obj.LastUpdateDate, opt =>
opt.Condition(pre => pre.LastUpdateDate != null));
}
obj.LastUpdateDate = Datetime.Now;
Mapper.map(objDTO,obj);
after mapping obj.LastUpdateDate will become null despite the condition I created.
happens for all members of the object.
Automapper 5.02
Are you positive LastUpdateDate is null? Datetime has a default value so if you're not expliciting setting it to null your condition won't catch it.
Try:
cfg.CreateMap<ObjectDTO, Object>()
.ForMember(dest => dest.LastUpdateDate, opt => opt.Condition(c => c.LastUpdateDate != null && c.LastUpdateDate != default(DateTime)));
Edit:
class Program
{
static void Main(string[] args)
{
Mapper.Initialize(cfg =>
{
cfg.CreateMap<ObjectDTO, Object>()
.ForMember(dest => dest.Number, opt => opt.Condition(src => src.Number.HasValue))
.ForMember(dest => dest.LastUpdateDate, opt => opt.Condition(src => src.LastUpdateDate.HasValue));
});
Mapper.AssertConfigurationIsValid();
var source = new ObjectDTO { LastUpdateDate = DateTime.Now };
var destination = new Object { Number = 10, LastUpdateDate = DateTime.Now.AddDays(-10) };
var result = Mapper.Map(source, destination);
}
}
public class ObjectDTO
{
public int? Number { get; set; }
public DateTime? LastUpdateDate { get; set; }
}
public class Object
{
public int? Number { get; set; }
public DateTime? LastUpdateDate { get; set; }
}
The condition is on the source object, not on the destination.
So with better naming for variables you are saying:
.ForMember(dest => dest.LastUpdateDate, opt => opt.Condition(src => src.LastUpdateDate != null));
So, what is in your objDTO?
I am trying to map object's of the same type which have a collection of child objects and am finding that Ignore() applied to properties on the child object seem to be umm... ignored!
Here's a unit test which demonstrates the problem.
class A
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<B> Children { get; set; }
}
class B
{
public int Id { get; set; }
public string Name { get; set; }
}
[TestClass]
public class UnitTest1
{
[TestInitialize()]
public void Initialize()
{
Mapper.CreateMap<A, A>()
.ForMember(dest => dest.Id, opt => opt.Ignore());
Mapper.CreateMap<B, B>()
.ForMember(dest => dest.Id, opt => opt.Ignore());
}
[TestMethod]
public void TestMethod1()
{
A src = new A { Id = 0, Name = "Source", Children = new List<B> { new B { Id = 0, Name = "Child Src" } } };
A dest = new A { Id = 1, Name = "Dest", Children = new List<B> { new B { Id = 11, Name = "Child Dest" } } };
Mapper.Map(src, dest);
}
After the Map call the A object's Id property is still 1, as expected, but child B object's Id property is changed from 11 to 0.
Why?
There are several bugs in AutoMapper 4.1.1.
First is about UseDestinationValue: https://github.com/AutoMapper/AutoMapper/issues/568
Second is about nested collections: https://github.com/AutoMapper/AutoMapper/issues/934
Horrifying! The workaround is to map your B instances directly:
Mapper.CreateMap<A, A>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.Children, opt => opt.Ignore());
Mapper.CreateMap<B, B>()
.ForMember(dest => dest.Id, opt => opt.Condition((ResolutionContext src) => false));
and add additional mapping calls:
Mapper.Map(src, dest);
Mapper.Map(src.Children.First(), dest.Children.First()); //example!!!
You may call Mapper.Map in cycle:
for (int i = 0; i < src.Children.Count; i++)
{
var srcChild = src.Children[i];
var destChild = dest.Children[i];
Mapper.Map(srcChild, destChild);
}
This will make things work right.