Mapping to single object from source with collection of object with Automapper - c#

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

Related

How to merge two nested object into one using AutoMapper

Hi I have been using AutoMapper to convert my objects, and now I'm trying to merge two nested objects into one, but I can't figure out how to do it.
I have the following code:
These are my source objects
class SourceSubItemA
{
string subPropertyA;
}
class SourceSubItemB
{
string subPropertyB;
}
class Source
{
SourceSubItemA subItemA;
SourceSubItemB subItemB;
}
Destination objects
class DestinationSubItem
{
string propertyA;
string propertyB;
}
class Destination
{
DestinationSubItem destItem;
}
This is my Automapper configuration
Mapper.CreateMap<SourceSubItemA, DestinationSubItem>()
.ForMember(dest => propertyA, opt => opt.MapFrom(src => src.subPropertyA));
Mapper.CreateMap<SourceSubItemB, DestinationSubItem>()
.ForMember(dest => propertyB, opt => opt.MapFrom(src => src.subPropertyB));
// Probably I have to do something more here.
Mapper.CreateMap<Source, Destination>()
.ForMember(dest => dest.destItem, opt => opt.MapFrom(src => src.subItemA));
And finally I'm using the mapper by this way
Mapper.Map<Destination>(sourceObject);
The expected result must be a Destination object with a subitem with both properties filled.
Please Help!
As I can see, Destination is just a wrapper of DestinationSubItem that is the one that really needs to get the values from Source.
In that case, you could define a mapper from Source to Destination that puts in dest.destItem the result of mapping from Source directly to DestinationSubItem.
Here an example:
Mapper.CreateMap<Source, Destination>()
.ForMember(dest => dest.destItem, opt => opt.MapFrom(src => Mapper.Map<DestinationSubItem>(src));
Mapper.CreateMap<Source, DestinationSubItem>()
.ForMember(dest => dest.propertyA, opt => opt.MapFrom(src => src.subItemA.subPropertyA)
.ForMember(dest => dest.propertyB, opt => opt.MapFrom(src => src.subItemB.subPropertyB);

AutoMapper - mapping individual ints to an array

I have 12 integers representing 12 months in an old database, and I need to map them to an array/list. The problem is I'm not sure how I can initialize the array in the model to size 12, in order to map it.
Here's what I'm trying to do:
Model:
public class Year
{
public int[] Months { get; set; } //How do I initialize to 12?
}
Mapping:
CreateMap<DataRow, Year>()
.ForMember(dest => dest.Months[0], opt => opt.MapFrom(src => src["Jan"]))
.ForMember(dest => dest.Months[1], opt => opt.MapFrom(src => src["Feb"]))
.ForMember(dest => dest.Months[2], opt => opt.MapFrom(src => src["Mar"]))
I've searched everywhere for pre-initialized arrays in models, but couldn't find anything on the syntax.
Very simple:
public class Year
{
public int[] Months { get; } = new int[12];
}
I also suggest to remove setter, make it as read only property - does not change ability to set individual items in array itself.

AutoMapper and Entity Framework Include with circular relationships with No Tracking

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();

Automapper. Map if source member is null

I have two classes and map one to other with Automapper. For instance:
public class Source
{
// IdName is a simple class containing two fields: Id (int) and Name (string)
public IdName Type { get; set; }
public int TypeId {get; set; }
// another members
}
public class Destination
{
// IdNameDest is a simple class such as IdName
public IdNameDest Type { get; set; }
// another members
}
Then I use Automapper to map Source to Destination:
cfg.CreateMap<Source, Destination>();
It works properly but sometimes member Type in class Source becomes null. In these cases I would like to map member Type in class Destination from TypeId property. That's what I want in a nutshel:
if Source.Type != null
then map Destination.Type from it
else map it as
Destination.Type = new IdNameDest { Id = Source.Id }
Is it possible with AutoMapper?
You can use the .ForMember() method while declaring the mapping.
Like so :
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type != null ? src.Type : new IdNameDest { Id = src.Id }));
While LeeeonTMs answer works fine AutoMapper provides a specialised mechanism to substitute null values. It "allows you to supply an alternate value for a destination member if the source value is null anywhere along the member chain" (taken from the AutoMapper manual).
Example:
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Value, opt => opt.NullSubstitute(new IdNameDest { Id = src.Id }));
With C# 6.0, the null-coalescing operator can be used.
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type ?? new IdNameDest { Id = src.Id }));
I managed to resolve it with Mapping Resolvers
public class SomeResolver : IValueResolver<Soruce, Dest, Guid>
{
public Guid Resolve(Source source, Dest destination, Guid, destMember, ResolutionContext context)
{
destination.Value= source.Value!=null ? source.Value:0;
return destination.MainGuid = Guid.NewGuid();
}
}
and then on mapping configuraiton
CreateMap<BioTimeEmployeeSummaryDTO, BioTimeEmployeeAttendanceSummary>()
.ForMember(dest => dest.MainGuid, opt => opt.MapFrom<YourResolverClass>())
.ReverseMap();

