I have the DTO below in which I need to map it to a flat view model, the idea is that some of the properties that come through from the request are shared, but there could be a list of names that come through.
public class ShinyDTO
{
public List<UserDetails> Users { get; set; }
public string SharedPropertyOne { get; set; }
public string SharedPropertyTwo { get; set; }
}
public class UserDetails
{
public string Title { get; set; }
public string Forename { get; set; }
public string Surname { get; set; }
}
public class MyRealClass
{
public string SharedPropertyOne {get;set;}
public string SharedPropertyTwo {get;set;}
public string Title {get;set;}
public string Forename {get;set;}
public string Surname {get;set;}
}
//This will map all the shared properties
MyRealClass request = Mapper.Map<MyRealClass>(dto);
foreach (var record in dto.Users){
//This bit overwrites the properties set above and then I only have the properties set for Forename, Surname, etc...
request = Mapper.Map<MyRealClass>(record);
}
I need to map this into a list of MyRealClass. I've tried creating seperate mappings and then looping it within a foreach, but this kept removing the initial attributes.
I've also tried setting up the second mapping to ignore the properties set above and I couldn't get this working, it was still overwriting the properties.
var autoMapperConfiguration = new MapperConfigurationExpression();
autoMapperConfiguration
.CreateMap<MyRealClass, UserDetails>()
.ForMember(c => c.SharedPropertyOne, d => d.Ignore())
.ForMember(c => c.SharedPropertyTwo, d => d.Ignore());
I think you're close, but your question states:
I need to map this into a list of MyRealClass
... and your attempted mapping maps MyRealClass to UserDetails. It seems like you actually want a map from UserDetails to MyRealClass instead.
Anyway, here's one way to accomplish this:
var autoMapperConfiguration = new MapperConfigurationExpression();
autoMapperConfiguration.CreateMap<UserDetails, MyRealClass>();
autoMapperConfiguration.CreateMap<ShinyDTO, MyRealClass>();
var results = new List<MyRealClass>();
foreach(var record in dto.Users) {
var mapped = Mapper.Map<MyRealClass>(dto);
Mapper.Map(record, mapped);
results.Add(mapped);
}
Here, the second Mapper.Map call maps record onto mapped, and it should not overwrite the values that have already been mapped over by the mapping from ShinyDTO to MyRealClass.
You could also get fancy and do all of this in a ConstructUsing call, but this seems clearer to me.
You can create a map between UserDetails and IEnumerable<MyRealClass>.
var autoMapperConfiguration = new MapperConfigurationExpression();
autoMapperConfiguration
.CreateMap<IEnumerable<MyRealClass>, UserDetails>()
.ForMember(dest => dest.SharedPropertyOne, opt => opt.MapFrom(x => x.get(0).SharedPropertyOne)); //you can check if the list is empty
.ForMember(dest => dest.SharedPropertyTwo, opt => opt.MapFrom(x => x.get(0).SharedPropertyTwo)); //you can check if the list is empty
.AfterMap((src,dest) => //src is a list type
{
foreach(MyRealClass myrealclass in src)
dest.Users.add(new UserDetails(){
Title = myrealclass.Title,
Forename = myrealclass.Forename,
Surname = myrealclass.Surname
});
});
Related
I have an API endpoint something like this:
[HttpGet("{shoppingCartId}")]
public async Task<IActionResult> GetShoppingCart(int shoppingCartId)
{
var shoppingCart = await context.ShoppingCarts.Include(c => c.ShoppingCartItems).SingleOrDefaultAsync(c => c.Id == shoppingCartId);
if (shoppingCart == null)
return NotFound("There is no shoppingCart for specified query.");
return Ok(shoppingCart);
}
It works fine. Returns a ShoppingCart with ShoppingCartItems as expected.
But, I dont want to return shoppingCart but shoppingCartResource.
Something like that:
[HttpGet("{shoppingCartId}")]
public async Task<IActionResult> GetShoppingCart(int shoppingCartId)
{
var shoppingCart = await context.ShoppingCarts.Include(c => c.ShoppingCartItems).SingleOrDefaultAsync(c => c.Id == shoppingCartId);
if (shoppingCart == null)
return NotFound("There is no shoppingCart for specified query.");
var shoppingCartResource = mapper.Map<ShoppingCart, ShoppingCartResource>(shoppingCart);
return Ok(shoppingCartResource);
}
As you can see the ShoppingCart model has a Collection of ShoppingCartItem inside.
public class ShoppingCart
{
public int Id { get; set; }
public DateTime DateCreated { get; set; }
public ICollection<ShoppingCartItem> ShoppingCartItems { get; set; }
public ShoppingCart()
{
ShoppingCartItems = new Collection<ShoppingCartItem>();
}
}
And here is the ShoppingCartResource model
public class ShoppingCartResource
{
public int Id { get; set; }
public DateTime DateCreated { get; set; }
public ICollection<ShoppingCartItemResource> ShoppingCartItemResources { get; set; }
public ShoppingCartResource()
{
ShoppingCartItemResources = new Collection<ShoppingCartItemResource>();
}
}
Mapping Code is:
CreateMap<ShoppingCart, ShoppingCartResource>();
CreateMap<ShoppingCartItem, ShoppingCartItemResource>();
There is no error but I got the only shoppingCartResource with empty ShoppingCartItemResource.
You are lacking single line telling AutoMapper where from it should map ShoppingCartItemResources because corresponding property in source object does not have the same name - ShoppingCartItems, thus it won't work automatically. Just add this:
CreateMap<ShoppingCart, ShoppingCartResource>()
.ForMember(
dst => dst.ShoppingCartItemResources,
opts => opts.MapFrom(src => src.ShoppingCartItems));
CreateMap<ShoppingCartItem, ShoppingCartItemResource>();
By default, AutoMapper follows a naming convention that automatically creates a map between a source property and a target property if their names match, and you don't have to configure anything for this.
But, if you want a map between two properties and their names don't match, then you have to explicitly define a configuration for that map.
Your ShoppingCartResource and ShoppingCart objects both have properties named Id and DateCreated, and so they got mapped automatically.
To get a map between your ICollection properties -
if you want an automatic map, you have to give them a matching name
if you want to keep distinction in naming, you have to explicitly define a configuration for their mapping. Something like -
CreateMap<ShoppingCart, ShoppingCartResource>()
.ForMember(d => d.ShoppingCartItemResources, opt => opt.MapFrom(s => s.ShoppingCartItems));
CreateMap<ShoppingCartItem, ShoppingCartItemResource>();
I have these classes
public class Student
{
public int StudentId { get; set; }
public int Name { get; set; }
public string SubjectIds { get; set; } //Comma delimited string of subject ID
public string Grades { get; set; } //Comma delimited string of subject ID
}
public class StudentListItemDto
{
public int StudentId { get; set; }
public int StudentName { get; set; }
public IEnumerable<SubjectGradeDto> Tags { get; set; }
}
public class SubjectGradeDto
{
public int SubjectId { get; set; }
public string Grade { get; set; }
}
In my mapping profile I have this where I map IEnumerable<Student> to List<StudentListItemDto>:
CreateMap<IEnumerable<Student>, List<StudentListItemDto>>()
.AfterMap((src, dest) =>
{
List<StudentListItemDto> iStudentListItemDto;
iStudentListItemDto= src.Select(p =>
new StudentListItemDto
{
StudentId= p.StudentId,
StudentName= p.Name,
Tags = p.SubjectIds!= null ? p.SubjectIds.Split(",").Select((tag, i) => new SubjectGradeDto { SubjectIds= int.Parse(tag), Grade= p.Grades.Split(",")[i] }) : null
}).ToList();
dest = iStudentListItemDto; // I'm assigning values to destination
});
The reason why I am mapping it this ways is because I need to map Subjects and Grades property of Students class, which is are comma delimited values to IEnumerable<SubjectGradeDto>
Further improvement to the mapping would be much appreciated.
But my problem is when I am assigning
dest = iStudentListItemDto;
in the AfterMap. When I debug it, it has a value, but when the mapping gets executed here
IEnumerable<StudentListItemDto> iStudentListItemDto= new List<StudentListItemDto>();
iStudentListItemDto = _mapper.Map<IEnumerable<Student>, IEnumerable<StudentListItemDto>>(searchResult, iStudentListItemDto);
where search result is IEnumerable<Student>
iStudentListItemDto evaluates to empty List.
The question is why?
In your CreateMap your mapping IEnumerable to List and when effectively mapping you are mapping from IEnumerable to IEnumerable.
List is IEnumerable, but IEnumerable is not a List, basically.
With this mapping you created you only have the direction from IEnumerable to List.
A better approach, as I can see you are only using this way for mapping the tags, is to do something like this
CreateMap<Student, StudentListItemDto>()
.ForMember(dto => dto.StudentName, opt => opt.MapFrom(s => s.Name))
.ForMember(dto => dto.Tags, opt => opt.Ignore())
.AfterMap((student, dto) =>
{
dto.Tags = student.SubjectIds!= null ? student.SubjectIds.Split(",").Select((tag, i) => new SubjectGradeDto { SubjectIds= int.Parse(tag), Grade= student.Grades.Split(",")[i] }) : null;
});
Now if AutoMapper sees that you are mapping a single object or a list of these objects, it will map each object of this list correctly.
This because you are not using the whole list to create this list of Tags.
I have a simple poco that need to be mapped to an object supplied by a third party that uses a complex object hierarchy. I would like to use AutoMapper if possible but I am unsure how to set it up correctly.
I have supplied a simplified example below to show what I am trying to do.
My poco:
public class Person
{
public string FirstName { get; set; }
public string Lastname { get; set; }
public string FullName { get { return Firstname + " " + Lastname; } }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string City { get; set; }
public string Country { get; set; }
public string Postcode { get; set; }
public string Reference { get; set; }
}
Third Party objects
public class People
{
public Person[] Person { get; set; }
}
public class Person
{
public Names Names { get; set; }
public Address Address { get; set; }
public ReasonsForRequest[] Reasons { get; set; }
}
public class Names
{
public string Fullname { get; set; }
}
public class Address
{
public string AddressLine[] { get; set; }
public string City { get; set; }
public string Country { get; set; }
public string Postcode { get; set; }
}
public class ReasonsForRequest
{
public StructuredReasons StructuredReasons { get; set; }
}
public class StructuredReasons
{
public Reference Ref { get; set; }
}
public class Reference
{
public string Ref { get; set; }
}
The issue I am having is Registering the mappings to get the reference mapped. How can I register a mappig to an object that just contains an object when I need to map to the inner object and to a parent object?
[For clarity, I'm going to call your Person POCO PersonDTO, since the 3rd party code also has a class called Person.]
There are a couple of ways of doing this. One, which I've used in the past, involves setting up a mapping from PersonDTO to Names, another from PersonDTO to Address, and another from PersonDTO to Reasons. Finally, you add a mapping from PersonDTO to Person. It looks like this (I've left out Reasons, but you get the idea):
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<PersonDTO, Names>()
.ForMember(d => d.Fullname, o => o.MapFrom(s => s.FullName));
cfg.CreateMap<PersonDTO, Address>()
.ForMember(d => d.AddressLine,
o => o.MapFrom(s => new[] { s.AddressLine1, s.AddressLine2 }));
cfg.CreateMap<PersonDTO, Person>()
.ForMember(d => d.Names, o => o.MapFrom(s => s))
.ForMember(d => d.Address, o => o.MapFrom(s => s));
});
var mapper = config.CreateMapper();
var myPerson = new PersonDTO() {
FirstName = "Bob",
LastName = "Gold",
AddressLine1 = "123 Main Street",
AddressLine2 = "Apt. 2"
};
var theirPerson = mapper.Map<Person>(myPerson);
But a recent version of AutoMapper added a ForPath() method which makes all of this simpler by letting you access inner objects. Now the code collapses to this:
var config = new MapperConfiguration(cfg =>
cfg.CreateMap<PersonDTO, Person>()
.ForPath(d => d.Names.Fullname, o => o.MapFrom(s => s.FullName))
.ForPath(d => d.Address.AddressLine,
o => o.MapFrom(s => new[] { s.AddressLine1, s.AddressLine2 }))
);
var mapper = config.CreateMapper();
Edit: I left out one aspect which may change the balance between these two methods. In the first method, with multiple maps defined, you get for free any fields with matching names. For example, you don't need to explicitly map PesronDTO.City to Address.City (and if you change the case on PersonDTO.FullName to be PersonDTO.Fullname, you'd get that for free as well). But in the second method, you have to explicitly map all nested fields, even if the names match. In your case, I think the first method would probably work better for you, because you'd have to map only 3 fields explicitly and would get the other 4 for free. For the second method, you'd have to do a ForPath() for all 7 fields.
By default it tries to match the properties of the SourceType to those of the DestinationType. But for your scenario you'll have add some specifications using the ForMember extension method, like this:
Mapper.CreateMap<Foo, FooDTO>()
.ForMember(e => e.Bars, o => o.ExplicitExpansion());
There's an existing thread on this one here:
AutoMapper define mapping level
I'm having a lot of trouble with creating my business entities from my data entities.
Github
My Data.Entities.User looks as follows:
public class User
{
public User()
{
Messages = new List<Message>();
Followers = new List<User>();
Favorites = new List<Message>();
Notifications = new List<Notification>();
SubscribedTopics = new List<Topic>();
}
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Tag { get; set; }
public string Picture { get; set; }
public ICollection<Message> Messages { get; set; }
public ICollection<User> Followers { get; set; }
public ICollection<Message> Favorites { get; set; }
public ICollection<Notification> Notifications { get; set; }
public ICollection<Topic> SubscribedTopics { get; set; }
}
My Data.Mappers.UserMapper looks like this:
class UserMapper : EntityTypeConfiguration<User>
{
public UserMapper()
{
// Table Mapping
ToTable("Users");
// Primary Key
HasKey(u => u.Id);
Property(u => u.Id)
.IsRequired();
// Properties
Property(u => u.Name)
.IsRequired()
.HasMaxLength(140);
Property(u => u.Email)
.IsRequired()
.HasMaxLength(255)
.IsUnicode(false);
Property(u => u.Tag)
.IsRequired()
.IsUnicode(false)
.HasMaxLength(255)
.HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute()));
Property(u => u.Picture)
.IsOptional();
// Relationships
HasMany(u => u.Followers)
.WithMany()
.Map(u => u.MapLeftKey("FollowerID"));
HasMany(u => u.Favorites)
.WithMany()
.Map(u => u.MapLeftKey("MessageID"));
HasMany(u => u.SubscribedTopics)
.WithMany(t => t.Subscribers)
.Map(u =>
{
u.ToTable("TopicSubscribers");
u.MapLeftKey("UserID");
u.MapRightKey("TopicID");
});
}
}
Finally, my Domain.Entities.User like this:
public class User : EntityBase<string>
{
public string Name { get; set; }
public string Email { get; set; }
public string Tag { get; set; }
public string Picture { get; set; }
public IEnumerable<Message> Messages { get; set; }
public IEnumerable<User> Followers { get; set; }
public IEnumerable<Message> Favorites { get; set; }
public IEnumerable<Notification> Notifications { get; set; }
public IEnumerable<Topic> SubscribedTopics { get; set; }
protected override void Validate()
{
if (string.IsNullOrWhiteSpace(Name))
{
AddBrokenRule(new ValidationRule("Name", "Name_Missing"));
}
if (string.IsNullOrWhiteSpace(Email))
{
AddBrokenRule(new ValidationRule("Email", "Email_Missing"));
}
if (string.IsNullOrWhiteSpace(Tag))
{
AddBrokenRule(new ValidationRule("Tag", "Tag_Missing"));
}
System.Uri uriResult;
if (!string.IsNullOrWhiteSpace(Picture) &&
Uri.TryCreate(Picture, UriKind.Absolute, out uriResult) &&
(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps))
{
AddBrokenRule(new ValidationRule("Picture", "Picture_InvalidURI"));
}
}
}
EntityBase adds the Id parameter, so as far as parameters are concerned, these two classes should be identical.
The part where I run into trouble is mapping the Data Entity to the Domain Entity.
public override IEnumerable<User> GetAll()
{
IEnumerable<User> user = _context.Users.Project()
.To<User>("Followers");
return user;
}
I think what's causing trouble is the circular navigational properties. User1 might have a follower named User2, while at the same time following User2.
So far I have tried both AutoMapper and ValueInjecter, but I have not had any success with either.
I tried adding "Virtual" to all navigational properties, enabling lazy and proxy loading, but this causes both AutoMapper and ValueInjecter to fail. ValueInjecter due to a already opened datareader and AutoMapper because of a type mismatch.
I tried explicitly loading navigational properties, but as soon as I Include("Followers") on User, I get a stackoverflow.
Trying to create a AutoMapperConfiguration where I specify a maxDepth of 1 yields a stackoverflow unless I add opt.ExplicitExpansion to every navigational property.
If i then try to explicitly expand a navigational property, I get
The type 'ShortStuff.Domain.Entities.User' appears in two structurally
incompatible initializations within a single LINQ to Entities query. A
type can be initialized in two places in the same query, but only if
the same properties are set in both places and those properties are
set in the same order.
Ideally I would want a solution that lets me explicitly control which navigational properties to expand without recursing.
For example, I'd like to do something like:
_context.Users.Include("Followers").NoNavigation().AsEnumerable();
And then I would be able to access User.Followers and have a list of other users, with their navigational properties set to null.
Many thanks!
Full source code of my Repository / Service learning project can be found on Github at https://github.com/Bio2hazard/ShortStuff/tree/master/ShortStuffApi
Edit:
I made some progress.
I got things to work by turning off proxy generation & lazy loading, and then using ValueInjector like so:
IEnumerable<Data.Entities.User> userList = _context.Users.Include("Followers").Include("Favorites").Include("Messages").Include("Notifications").Include("SubscribedTopics");
IEnumerable<User> users = userList.Select(u => new User
{
Id = u.Id,
Email = u.Email,
Picture = u.Picture,
Tag = u.Tag,
Name = u.Name,
Followers = u.Followers.Select(uu => new User().InjectFrom<SmartConventionInjection>(uu)).Cast<User>(),
Favorites = u.Favorites.Select(uf => new Message().InjectFrom<SmartConventionInjection>(uf)).Cast<Message>(),
Messages = u.Messages.Select(um => new Message().InjectFrom<SmartConventionInjection>(um)).Cast<Message>(),
Notifications = u.Notifications.Select(un => new Notification().InjectFrom<SmartConventionInjection>(un)).Cast<Notification>(),
SubscribedTopics = u.SubscribedTopics.Select(ut => new Topic().InjectFrom<SmartConventionInjection>(ut)).Cast<Topic>()
});
But that's a ton of code. I could probably create a factory for this, but there has got to be a easier way, right?
with ValueInjecter you can use the SmartConventionInjection which will only access the properties if it needs to get the value:
http://valueinjecter.codeplex.com/wikipage?title=SmartConventionInjection&referringTitle=Home
other injections usually get the value too so that you could use it in the matching algorithm
for an example of using valueinjecter with Entity Framework (code first, latest)
have a look at this project: http://prodinner.codeplex.com
I am a newbie to the Automapper framework. I have a domain class and a DTO class as follows:
public class Employee
{
public long Id {get;set;}
public string Name {get;set;}
public string Phone {get;set;}
public string Fax {get;set;}
public DateTime DateOfBirth {get;set;}
}
public class EmployeeDto
{
public long Id {get;set;}
public string FullName {get;set;}
public DateTime DateOfBirth {get;set;}
}
Note: The name of property "Name" of Employee class is not the same as that of property "FullName" of EmployeeDto class.
And here's the code to map the Employee object to EmployeeDto:
Mapper.CreateMap<Employee, EmployeeDto>(); // code line (***)
EmployeeDto dto = Mapper.Map<Employee, EmployeeDto>(employee);
My question is: If I want to map Employee (source class) to EmployeeDto (destination class), how can I specify the mapping rule? In other words, how should I do more with code line (***) above?
Never mind, I myself found a solution:
Mapper.CreateMap<Employee, EmployeeDto>()
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.Name));
Just to roll the comments above into an updated approach using Automapper 8.1+...
var mapConfig = new MapperConfiguration(
cfg => cfg.CreateMap<Employee, EmployeeDto>()
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.Name))
);
Then you would build the mapper using the mapConfig:
var mapper = mapConfig.CreateMapper();
We can also specify on Class attributes for mapping
From https://docs.automapper.org/en/stable/Conventions.html#attribute-support
Attribute Support
AddMemberConfiguration().AddName<SourceToDestinationNameMapperAttributesMember>();
* Currently is always on
Looks for instances of SourceToDestinationMapperAttribute for
Properties/Fields and calls user defined isMatch function to find
member matches.
MapToAttribute is one of them which will match the property based on
name provided.
public class Foo
{
[MapTo("SourceOfBar")]
public int Bar { get; set; }
}
Considering that we have two classes
public class LookupDetailsBO
{
public int ID { get; set; }
public string Description { get; set; }
}
and the other class is
public class MaterialBO
{
[MapTo(nameof(LookupDetailsBO.ID))]
public int MaterialId { get; set; }
[MapTo(nameof(LookupDetailsBO.Description))]
public string MaterialName { get; set; }
public int LanguageId { get; set; }
}
In this way you know typically to which property you follow .
and you make sure of the naming convention , so if you have changed the propery name in the source . The MapTo() will prompt an error
The new style of Attribute Mapping via Data Annotations:
https://docs.automapper.org/en/v8.1.0/Attribute-mapping.html?highlight=annotation
[AutoMap(typeof(Order))]
public class OrderDto {
// This is equivalent to a CreateMap<Order, OrderDto>()
For mapping the member
[SourceMember(nameof(Order.OrderTotal))]
public decimal Total { get; set; }
If you want reverse map then you add that property in
[AutoMap(typeof(Order), ReverseMap = true )]
public class OrderDto {
// This is equivalent to a CreateMap<Order, OrderDto>().ReverseMap()
The above answers are great and hope OP has got his answer. I just want to add how we can map fixed values instead of fields using UseValue() method of IMemberConfigurationExpression Interface.
Mapper.CreateMap<Employee, EmployeeDto>()
.ForMember(dest => dest.Department, opt => opt.UseValue("Development"));
This will map "Development" as a Department property value for destination data.