Automapper Expression Mapping Issue - c#

Expression mapping using AutoMapper.Extensions.Expression is not mapping the expression.
Here are my models:
public class UserDto
{
public int Id { get; set; }
public User User { get; set; }
//other info
}
public class User
{
public string Email { get; set; }
//other info
}
public class UserEntity
{
public int Id { get; set; }
public string Email { get; set; }
//other info
}
My mappers:
public static class MapperConfiguration
{
public static void Configure()
{
Mapper.Initialize(cfg =>
{
cfg.AddProfile(new UserDtoProfile());
});
}
}
internal class UserDtoProfile : Profile
{
public UserDtoProfile()
{
CreateMap<UserDto, UserEntity>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.User.Email));
CreateMap<UserEntity, UserDto>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.User, opt => opt.MapFrom(src => Mapper.Map<UserEntity>(src)));
CreateMap<UserEntity, User>()
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email)).ReverseMap();
}
}
Add a snippet to reproduce the issue:
static void Main(string[] args)
{
MapperConfiguration.Configure();
var email = "test#test.com";
Expression<Func<UserDto, bool>> filterExpression = x => x.User.Email == email;
var expression = Mapper.Map<Expression<Func<UserEntity, bool>>>(filterExpression);
Console.WriteLine("Old Expression : " + filterExpression.Body);
Console.WriteLine("New Expression : " + expression.Body);
Console.Read();
}
The output of this is:
Old Expression : (x.User.Email == "asd")
New Expression : (x.User.Email == "asd")
However, I'm expecting it to output:
Old Expression : (x.User.Email == "asd")
New Expression : (x.Email == "asd")
The particular lines that aren't working as I expect are:
Expression<Func<UserDto, bool>> filterExpression = x => x.User.Email == email;
var expression = Mapper.Map<Expression<Func<UserEntity, bool>>>(filterExpression);
I'm using AutoMapper v8.0.0 and AutoMapper.Extensions.Expression v2.0.0

You forgot to configure the Expression mapping. Your setup should be:
Mapper.Initialize(cfg =>
{
cfg.AddExpressionMapping();
cfg.AddProfile(new UserDtoProfile());
});
I haven't really used AutoMapper, and have never used the expression mapping, but as per this github issue, the mapping appears to be backwards for expressions. Your current mapping won't work, and will crash when executing. You'll need to change the mapping to be:
internal class UserDtoProfile : Profile
{
public UserDtoProfile()
{
CreateMap<UserDto, UserEntity>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.User.Email))
.ReverseMap();
CreateMap<UserEntity, User>()
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email)).ReverseMap();
}
}
Running this gives the output:
Old Expression : (x.User.Email == "asd")
New Expression : (x.Email == "asd")

Related

ASP .Net Core: how to config auto mapper profile for different source and destination

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,

How to Map to abstract conditionally

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)

Automapper - reuse of mapping rules for projection

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 :)

AutoMapper don't ignore null properties despite conditions

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?

AutoMapper Ignore on child collection property

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.

Categories

Resources