AutoMapper and Entity Framework Include with circular relationships with No Tracking - c#

Using Entity Framework 6, I'm trying to eagerly load my Caller models from the database using .AsNoTracking(), but I'm hitting a snag when I try to map these models to their ViewModels using AutoMapper 6.
The Caller has an Address, which is a many-to-one relationship (caller's can have one address, address can have multiple callers).
Here are the (reduced) model classes (ViewModels are nearly identical)
public class Caller
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public Address Address { get; set; }
}
public class Address
{
public Guid Id { get; set; }
public string City { get; set; }
public virtual ICollection<Caller> Callers { get; set; }
}
Here is how I am mapping them
// Address
CreateMap<Address, AddressViewModel>()
.ForMember(vm => vm.Id, map => map.MapFrom(m => m.Id))
.ForMember(vm => vm.CallerViewModels, map => map.MapFrom(m => m.Callers))
.ForMember(vm => vm.City, map => map.MapFrom(m => m.City))
.ReverseMap();
// Caller
CreateMap<Caller, CallerViewModel>()
.ForMember(vm => vm.Id, map => map.MapFrom(m => m.Id))
.ForMember(vm => vm.AddressViewModel, map => map.MapFrom(m => m.Address))
.ForMember(vm => vm.FirstName, map => map.MapFrom(m => m.FirstName))
.ReverseMap();
In my CallerRepository I am using this function:
public async Task<Caller> GetFullCallerAsNoTrackingAsync(Guid id)
{
return await _context.Callers
.AsNoTracking()
.Include(c => c.Address)
.FirstOrDefaultAsync(c => c.Id == id);
}
My problem happens here:
// Map Caller to a CallerViewModel
Caller caller = await unitOfWork.CallerRepo.GetFullCallerAsNoTrackingAsync(Guid.Parse(callerId));
CallerViewModel callerViewModel = Mapper.Map<CallerViewModel>(caller); // Throws exception
The exception that gets thrown says
Error Mapping Types ... Caller.Address -> CallerViewModel.Address ... When an object is returned with a NoTracking merge option, Load can only be called when the EntityCollection or EntityReference does not contain objects.
This works just fine when I remove the .AsNoTracking(), but for performance reasons I'm trying to keep that in.
I don't need to know Caller -> Address -> Callers, I just need Caller -> Address
Any suggestions on how I can achieve this?
Edit / Update:
Thanks to FoundNil's answer I was able to get this done.
I changed my Address Map to:
CreateMap<Address, AddressViewModel>()
.ForMember(vm => vm.Id, map => map.MapFrom(m => m.Id))
.ForMember(vm => vm.CallerViewModels, map => map.MapFrom(m => m.Callers).Ignore())
.ForMember(vm => vm.City, map => map.MapFrom(m => m.City))
.ReverseMap();
And I did the same to a different property, CallDetailViewModels, on my Caller -> CallerViewModel map
CreateMap<Caller, CallerViewModel>()
.ForMember(vm => vm.Id, map => map.MapFrom(m => m.Id))
.ForMember(vm => vm.AddressViewModel, map => map.MapFrom(m => m.Address))
.ForMember(vm => vm.CallDetailViewModels, map => map.MapFrom(m => m.CallDetails).Ignore())
The similarities I see between this and Address is that Caller is the parent object of Address, and CallDetail is the parent object of Caller
Both of these parents were navigation properties in their respective Model class:
Caller -> public virtual ICollection<CallDetail> CallDetails { get; set; }
Address -> public virtual ICollection<Caller> Callers { get; set; }
Perhaps this might give a useful flag to others of where they might encounter this problem.
Note: My CallDetail has a many-to-many relationship with Caller, so it also has a navigation property of Callers, and I'm not ignoring that in my CallDetail Map.

I'm not entirely sure why its happening, but I would guess the problem is that when you use .AsNoTracking() something happens between Address -> Callers in the context, so there is no longer a way to map ICollection<Caller> and its view model.
And since you mentioned you only want Caller -> Address you should try this map:
// Address
CreateMap<Address, AddressViewModel>()
.ForMember(x => x.Callers, opt => opt.Ignore())
.ReverseMap();
// Caller
CreateMap<Caller, CallerViewModel>()
.ForMember(vm => vm.AddressViewModel, map => map.MapFrom(m => m.Address))
.ReverseMap();

Related

3rd Level Automapper Relationship

I'm integrating with a 3rd party API which is returning a complex data structure and in a part of it I have the following relationship.
public class Parent{
public List<SmartLink> SmartLink { get; set; }
}
The SmartLink object looks like below:
public class SmartLink {
public Address AddressInfo { get; set; }
}
I have tried to map it in several ways, one of them below, but I still get a null on the AddressInfo object.
cfg.CreateMap<Address, AddressInfo>();
cfg.CreateMap<Source, Parent>()
//This is not allowed since Automapper cannot map to 2nd level
.ForMember(d => d.SmartLink.AddressInfo, map => map.MapFrom(src => src.Smartlink.ToList().Select(addr => addr.Address)));
The line below works perfectly:
.ForMember(d => d.SmartLink, map => map.MapFrom(s => s.Smartlink.ToList()))
How can I map/flatten a 3rd level property with Automapper, any pointers?
I had overthought on it. I simply added the following mapping and it worked.
cfg.CreateMap<Address, AddressInfo>();
cfg.CreateMap<SmartlinkPart, SmartLink>(MemberList.Destination)
.ForMember(d => d.AddressInfo, map => map.MapFrom(s => s.Address));
The idea is that for the member AddressInfo, the first line above will provide it's mapping instruction.

