How to assign value to destination property on aftermap in automapper - c#

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.

Related

How to map a simple poco into a complex object hierachy using automapper?

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

AutoMapper from list in DTO to individual object

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

Automapper projection and union

I have a problem with union and automapper projections.
I have two entities:
public class Entity
{
public DateTime ActionDate { get; set; }
public int SomeProp { get; set; }
}
public class ExtendedEntity
{
public DateTime ActionDate { get; set; }
public int SomeProp { get; set; }
public int SomeOtherProp { get; set; }
}
and projection:
public class EntityProjection
{
public DateTime ActionDate { get; set; }
public int SomeProp { get; set; }
public int SomeOtherProp { get; set; }
public string Source { get; set; }
}
i map entities to one projection, Entity does not have SomeOtherProp so i set 0 to it:
public class EntityProfile : Profile
{
protected override void Configure()
{
CreateMap<ExtendedEntity, EntityProjection>()
.ForMember(dest => dest.Source, opt => opt.MapFrom(src => "ext entity"));
CreateMap<Entity, EntityProjection>()
.ForMember(dest => dest.SomeOtherProp, opt => opt.MapFrom(src => 0))
.ForMember(dest => dest.Source, opt => opt.MapFrom(src => "entity"));
}
}
when i try to use next code i get error:
var entities = context.Set<Entity>()
.Project().To<EntityProjection>();
var extEntities = context.Set<ExtendedEntity>()
.Project().To<EntityProjection>();
var result = entities.Union(extEntities).OrderBy(p => p.ActionDate).ToList();
Error text: The type 'UserQuery+EntityProjection' appears in two structurally incompatible initializations within a single LINQ to Entities query. A type can be...
That means that properties in projection must be initialized in same order, how i can set projection properties initialization order by automapper?
Very late answer, and the short version seems to be "You can't".
I had exactly the same question (Can I force Automapper to initialise properties in a certain order?) and ended up mapping everything within a LINQ select statement.
For ease, I made it a static method within my DTO (cut-down code):
public static IQueryable<MyDto> QueryableFromTaskType1(
IQueryable<TaskType1> query)
{
return query.Select(src => new MyDto()
{
TaskId = src.Id,
AssetTypeName = src.Asset.AssetType.Name,
AssetId = src.Asset.Id,
AssetCode = src.Asset.Code,
AssetName = src.Asset.Name,
});
}
public static IQueryable<MyDto> QueryableFromTaskType2(
IQueryable<TaskType2> query)
{
return query.Select(src => new MyDto()
{
TaskId = src.Id,
AssetTypeName = src.AssetTypeName,
AssetId = src.AssetId,
AssetCode = src.AssetCode,
AssetName = src.AssetName,
});
}
then you can get your objects, as an IQueryable, simply pass them through the appropriate static method (which appends a select into the DTO - or projects as it's otherwise known) and then Union or Concat the resulting IQueryables.
The only downside is that Automapper will normally deal with recursive automapping, although I'm pretty certain that wouldn't map to SQL well anyway, so you probably don't lose much.

Convert IGrouping from one class to another using linq

I have two classes Teams and PlayerTeams
public class PlayerTeams
{
public int ID { get; set; }
public string PlayerName { get; set; }
public string PlayerCountry { get; set; }
public bool IsActive { get; set; }
public string PlayerTeam { get; set; }
}
public class Players
{
public int ID { get; set; }
public string Name { get; set; }
public string Country { get; set; }
public bool? Status { get; set; }
}
I have a list of PlayerTeams which is grouped by PlayerTeam like this.
var groupedPlayers = teams
.OrderBy(x => x.PlayerName)
.GroupBy( x => x.PlayerTeam)
.ToList();
Its of type List<IGrouping<string, PlayerTeams>> but I want it to be of type List<IGrouping<string, Players>> as I do not want the redundant key information on every row.
How could I possibly achieve that? I could only think of something like .ConvertAll() on the IGrouping. I am not able to make it also.
Is there an efiicient way to do this?
If you can change the grouping, I'd just use:
var groupedPlayers = teams
.OrderBy(x => x.PlayerName)
.GroupBy(x => x.PlayerTeam, Players.FromPlayerTeam)
.ToList();
Where Players.FromPlayerTeam is a static method in Players which takes a PlayerTeam and returns a Players.
Additionally, I'd suggest using ToLookup instead of GroupBy - a Lookup is exactly what you want here, without bothering with the ToList call.
This not testet, just an idea.
If you have trouble converting your linq statement, which is expecting the IGrouping type, to a string list, then you might have to select it before.
var groupedPlayers = new List<string>();
groupedPlayers = teams
.OrderBy(x => x.PlayerName)
.GroupBy(x => x.PlayerTeam, Players.FromPlayerTeam)
.Select(x => x.Key) // << added select
.ToList();

How to select and consume a collection of value objects in an NHibernate QueryOver query

I have a simple model, consisting of a document that references one or more article using a reference object (this is because in the domain, we do not own the articles so we can only reference them).
I'm trying to write a query that lists the documents, printing the ID and a string consisting of a comma separated list of article numbers. For example:
ID ARTICLES
------------------
1 ACC, PE2,
2 ER0, AQ3, FEE
3 PE2
My problem is with selecting the comma separated list.
Here are the domain classes:
// The Entity class has an Id property.
public class Document : Entity
{
public virtual IEnumerable<ArticleReference> ArticleReferences { get; set; }
public virtual DateTime ReceiveDate { get; set; }
}
// The ValueObject does not have an Id property ofcourse.
public class ArticleReference : ValueObject
{
public virtual string ArticleNumber { get; set; }
public virtual string ArticleName { get; set; }
}
The article reference is a value object so it does not have an ID of its own.
This is the view model that represents an item in the result list:
public class DocumentListItemModel
{
public int Id { get; set; }
public string ArticleNumbers { get; set; }
public string ReceiveDate { get; set; }
}
And here's the query class I have come up with so far:
public class DocumentQuery
{
public IList<DocumentListItemModel> ExecuteQuery()
{
IntermediateModel model = null;
ArticleReference articleReferenceAlias = null;
return Session
.QueryOver<Document>()
.JoinAlias(n => n.ArticleReferences, () => articleReferenceAlias);
.SelectSubQuery(
QueryOver.Of<ArticleReference>(() => articleReferenceAlias)
// There is no way of matching references to documents from a domain
// point of view since the references are value objects and
// therefore don't have an ID.
.Where(n => ...)
.Select(q => articleReferenceAlias.Number))
.WithAlias(() => model.ArticleNumbers)
.TransformUsing(Transformers.AliasToBean<IntermediateModel>());
.Future<IntermediateModel>()
.ToList()
.Select(n =>
new DocumentListItemModel()
{
Id = n.Id,
ArticleNumbers = string.Join(", ", n.ArticleNumbers.OrderBy(p => p)),
ReceiveDate = n.ReceiveDate.ToString("d", CultureInfo.CurrentCulture)
})
.ToList();
}
private class IntermediateModel
{
public int Id { get; set; }
public IEnumerable<string> ArticleNumbers { get; set; }
public DateTime ReceiveDate { get; set; }
}
}
As you can see, I can't express the .Where statement because there is no way of matching references to documents from a domain point of view. The references are value objects and therefore don't have an ID.
The question is: how do I fix the query to properly select the list of article numbers so I can use it in my string.Join statement to make the comma separated string?
I think you are taking the definition of value object too literally. Assigning a surrogate identifier (identity column, Guid, etc.) to a value object does not make it any less of a value object. It's a value object because its equality is based its values, not its identity. This does not require that a value object cannot have an identity, and in practice it almost always has to.
Your application obviously has to be able to link a Document to a set of ArticleReferences and the best way to accomplish that is by adding an ID to ArticleReference.
I managed to solve the problem. This is what I ended up with:
public IList<DocumentListItemModel> ExecuteQuery()
{
ArticleReference articleReferenceAlias = null;
return Session
.QueryOver<Document>()
.JoinAlias(n => n.ArticleReferences, () => articleReferenceAlias,
JoinType.LeftOuterJoin)
.SelectList(list => list
.Select(n => n.Id)
.Select(n => articleReferenceAlias.Number))
.List<object[]>()
.Select(x => new
{
Id = (int)x[0],
ArticleNumber = (string)x[1]
})
.GroupBy(n => n.Id).Select(n =>
{
return new DocumentListItemModel
{
Id = n.First().Id,
ArticleNumbers = string.Join(", ", n.Select(p => p.ArticleNumber))
};
}).ToList();
}
}
I couldn't use the alias-to-bean transformer anymore because it cannot handle collection properties. That's why the solution has the GroupBy, it consolidates the rows, aggregating the article numbers into a string.

Categories

Resources