AutoMapper ProjectTo: Not working with nested objects - c#

I have the following dto:
public class SingleForm
{
// other props left out for brevity
public List<Filter> Filters { get; set; }
}
I then try mapping it with AutoMapper like so:
CreateMap<Form, SingleForm>()
.ForMember(dest => dest.Filters,
opts => opts.MapFrom(src =>
Mapper.Map<List<Filter>>(src.Questions)));
CreateMap<FormQuestion, Filter>()
.ForMember(dest => dest.Header,
opts => opts.MapFrom(src => src.Question.QuestionText));
I then use ProjectTo:
var query = this.context.Forms
.Where(e => e.Id == message.FormId)
.ProjectTo<SingleForm>()
.FirstOrDefault();
However, my filters collection is empty when I execute the query.
When I try to manually map the collection using LINQ, like below, it works correctly, so I'm wondering if I am doing something wrong?
var query = this.context.Forms
.Where(e => e.Id == message.FormId)
.Select(e => new SingleForm
{
Id = e.Id,
Filters = e.Questions.Select(q =>
new Filter {
Header = q.Question.QuestionText
}).ToList()
})
.FirstOrDefault();

In general, I think it is best to avoid calling Mapper.Map() within your profile configuration. With this in mind, I think changing your first mapping to the following may help:
CreateMap<Form, SingleForm>()
.ForMember(dest => dest.Filters,
opts => opts.MapFrom(src => src.Questions));

If the mapping happens out side DbContext then should using includes method to retrieve all relationships items which nit able to lazy load without DbContext.

Related

AutoMapper does not update Collection items