Conditional property mapping in Automapper not working

I am using Automapper 6.2.0 and I have the following classes:
public class User
{
public Address Address { get; set; }
}
public class Address
{
public string Street { get; set; }
}
public class UserDto
{
public string AddressStreet { get; set; }
}
My mapping is configured as follows:
CreateMap<UserDto, User>()
.ForPath(dest => dest.Address.Street, opt => opt.Condition(cond => !string.IsNullOrEmpty(cond.Source.AddressStreet)))
.ForPath(dest => dest.Address.Street, opt => opt.MapFrom(src => src.AddressStreet));
I map UserDto to User like so:
var userDto = new UserDto{ AddressStreet = null };
var user = mapper.Map<User>(userDto);
var address = user.Address;//I expect the prop to be null, since the mapping condition is not met...
This produces a user.Address object instance with Street set to null. I would rather have user.Address not be instantiated at all.
Your mapping configuration throws following exception.
System.ArgumentException occurred
HResult=0x80070057
Message=Expression 'dest => dest.Address.Street' must resolve to top-
level member and not any child object's properties. You can use
ForPath, a custom resolver on the child type or the AfterMap option
instead.
Source=<Cannot evaluate the exception source>
StackTrace:
at AutoMapper.Internal.ReflectionHelper.FindProperty(LambdaExpression
lambdaExpression)
at AutoMapper.Configuration.MappingExpression`2.ForMember[TMember]
(Expression`1 destinationMember, Action`1 memberOptions)
at NetCore.AutoMapperProfile..ctor()
Try the following Mapping configuration instead:
CreateMap<UserDto, User>()
.ForMember(dest => dest.Address, opt => opt.Condition(src => !string.IsNullOrEmpty(src.AddressStreet)))
.ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.AddressStreet));
The above will result in user.Address = null

Mapping to single object from source with collection of object with Automapper

I have a customer object with a collection of addresses that I would like to map to a customer view model with a single address view model. The address from the collection that I want to map to the view model is selected by a specific value in the address. i.e. where type Id == 1
My Automapper config is:
cfg.CreateMap<Customer, CustomerVM>()
.ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.Type.Id== 2).FirstOrDefault())
.ReverseMap();
cfg.CreateMap<Address, AddressVM>()
.ForMember(dest => dest.Street,opt=>opt.MapFrom(src=>src.Street1))
.ForMember(dest => dest.State,opt=>opt.MapFrom(src=>src.Region))
.ForMember(dest => dest.Postal, opt => opt.MapFrom(src => src.PostalCode))
public class Customer{
public virtual ICollection<Address> Addresses{get; set;}
}
public class CustomerVM{
public AddressVM Address{get; set;}
}
This is mapping but the address is null.
Is there a way to select a specific object from a collection and map it to single object.
This works for me.
cfg.CreateMap<Customer, CustomerVM>()
.ForMember(dest => dest.Address, address => address
.MapFrom(src => src.Addresses.FirstOrDefault(add => add.Type.Id == 2)));
Nicely it won't throw or map if there is no address.type == 2

EF Code First: Map only one property while leaving other properties as default

I have class Product with more than 20 properties. I want to map only Picture to another table and leave others to get mapped to the table Product.
class Product
{
public int Id {get; set;}
public byte[] Picture {get; set;}
...
...
}
The only way I know is through ModelBuilder:
modelBuilder.Entity<Product>()
.Map(m =>
{
m.Property(p => p.Picture);
m.ToTable("ProductPic");
})
.Map(m =>
{
// All other properties here:
m.Property(p => p.Id);
m.ToTable("Product");
// But there are too many!!!
});
As shown above, mapping all other properties is tedious. Is there any way to exclude just one property to go different way and leave other follow the default?
This syntax would ease the pain a bit:
modelBuilder.Entity<Product>()
.Map(m =>
{
m.Properties(p => new
{
p.Picture
});
m.ToTable("ProductPic");
}
.Map(m =>
{
m.Properties(p => new
{
p.Field1,
p.Field2,
...
p.Fieldn
});
m.ToTable("Product");
}
Don't need to map the ID and ToTable() only specified once.

AutoMapper: two-way, deep mapping, between domain models and viewmodels

I need to map two ways between a flat ViewModel and a deep structured Domain model. This will be a common scenario in our solution.
My models are:
public class Client
{
...
public NotificationSettings NotificationSettings { get; set; }
public ContactDetails ContactDetails { get; set; }
...
}
public class NotificationSettings
{
...
public bool ReceiveActivityEmails { get; set; }
public bool ReceiveActivitySms { get; set; }
...
}
public class ContactDetails
{
...
public string Email { get; set }
public string MobileNumber { get; set; }
...
}
public class ClientNotificationOptionsViewModel
{
public string Email { get; set }
public string MobileNumber { get; set; }
public bool ReceiveActivityEmails { get; set; }
public bool ReceiveActivitySms { get; set; }
}
Mapping code:
Mapper.CreateMap<Client, ClientNotificationOptionsViewModel>()
.ForMember(x => x.ReceiveActivityEmails, opt => opt.MapFrom(x => x.NotificationSettings.ReceiveActivityEmails))
.ForMember(x => x.ReceiveActivitySms, opt => opt.MapFrom(x => x.NotificationSettings.ReceiveActivitySms))
.ForMember(x => x.Email, opt => opt.MapFrom(x => x.ContactDetails.Email))
.ForMember(x => x.MobileNumber, opt => opt.MapFrom(x => x.ContactDetails.MobileNumber));
// Have to use AfterMap because ForMember(x => x.NotificationSettings.ReceiveActivityEmail) generates "expression must resolve to top-level member" error
Mapper.CreateMap<ClientNotificationOptionsViewModel, Client>()
.IgnoreUnmapped()
.AfterMap((from, to) =>
{
to.NotificationSettings.ReceiveActivityEmail = from.ReceiveActivityEmail;
to.NotificationSettings.ReceiveActivitySms = from.ReceiveActivitySms;
to.ContactDetails.Email = from.Email;
to.ContactDetails.MobileNumber = from.MobileNumber;
});
...
// Hack as ForAllMembers() returns void instead of fluent API syntax
public static IMappingExpression<TSource, TDest> IgnoreUnmapped<TSource, TDest>(this IMappingExpression<TSource, TDest> expression)
{
expression.ForAllMembers(opt => opt.Ignore());
return expression;
}
I dislike it because:
1) It is cumbersome
2) The second mapping pretty much dismantles Automapper's functionality and implements the work manually - the only advantage of it is consistency of referencing Automapper throughout the code
Can anyone suggest:
a) A better way to use Automapper for deep properties?
b) A better way to perform two-way mapping like this?
c) Advice on whether I should bother using Automapper in this scenario? Is there a compelling reason not to revert to the simpler approach of coding it up manually? eg.:
void MapManually(Client client, ClientNotificationOptionsViewModel viewModel)
{
viewModel.Email = client.ContactDetails.Email;
// etc
}
void MapManually(ClientNotificationOptionsViewModel viewModel, Client client)
{
client.ContactDetails.Email = viewModel.Email;
// etc
}
-Brendan
P.S. restructuring domain models is not the solution.
P.P.S It would be possible to clean up the above code through extension methods & some funky reflection to set deep properties... but I'd rather use automapper features if possible.
This can be done also in this way:
Mapper.CreateMap<ClientNotificationOptionsViewModel, Client>()
.ForMember(x => x.NotificationSettings, opt => opt.MapFrom(x => new NotificationSettings() { ReceiveActivityEmails = x.ReceiveActivityEmails, ReceiveActivitySms = x.ReceiveActivitySms}))
.ForMember(x => x.ContactDetails, opt => opt.MapFrom(x => new ContactDetails() { Email = x.Email, MobileNumber = x.MobileNumber }));
But is not much different than your solution.
Also, you can do it by creating a map from your model to your inner classes:
Mapper.CreateMap<ClientNotificationOptionsViewModel, ContactDetails>();
Mapper.CreateMap<ClientNotificationOptionsViewModel, NotificationSettings>();
Mapper.CreateMap<ClientNotificationOptionsViewModel, Client>()
.ForMember(x => x.NotificationSettings, opt => opt.MapFrom(x => x))
.ForMember(x => x.ContactDetails, opt => opt.MapFrom(x => x));
You don't need to specify ForMember in the new mappings because the properties has the same name in both classes.
In the end I found AutoMapper was unsuited to my scenario.
Instead I built a custom utility to provide bidirectional mapping & deep property mapping allowing configuration as follows. Given the scope of our project I believe this is justified.
BiMapper.CreateProfile<Client, ClientNotificationsViewModel>()
.Map(x => x.NotificationSettings.ReceiveActivityEmail, x => x.ReceiveActivityEmail)
.Map(x => x.NotificationSettings.ReceiveActivitySms, x => x.ReceiveActivitySms)
.Map(x => x.ContactDetails.Email, x => x.Email)
.Map(x => x.ContactDetails.MobileNumber, x => x.MobileNumber);
BiMapper.PerformMap(client, viewModel);
BiMapper.PerformMap(viewModel, client);
Apologies I cannot share the implementation as it's commercial work. However I hope it helps others to know that it isn't impossible to build your own, and can offer advantages over AutoMapper or doing it manually.

Categories

Resources