I'm a AutoMapper newb. My mappings are not working as expected and I'm sure I'm doing somthing wrong but can't figure it out. Sorry if this question is confusing, but i'll do my best to be clear. Lets say we have three classes:
public class Person
{
public ContactInfo1 Contact { get; set; }
}
public class ContactInfo1
{
public string Name { get; set; }
}
public class ContactInfo2
{
public string AnotherName { get; set; }
}
Now, I want to setup my mappings so that ContactInfo1 can map to and from ContactInfo2. And then I want to be able to map Person1 -> ContactInfo2 (which might look stange, but I need to do it anyway). I have tried the following mapping config:
var autoMapperConfig = new AutoMapper.MapperConfiguration(cfg =>
{
cfg.CreateMap<ContactInfo1, ContactInfo2>()
.ForMember(dest => dest.AnotherName, opt => opt.MapFrom(src => src.Name)).ReverseMap();
cfg.CreateMap<ContactInfo2, Person>()
.ForMember(dest => dest.Contact, opt => opt.MapFrom(src => src)).ReverseMap();
});
var mapper = autoMapperConfig.CreateMapper();
For the test data:
var testPerson = new Person();
testPerson.Contact = new ContactInfo1() { Name = "Person1" };
I do the following:
var contactInfo2Test = mapper.Map<Person, ContactInfo2>(testPerson);
This does NOT give me any errors, but contactInfo2Test.AnotherName is empty. Please advise! Thanks.
Please note that I realize I could go:
cfg.CreateMap<Person, ContactInfo2>()
.ForMember(dest => dest.AnotherName, opt => opt.MapFrom(src => src.Contact.Name));
Bu then I would have mapped Contact1->Contact2 all over again, and in a more complex scenario I really want to avoid that.
Here's one way of doing it:
var autoMapperConfig = new AutoMapper.MapperConfiguration(cfg =>
{
cfg.CreateMap<ContactInfo1, ContactInfo2>()
.ForMember(dest => dest.AnotherName, opt => opt.MapFrom(src => src.Name))
.ReverseMap();
cfg.CreateMap<Person, ContactInfo2>()
.ConstructUsing((p, ctx) => ctx.Mapper.Map<ContactInfo2>(p.Contact));
});
Related
I want to convert an array of ids from a dto to a list of related objects using AutoMapper, however, the mapping only works one way.
The current setup is the following:
public class Group
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Location> Locations { get; set; }
}
public class GroupDto
{
public int id { get; set; }
public string name { get; set; }
public int[] locationIds { get; set; }
}
And the code in the GroupProfile:
CreateMap<Group, GroupDto>()
.ForMember(dest => dest.id, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.name, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.locationIds, opt =>
opt.MapFrom(src => src.Locations.Select(l => l.Id).ToArray()));
This code works when I try to convert a Group to a GroupDto, but not when I try it the other way around. The following error occurs:
An unhandled exception has occurred while executing the request.
AutoMapper.AutoMapperMappingException: Missing type map configuration or unsupported mapping.
Mapping types:
GroupDto -> Group
Also, when I do it without the AutoMapper like below, it just works:
Group group = new Group
{
Name = groupDto.name,
Locations = _context.Locations
.Where(l => groupDto.locationIds.Contains(l.Id))
.ToList()
};
Help is appreciated!
AutoMapper cannot convert an int[] to a ICollection<Location>. You need to create a seperate mapping for this.
CreateMap<GroupDto, Group>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.id))
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.name))
.ForMember(dest => dest.Locations, opt => opt.MapFrom(src => src.Select(l => new Location { Id = src }));
I haven't tested this snippet so I hope you get the gist of it.
Your AutoMapper doesn't find the mapping from GroupDto to Group.
You need .ReverseMap() to support reverse mapping from Group to GroupDto.
Mapping configuration for mapping from Location to int and vice versa via .ConstructUsing().
Modify the mapping for the member locationIds.
The mapping for the members id and name may be omitted as AutoMapper will perform the mapping between members by ignoring case-sensitive.
CreateMap<Location, int>()
.ConstructUsing(src => src.Id);
CreateMap<int, Location>()
.ConstructUsing(src => new Location { Id = src });
CreateMap<Group, GroupDto>()
.ForMember(dest => dest.locationIds, opt => opt.MapFrom(src => src.Locations))
.ReverseMap();
Demo # .NET Fiddle
it seems like i cannot get nested mappings working properly
after reading: http://docs.automapper.org/en/stable/Nested-mappings.html it should be very easily.
i have the following classes:
public class CatalogVehicle : VehicleBase
{
public string Type { get; set; }
public int Year { get; set; }
public VehicleSpecification VehicleSpecification { get; set; } = new VehicleSpecification();
} //removed some properties for readability
public class VehicleSpecification
{
public Engine Engine { get; set; } = new Engine();
public Transmission Transmission { get; set; } = new Transmission();
public int Co2 { get; set; }
public int Weight { get; set; }
} //again removed some more properties (all classes)
for mapping:
CreateMap<VehicleAndQuote, CatalogVehicle>()
.ForMember(catalogVehicle => catalogVehicle.Id,
source => source.MapFrom(vehicleAndQuote => vehicleAndQuote.Quote.QuotationIdentifier.Trim()))
.ForMember(catalogVehicle => catalogVehicle.Make,
source => source.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.VehicleMakeName))
.ForMember(catalogVehicle => catalogVehicle.Model,
source => source.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.VehicleModelTypeName))
.ForMember(catalogVehicle => catalogVehicle.VehicleSpecification, opt => opt.Ignore()); //removed some lines
CreateMap<VehicleAndQuote, VehicleSpecification>()
.ForMember(vehicleSpecification => vehicleSpecification.Co2, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.Co2.ToSafeInt()))
.ForMember(vehicleSpecification => vehicleSpecification.Weight, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.Weight.ToSafeInt()))
.ForMember(vehicleSpecification => vehicleSpecification.Rating, opt => opt.Ignore())
.ForMember(vehicleSpecification => vehicleSpecification.Tyres, opt => opt.Ignore()) //removed some lines as well
CreateMap<VehicleAndQuote, Engine>()
.ForMember(engine => engine.Displacement, opt => opt.Ignore())
.ForMember(engine => engine.Fuel, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.FuelType))
.ForMember(engine => engine.Power, src => src.ResolveUsing(GetEnginePower))
.ForMember(engine => engine.Cylinders, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote.Vehicle.Cylinders));
//etc
as you can see i am ignoring the some properties because otherwise i get the unmapped properties error. After reading the article it should work, as long as you have all classes mapped.
i am calling the method like: var vehicle = _mapper.Map<VehicleAndQuote, CatalogVehicle>(vehicleAndQuote); <= this is the big class that contains all the information
so from mapping from VehicleAndQuote to CatalogVehicle -first few properties work properly, but then the mapping to the VehicleSpecification lies my problem. that one will not be populated properly...
does anyone see the problem?
You need to configure mapping for nested classes instead of ignoring them:
.ForMember(catalogVehicle => catalogVehicle.VehicleSpecification, src => src.MapFrom(vehicleAndQuote => vehicleAndQuote);
instead of
.ForMember(catalogVehicle => catalogVehicle.VehicleSpecification, opt => opt.Ignore());
And the same for other mappings
I'm having this exception when I'm trying to map from one object to another.
On my global.asax.cs I got this:
RoleManager<IdentityRole> roleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(new AppContext()));
Mapper.Initialize(cfg =>
{
cfg.CreateMap<AppUser, TokenAuthorizationModel>()
.ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.UserName))
.ForMember(dest => dest.Role, opt => opt.MapFrom(src => roleManager.FindById(src.Roles.First().RoleId).Name));
});
And I got this AutoMapper.AutoMapperMappingException exception on my login controller, especifically on this line:
TokenAuthorizationModel tokenClaims = Mapper.Map<TokenAuthorizationModel>(validUser);
And these are my models:
public class AppUser : IdentityUser
{
public virtual List<CourseModel> Courses { get; set; }
public string FullName { get; set; }
public int Reputation { get; set; }
}
And destination:
public class TokenAuthorizationModel
{
public string UserName { get; set; }
public string Role { get; set; }
}
Can anybody give me hand? Thanks in advance! :)
I think the problem is at this part:
.ForMember(dest => dest.Role, opt => opt.MapFrom(src => roleManager.FindById(src.Roles.First().RoleId).Name));
I would not use MapFrom in this case. Try to use ResolveUsing method instead which takes a lambda function.
.ForMember(dest => dest.Role, opt => opt.ResolveUsing(src => roleManager.FindById(src.Roles.First().RoleId).Name));
If this does not work please let me know!
You should write a resolver for the Role property and use dependency injection inside it so you have the right DbContext as you would anywhere else in your app.
I have a pretty basic Entity Framework entity that looks like this:
public class Student
{
public string Given { get; set; }
public string Surname { get; set; }
public ICollection<Address> Addresses { get; set; }
}
I'd like to use AutoMapper to map this entity to a corresponding flattened ViewModel that looks like this:
public class StudentViewModel
{
public string Given { get; set; }
public string Surname { get; set; }
public string PhysicalAddressStreet { get; set; }
public string PhysicalAddressCity { get; set; }
public string PhysicalAddressState { get; set; }
public string PostalAddressStreet { get; set; }
public string PostalAddressCity { get; set; }
public string PostalAddressState { get; set; }
}
For this I've tried the following mapping configuration:
CreateMap<Student, StudentViewModel>()
.ForMember(dest => dest.Given, opt => opt.MapFrom(src => src.Given))
.ForMember(dest => dest.Surname, opt => opt.MapFrom(src => src.Surname))
.ForMember(dest => dest.PhysicalAddressStreet, opt => opt.MapFrom(src => src.Addresses.FirstOrDefault(add => add.Type == AddressType.Physical).Street))
.ForMember(dest => dest.PhysicalAddressCity, opt => opt.MapFrom(src => src.Addresses.FirstOrDefault(add => add.Type == AddressType.Physical).City))
.ForMember(dest => dest.PhysicalAddressState, opt => opt.MapFrom(src => src.Addresses.FirstOrDefault(add => add.Type == AddressType.Physical).State))
.ForMember(dest => dest.PostalAddressStreet, opt => opt.MapFrom(src => src.Addresses.FirstOrDefault(add => add.Type == AddressType.Postal).Street))
.ForMember(dest => dest.PostalAddressCity, opt => opt.MapFrom(src => src.Addresses.FirstOrDefault(add => add.Type == AddressType.Postal).City))
.ForMember(dest => dest.PostalAddressState, opt => opt.MapFrom(src => src.Addresses.FirstOrDefault(add => add.Type == AddressType.Postal).State));
The problem is, when I run this mapping using projections:
studentDbSet.Where(st => st.Id == studentId)
.ProjectTo<TProjection>(_mapper.ConfigurationProvider);
I get the following error:
Dynamic SQL Error SQL error code = -104 Token unknown - line 14,
column 2 OUTER
This is a Firebird error, it seems that when compiling the Linq to SQL the query that is being generated includes OUTER APPLY, which is not supported in Firebird.
Is there any way to rework my projection to avoid the OUTER APPLY?
To the best of my knowledge, the OUTER APPLY is generated from the FirstOrDefault() call. Is there another way I can write the Linq to avoid using that?
Edit for clarification: This is a situation where I am not in a position to be able to modify the Entity or the database schema, so assume that those are untouchable.
I think you have a modeling problem at the core here. If you need the physical address, just include a PhysicalAddress property on the model, and maintain that relationship. You can still have the collection of addresses with the type. It looks like you're doing "FirstOrDefault", meaning either you can only have one physical address or only the first matters. I'm guessing it's that you can only have one.
So just have one. On the Student model (and Student table), have a FK to the Address table, "PhysicalAddress". Then in the places in the code you maintain addresses, update the PhysicalAddress appropriately. Encapsulating the child collection so that you can't do just any add/remove operation helps.
Once you have a PhysicalAddress relationship on the Student, this problem becomes trivial, it's just a normal mapping.
Here is the only way of writing the LINQ query that avoids OUTER APPLY (not sure how that can be mapped with AutoMapper, leaving that part for you if you really need it):
var query =
from student in studentDbSet
where student.Id == studentId
from physicalAddress in student.Addresses.Where(a => a.Type == AddressType.Physical)
from postalAddress in student.Addresses.Where(a => a.Type == AddressType.Postal)
select new StudentViewModel
{
Given = student.Given,
Surname = student.Surname,
PhysicalAddressStreet = physicalAddress.Street,
PhysicalAddressCity = physicalAddress.City,
PhysicalAddressState = physicalAddress.State,
PostalAddressStreet = postalAddress.Street,
PostalAddressCity = postalAddress.City,
PostalAddressState = postalAddress.State,
};
I'm trying to use AutoMapper to flatten multiple levels of arrays.
Consider the following source classes:
class X {
public string A { get; set; }
public Y[] B { get; set; }
}
class Y {
public string C { get; set; }
public Z[] D { get; set; }
}
class Z {
public string E { get; set; }
public string F { get; set; }
}
And the following destination:
class Destination {
public string A { get; set; }
public string C { get; set; }
public string E { get; set; }
public string F { get; set; }
}
What I'd like to be able to do is get a List from one or more X, e.g.:
Mapper.Map<IEnumerable<X>, IEnumerable<Destination>>(arrayOfX);
I'm unable to figure out what sort of mapping configuration to use to achieve this. MapFrom seems like the way to go for 1:1 compositions, but doesn't seem to be able to handle the array (or other enumerable) unless I use AutoMapper's destination naming convention.
Any insights on how to achieve this?
Try this mapper,
Mapper.CreateMap<Z, Destination>();
Mapper.CreateMap<Y, Destination>();
Mapper.CreateMap<X, Destination>()
.ForMember(destination => destination.A, options => options.MapFrom(source => source.A)).IgnoreAllNonExisting()
.ForMember(destination => destination.C, options => options.MapFrom(source => Mapper.Map<IEnumerable<Y>, IEnumerable<Destination>>(source.B).FirstOrDefault().C))
.ForMember(destination => destination.E, options => options.MapFrom(source => Mapper.Map<IEnumerable<Z>, IEnumerable<Destination>>(source.B.SelectMany(d => d.D)).FirstOrDefault().E))
.ForMember(destination => destination.F, options => options.MapFrom(source => Mapper.Map<IEnumerable<Z>, IEnumerable<Destination>>(source.B.SelectMany(d => d.D)).FirstOrDefault().F));
var result = Mapper.Map<IEnumerable<X>, IEnumerable<Destination>>(arrayOfX);
I had a very similar problem a while ago. I had a collection of locations, and each location had a collection of streets. I wanted to map them to a collection of view models where each view model represented a street (including the location details).
This was my solution: https://groups.google.com/forum/#!topic/automapper-users/b66c1M8eS8E
For this particular problem, this could be your mapping configuration:
public static class AutoMapperConfig
{
public static void Configure()
{
Mapper.CreateMap<Z, Destination>()
.ForMember(dest => dest.A, opt => opt.Ignore())
.ForMember(dest => dest.C, opt => opt.Ignore());
Mapper.CreateMap<Y, Destination>()
.ForMember(dest => dest.A, opt => opt.Ignore())
.ForMember(dest => dest.E, opt => opt.Ignore())
.ForMember(dest => dest.F, opt => opt.Ignore());
Mapper.CreateMap<X, Destination>()
.ForMember(dest => dest.C, opt => opt.Ignore())
.ForMember(dest => dest.E, opt => opt.Ignore())
.ForMember(dest => dest.F, opt => opt.Ignore());
}
}
Because AutoMapper is primarily a 1:1 mapping, you need to implement a wee bit of magic to map to multiple objects. This is an example of how you could call that mapping to populate your object:
var rc = data.SelectMany(
x => x.B.SelectMany(
y => y.D
.Select(Mapper.Map<Z, Destination>)
.Select(z => Mapper.Map(y, z))
)
.Select(y => Mapper.Map(x, y))
);
Here are a couple of unit tests to validate the mapping and show it in action:
[TestFixture]
public class MapperTests
{
[Test]
public void Mapping_Configuration_IsValid()
{
AutoMapperConfig.Configure();
Mapper.AssertConfigurationIsValid();
}
[Test]
public void Mapping_TestItems_MappedOK()
{
AutoMapperConfig.Configure();
Mapper.AssertConfigurationIsValid();
var data = new[]
{
new X
{
A = "A1",
B = new[]
{
new Y
{
C = "A1C1",
D = new[]
{
new Z
{
E = "A1C1E1",
F = "A1C1F1"
},
new Z
{
E = "A1C1E2",
F = "A1C1F2"
},
}
},
new Y
{
C = "A1C2",
D = new[]
{
new Z
{
E = "A1C2E1",
F = "A1C2F1"
},
new Z
{
E = "A1C2E2",
F = "A1C2F2"
},
}
}
}
}
};
var rc = data.SelectMany(
x => x.B.SelectMany(
y => y.D
.Select(Mapper.Map<Z, Destination>)
.Select(z => Mapper.Map(y, z))
)
.Select(y => Mapper.Map(x, y))
);
Assert.That(rc, Is.Not.Null);
Assert.That(rc.Count(), Is.EqualTo(4));
var item = rc.FirstOrDefault(x => x.F == "A1C2F2");
Assert.That(item, Is.Not.Null);
Assert.That(item.A, Is.EqualTo("A1"));
Assert.That(item.C, Is.EqualTo("A1C2"));
Assert.That(item.E, Is.EqualTo("A1C2E2"));
Assert.That(item.F, Is.EqualTo("A1C2F2"));
}
}
For anyone else coming across this post by searching how to Flatten object structure with AutoMapper - the new AutoMapper supports flattening with IncludeMembers() syntax.
Source: http://docs.automapper.org/en/stable/Flattening.html
So the original issue could be solved like this:
Mapper.CreateMap<Z, Destination>();
Mapper.CreateMap<Y, Destination>().IncludeMembers(src => src.D);
Mapper.CreateMap<X, Destination>().IncludeMembers(src => src.B);