I try to use AutoMapper to map models to dtos. The first try uses EF-Core but I was able to eliminate EF-Core and reproduce that without it.
I reproduced the behaviour in this DEMO.
(Old DEMO using EF-Core is here.)
TL;DR
It seams that this will not work:
var container = new Container("Container-Id 000", new List<Item> { new Item("Item-Id 000") { Name = "Item-Name" } });
var containerModel = mapper.Map<ContainerModel>(container);
// apply changes
container.Items[0].Name += " -- changed";
// update model
mapper.Map(container, containerModel);
// at this point the item does not contain the correct name:
container.Items[0].Name != containerModel.Items[0].Name !!!!!
Long explanation:
The Dto's and models have the following structure:
Container
+ Id: string { get; }
+ Items: IReadOnlyList<Item> { get; }
Item
+ Id: string { get; }
+ Name: string { get; set; }
ContainerModel
+ Id: string { get; set; }
+ Items: List<ItemModel> { get; set; }
ItemModel
+ Id: string { get; set; }
+ Name: string { get; set; }
The AutoMapper-Configuration is (maybe that's the point where I'm missing something):
var config = new MapperConfiguration(
cfg =>
{
cfg.CreateMap<Container, ContainerModel>(MemberList.None)
.EqualityComparison((src, dst) => src.Id == dst.Id)
.ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(dst => dst.Items, opt => opt.MapFrom(src => src.Items));
cfg.CreateMap<ContainerModel, Container>(MemberList.None)
.EqualityComparison((src, dst) => src.Id == dst.Id)
.ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(dst => dst.Items, opt => opt.MapFrom(src => src.Items));
cfg.CreateMap<IReadOnlyList<Item>, List<ItemModel>>(MemberList.None)
.ConstructUsing((src, ctx) => src.Select(ctx.Mapper.Map<ItemModel>).ToList());
cfg.CreateMap<List<ItemModel>, IReadOnlyList<Item>>(MemberList.None)
.ConstructUsing((src, ctx) => src.Select(ctx.Mapper.Map<Item>).ToList().AsReadOnly());
cfg.CreateMap<Item, ItemModel>(MemberList.None)
.EqualityComparison((src, dst) => src.Id == dst.Id)
.ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name));
cfg.CreateMap<ItemModel, Item>(MemberList.None)
.EqualityComparison((src, dst) => src.Id == dst.Id)
.ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name));
});
var result = config.CreateMapper();
result.ConfigurationProvider.AssertConfigurationIsValid();
return result;
I created a dto-instance and mapped them successfully to the model. (I also tested the way back from model to dto that also works but is not needed to reproduce the problem.)
var mapper = CreateMapper();
var container = new Container("Container-Id 000", new List<Item> { new Item("Item-Id 000") { Name = "Item-Name" } });
var containerModel = mapper.Map<ContainerModel>(container);
// apply changes
container.Items[0].Name += " -- changed";
// update model
mapper.Map(container, containerModel);
Console.WriteLine($"Src.Name: {container.Items[0].Name}");
Console.WriteLine($"Dst.Name: {containerModel.Items[0].Name}");
if (container.Items[0].Name != containerModel.Items[0].Name)
{
throw new InvalidOperationException("The names of dto and model doesn't match!");
}
The output printed before the exception has thrown shows the problem:
Src.Name: Item-Name -- changed
Dst.Name: Item-Name
The specified exception is thrown - but shouldn't (in my opinion).
I think the problem is mapper.Map(readContainer, readContainerModel);.
I specified an equality comparision to help AutoMapper to find the correct instances but without luck.
What am I missing here? What do I have to do to fix that issue?
All that persistence code is wrapped into a small framework and should be transparent to my colleques. All they have to do is specifying the dtos, models ans mapping profile. The framework does not know about "navigations". Yes I'm able to create code that analyses all the navigations of the model-types and try to find an equivalent dto and foreach all the properties and updates all instances manuelly. But that seams to treat much pain and errors what's the reason I tried the automated mapping.
Why do I need mapper.Map(src, dst)?
All that works together with EF-Core and a small persistence framework for my colleques. I tried using Persist() and InsertOrUpdate (that the preferred method) but I found that issue report for AutoMapper.Collection. The InsertOrUpdate-method is broken. The specified workarround is what I was trying to use until the issue is fixed - but it doesn't solve the problem.
I also found that article WHY MAPPING DTOS TO ENTITIES USING AUTOMAPPER AND ENTITYFRAMEWORK IS HORRIBLE containing the same trick. I don't care about the created model instances that AutoMapper will produce for every collection item. I'm also not easyly able to forward the DbContext to the mapping functions.
I found the problem. I added this to map the collections to the AutoMapper configuration:
cfg.CreateMap<IReadOnlyList<Item>, List<ItemModel>>(MemberList.None)
.ConstructUsing((src, ctx) => src.Select(ctx.Mapper.Map<ItemModel>).ToList());
cfg.CreateMap<List<ItemModel>, IReadOnlyList<Item>>(MemberList.None)
.ConstructUsing((src, ctx) => src.Select(ctx.Mapper.Map<Item>).ToList().AsReadOnly());
It seams that this will prevent AutoMapper from working correctly.
The solution is to remove both printed lines and add the following instead:
cfg.AddCollectionMappers();
I added the explicit collection mappings because I underestimated the power of AddCollectionMappers because I'm using immutable objects and interfaces to IReadOnlyList<> and IReadOnlyDictionary<,> and I was wrongly of the opinion that AutoMapper was not able to handle that. My fault.
See the working DEMO.

How not to explicitly specify members for which the name automatically match up?