Dynamically passing properties in Select for projection in Entity Framework

Is there a way to pass the properties I want to retrieve from the DB in a Select dynamically, I don't know the properties I need beforehand and I don't want to write the conditions in my repository.
I don't want to retrieve all the fields at once, just the ones I need based on some conditions.
For example:
public class Student
{
public string Property1 {get; set;}
public string Property2 {get; set;}
//other properties here
[NotMapped]
public string Selected
{
if(condition)
return Property1;
else
return Property2;
}
}
and in the service layer I have
query.Select(s => new StudentViewModel
{
Value = s.Selected; //this one will get the property we want based on a condition
//other stuff here
//OtherValue = s.OtherProperty
}
).FirstOrDefault();
Selector
An easy but ugly way is to use a Selector:
query.Select(Selector()).FirstOrDefault();
And the Selector can look like this:
private static Expression<Func<Student, StudentViewModel>> Selector()
{
if (Condition())
return x => new StudentViewModel
{
Name = x.Property1,
OtherName = x.OtherName
};
return x => new StudentViewModel
{
Name = x.Property2,
OtherName = x.OtherName
};
}
As you can see the obviously downside here is that you need to copy/paste all other selected properties. That is why its ugly.
AutoMapper
Configs
You can use AutoMapper with different configurations. First you need to define a standard mapping for all properties that don't need to be dynamic.
public static void AddStandardStudentMap(this IMappingExpression<Student, StudentViewModel> map)
{
map.ForMember(dest => dest.OtherName, opt => opt.MapFrom(src => src.OtherProperty))
.ForMember(dest => dest.OtherName2, opt => opt.MapFrom(src => src.OtherProperty2));
// you can concat .ForMember() for each property you need.
}
Next, you need to define the different configs and add the AddStandardStudentMap method to each invidual mapping.
var config1 = new MapperConfiguration(cfg =>
cfg.CreateMap<Student, StudentViewModel>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Property1))
.AddStandardStudentMap()
);
var config2 = new MapperConfiguration(cfg =>
cfg.CreateMap<Student, StudentViewModel>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Property2))
.AddStandardStudentMap()
);
After this, just use your conditions to decide which config do you need
IConfigurationProvider provider;
if(Condition())
provider = config1;
else
provider = config2;
And then instead of .Select() use:
query.ProjectTo<StudentViewModel>(provider).FirstOrDefault();
As we can see this solution is still ugly and has a lot of overhead but its needed in some cases, thats why i stated it here.
Expression
This is a bit similar to the Configs but brings you more flexibility and less writing effort.
First create a config but this time with a Selector
var config = new MapperConfiguration(cfg =>
cfg.CreateMap<Student, StudentViewModel>()
.ForMember(dest => dest.OtherName, opt => opt.MapFrom(src => src.OtherName))
.ForMember(dest => dest.OtherName2, opt => opt.MapFrom(src => src.OtherName2))
// and so on. Map all your properties that are not dynamically.
// and then the Selector
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => Selector()))
);
The Selector method can look like this:
private static Expression<Func<Student, StudentViewModel>> Selector()
{
if(Condition())
return src => src.Property1;
else
return src => src.Property2;
}
And then just use it like the configs solution but without selecting a particular config:
query.ProjectTo<StudentViewModel>(config).FirstOrDefault();
Conclusion
I know this is a lot input and there are even more possibilities to achieve the behaviour that you want, with or without AutoMapper. But i would suggest you (grounded on the information you gave us) to use AutoMapper with Expressions. It should give the flexibility you need and is extensible for further requirements.

Categories

Resources