I am using AutoMapper 9.0 and in the example below, I am mapping a Person to a People Object.
Because 1 member differs from Person to People (Person has Sfx while People has Suffix), I have to specifically map the rest of the properties that would otherwise automatically match up.
Is there a way to not specify them but for them to still be mapped ?
configurationExpression.CreateMap<JsonRequest, XmlRequest>()
.ForMember(
dest => dest.People,
opt => opt.MapFrom(src => new People
{
FirstName = src.Person.FirstName,
MiddleName = src.Person.MiddleName,
LastName = src.Person.LastName,
Suffix = src.Person.Sfx
}));
Checking the documentation, you should be able to achieve this by defining separate mappings for your OutterClass and InnerClass:
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<OuterSource, OuterDest>();
cfg.CreateMap<InnerSource, InnerDest>();
});
Have you tried something like this?
configurationExpression.CreateMap<Person, People>()
.ForMember(dest => dest.Suffix, opt => opt.MapFrom(src => src.sfx))
.ReverseMap();
configurationExpression.CreateMap<JsonRequest, XmlRequest>()
.ForMember(dest => dest.People, opt => opt.MapFrom(src => src.Person))
.ReverseMap();

Calling an object constructor from NHibernate QueryOver SelectList

I have a DTo class UserDTO with the following constructor public UserDTO(User user). I have also created an NHibernate query which retrieves a IList<TodoDTO> where each TodoDTO has the following property public IList<UserDTO> ResponsibleUsers { get; set; }.
I am wondering if it would be possible to this constructor on UserDTO in my query, so something like this:
var responsibleUsers = session.QueryOver<UserTodo>()
.JoinAlias(ut => ut.User, () => userAlias)
.Where(ut => ut.Todo.Id.IsIn(_todos.Select(t => t.Id).ToArray()))
.Select(u => new UserDTO(userAlias)).ToList<UserDTO>();
The constructor looks like this:
public UserDTO(User user) {}
The problem is that when I run this code the parameter in the UserDTO constructor the user is null.
Calling a Constructor, (or any other code) in the syntax of a query won't work. the entities here are only used to resolve the table and columns for the sql query. The ability to call methods of these objects is missleading...
You can use a Projection to select data from one, or multiple db-entities to a new object (in your case: to the UserDTO)
UserTodo userTodo = null
UserDTO result = null;
var responsibleUsers = session.QueryOver<UserTodo>(() => userTodo)
.JoinAlias(ut => ut.User, () => userAlias)
.Where(ut => ut.Todo.Id.IsIn(_todos.Select(t => t.Id).ToArray()))
.SelectList(list => list
.Select(() => userAlias.FirstName).WithAlias(() => result.FirstName)
.Select(() => userAlias.UserId).WithAlias(() => result.IdOfUser)
.Select(() => userTodo.Age).WithAlias(() => result.Age) // if you require any values from UserTodo
)
.TransformUsing(Transformers.AliasToBean<UserDTO >())
.List<UserDTO >();

Automapper missing type when mapping database model from entity to viewmodel

I am a newbie in using automapper and I want to implement it in my project. I am trying to map multiple model from EF to single viewmodel in asp project but before doing that I have encountered a problem as below.
I tried to follow solution provided as:
Automapper missing type map configuration or unsupported mapping
Automapper missing type map configuration or unsupported mapping?
but without any success.
I am using recent automapper.
I tried variation of method to create map such as
config.CreateMap<tblMeeting, MeetingViewModels>()
.ForMember(dest => dest.meetingDetails, input => input.MapFrom(i => new tblMeeting
{
meetingId = i.meetingId,
meetingType = i.meetingType??null,
startTime = i.startTime,
finishTime = i.finishTime,
meetingDate = i.meetingDate,
meetingNotes = i.meetingNotes,
meetingVenue = i.meetingVenue
}));
and this
config.CreateMap<tblMeeting, MeetingViewModels>()
.ForMember(dest => dest.meetingDetails.meetingId, opt => opt.MapFrom(s => s.meetingId))
.ForMember(dest => dest.meetingDetails.startTime,
opt => opt.MapFrom((s => s.startTime)))
.ForMember(dest => dest.meetingDetails.finishTime,
opt => opt.MapFrom(s => s.finishTime))
.ForMember(dest => dest.meetingDetails.meetingType,
opt => opt.MapFrom(s => s.meetingType ?? null))
.ForMember(dest => dest.meetingDetails.meetingDate,
opt => opt.MapFrom(s => s.meetingDate))
.ForMember(dest => dest.meetingDetails.meetingVenue,
opt => opt.MapFrom(s => s.meetingVenue))
.ForMember(dest => dest.meetingDetails.meetingNotes,
opt => opt.MapFrom(s => s.meetingNotes));
});
this also
config.CreateMap<tblMeeting, MeetingViewModels>().ConvertUsing<test();
public class test : ITypeConverter<tblMeeting, MeetingViewModels>
{
public MeetingViewModels Convert(tblMeeting source, MeetingViewModels destination, ResolutionContext context)
{
MeetingViewModels m = new MeetingViewModels();
m.meetingDetails.meetingId = Guid.Parse(source.meetingType.ToString());
m.meetingDetails.meetingNotes = source.meetingNotes;
m.meetingDetails.meetingType = Guid.Parse(source.meetingType.ToString());
m.meetingDetails.meetingDate = source.meetingDate;
m.meetingDetails.startTime = source.startTime;
m.meetingDetails.finishTime = source.finishTime;
m.meetingDetails.meetingVenue = source.meetingVenue;
return m;
}
}
but non could solve the problem.
if anyone could help me out it would be of great help.
Thank you.
Here is how I personally implement AutoMapper in my projects:
First create a MappingConfig class, generally I put it in App_Code folder.
In my projects I probably have different sections in the system, by section I mean different Areas or somehow the application needs to be logically separated in different parts like User Management, Meetings etc whatever you have there...
So from the moment that I can divide the system in logical sections I create a profile class for each section:
Here is an example of profile class:
public class GeneralMappingConfigProfile : Profile
{
public GeneralMappingConfigProfile()
{
CreateMap<sourceObject, destinationObject>()
.ForMember(d => d.X, o => o.MapFrom(s => s.Y))
}
}
The class above is an example for general mappings but you may have there a Meetings profile class if it is big enough to be distinguished as a section.
Then in my config class I configure all profile classes as below:
public class MappingConfig
{
public static void RegisterMappings()
{
Mapper.Initialize(config =>
{
config.AddProfile<GeneralMappingConfigProfile>();
config.AddProfile<MeetingsMappingConfigProfile>();
//etc
});
}
}
In the global.asax I call the static method like below:
MappingConfig.RegisterMappings();
Then I can create mappings as many as I see fit in each profile:
I just wrote all this code so you can organize the code better...
For your situation might be a lot of things that might cause this error but refer to this question here. Can you please share more code here because would like to see the MeetingViewModels model and the action code because there must be something wrong at the way how you get the tblMeeting object from database.

Can Automapper use an entity's navigation properties to pull values from related entities

Is Automapper able to drill down into an entityy navigation properties to map to a DTO class? Below is what I am doing to map results from an Entity Framework query to a DTO:
public List<ProductRequestDetailDto> GetProductRequestExtendedDetailAll()
{
List<ProductRequest> aProductRequestList = unitOfWork.getProductRequestRepository().GetProductRequestExtendedDetailAll();
List<ProductRequestDetailDto> ProductRequestDetailDtoList = new List<ProductRequestDetailDto>();
foreach (ProductRequest Req in aProductRequestList)
{
ProductRequestDetailDto ProdReqDetDto = new ProductRequestDetailDto();
ProdReqDetDto.ProductRequestId = Req.ProductRequestId;
ProdReqDetDto.FirstName = Req.Employee.FirstName;
ProdReqDetDto.MiddleInitial = Req.Employee.MiddleInitial;
ProdReqDetDto.LastName = Req.Employee.LastName;
ProdReqDetDto.DeptName = Req.Employee.Department.DeptName;
ProdReqDetDto.DeviceType = Req.ProductProfile.DeviceType;
ProdReqDetDto.ProductName = Req.ProductProfile.ProductName;
ProdReqDetDto.ProductId = Req.ProductProfile.ProductId;
ProdReqDetDto.ProductRequestStageId = Req.ProductRequestStage.ProductRequestStageId;
ProdReqDetDto.DateRequested = Req.DateRequested;
ProdReqDetDto.DateCompleted = Req.DateCompleted;
ProdReqDetDto.SerialNumber = Req.SerialNumber;
ProdReqDetDto.PhoneNumber = Req.PhoneNumber;
ProductRequestDetailDtoList.Add(ProdReqDetDto);
}
return ProductRequestDetailDtoList;
public List<ProductRequest> GetProductRequestExtendedDetailAll()
{
var ReportResult = from Req in context.ProductRequests
select Req;
return ReportResult.ToList();
}
I would like to avoid doing the above if Automapper can do it for me. Automapper has been able to map results to my DTOs when I don't need to drill down to the navigation properties of an entity which leads to other entities. I tried the following but it did not work probably because I need information that requires navigating to other entities such as Employee, Department, and ProductProfile:
List<ProductRequestDetailDto> ProductRequestDetailDtoList = Mapper.Map<List<ProductRequestDetailDto>>(aProductRequestList);
If this can be done what is the correct way to do it?
Have you look in to the extension Queryable Extensions for auto mapper it has an extension .ProjectTo that might help you with what you trying to achieve otherwise you will need to create a mapping configuration for the given case.
with something like
AutoMapper.Mapper.CreateMap<ProductRequestDetailDto, Req>()
.ForMember(dest => dest.FirstName ,
opts => opts.MapFrom(src => src.Employee.FirstName));
No, it cannot drill down into the properties because it cannot know how deep it should get or what to do with ambiguities.
By default it only maps automatically the properties with the same name, so this would already save you some code there but you would still need to teach it how to map the other properties. It can also map a list to another if the types they hold have a mapping to each other (which would remove the need for a foreach loop).
Note that if the properties with the same name don't have the same type you will also need to add a mapping for them (if they are not castable to each other).
public List<ProductRequestDetailDto> GetProductRequestExtendedDetailAll()
{
AutoMapper.Mapper.CreateMap<ProductRequest, ProductRequestDetailDto>()
.ForMember(dest => dest.FirstName, opt => opt.MapFrom(src => src.Employee.FirstName))
.ForMember(dest => dest.MiddleInitial, opt => opt.MapFrom(src => src.Employee.MiddleInitial))
.ForMember(dest => dest.LastName, opt => opt.MapFrom(src => src.Employee.LastName))
.ForMember(dest => dest.DeptName, opt => opt.MapFrom(src => src.Employee.Department.DeptName))
.ForMember(dest => dest.DeviceType, opt => opt.MapFrom(src => src.ProductProfile.DeviceType))
.ForMember(dest => dest.ProductName, opt => opt.MapFrom(src => src.ProductProfile.ProductName))
.ForMember(dest => dest.ProductId, opt => opt.MapFrom(src => src.ProductProfile.ProductId))
.ForMember(dest => dest.ProductRequestStageId, opt => opt.MapFrom(src => src.ProductRequestStage.ProductRequestStageId));
IQueryable<ProductRequest> aProductRequestList = unitOfWork.getProductRequestRepository().GetProductRequestExtendedDetailAll();
List<ProductRequestDetailDto> ProductRequestDetailDtoList = aProductRequestList.ProjectTo<ProductRequestDetailDto>().ToList();
// or also
// List<ProductRequestDetailDto> ProductRequestDetailDtoList = aProductRequestList.Select(AutoMapper.Mapper.Map<ProductRequestDetailDto>).ToList();
return ProductRequestDetailDtoList;
}
public IQueryable<ProductRequest> GetProductRequestExtendedDetailAll()
{
var ReportResult = from Req in context.ProductRequests
select Req;
return ReportResult;
}

Categories