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.
Related
I have declared a map that maps an entity to a DTO. That DTO has a foreign key reference to another DTO that has to be mapped by automapper, using ProjectTo. This works perfectly fine when running the solution, but when i use the maps in my unit tests, in doesnt work until i remove the foreign key property from my DTO. I think there is something missing in my AutoMapper setup, but im not sure.
The model looks like this:
public class PendingReportDto
{
public Guid Id { get; set; }
public Guid PatientId { get; set; }
public long Identifier { get; set; }
public DatabaseType Database { get; set; }
public DateTime? ReportedDate { get; set; }
public PatientDto Patient { get; set; }
public IdentifierType IdentifierType { get; set; }
}
The map looks like this:
CreateMap<Report, PendingReportDto>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.Database, opt => opt.MapFrom(src => src.Database))
.ForMember(dest => dest.PatientId, opt => opt.MapFrom(src => src.PatientId))
.ForMember(dest => dest.ReportedDate, opt => opt.MapFrom(src => src.ReportedDate))
.ForMember(dest => dest.Identifier, opt => opt.MapFrom(src => src.Identifier))
.ForMember(dest => dest.IdentifierType, opt => opt.MapFrom(src => src.IdentifierType))
.ForMember(dest => dest.Patient, opt => opt.MapFrom(src => src.Patient));
Patient has it's own map that works perfectly fine on it's own.
Above map is used like this:
return ReadContext.Reports
.Where(x => x.Database == databaseType && x.ReportedDate == null)
.ProjectTo<PendingReportDto>(_mapper.ConfigurationProvider)
.ToListAsync(cancellationToken: cancellationToken);
When doing that i get the following error:
System.ArgumentNullException: Value cannot be null. (Parameter 'bindings')
Automapper is setup like this in unit test project:
public static class SetupAutomapper
{
public static IMapper Setup()
{
var config = new MapperConfiguration(opts =>
{
var profiles = typeof(MappingProfile).Assembly.GetTypes().Where(x => typeof(MappingProfile).IsAssignableFrom(x));
foreach (var profile in profiles.Distinct())
{
opts.AddProfile(Activator.CreateInstance(profile) as MappingProfile);
}
});
return config.CreateMapper();
}
}
It works if i use a select statement, instead of using ProjectTo to map to my DTO.
UPDATE:
Further investigation shows that the culprit might be me running an in-memory database, instead of my regular database, when running my unit tests. If i swap it out, even with the same dataset, it works as intended. Could this be a bug with EF Core in-memory db and automapper?
So im pretty sure I found the issue with using ProjectTo to map reverse navigation properties. The issue doesn't lie with Automapper itself or the way I have configurated it in my test setup.
The culprit seems to be the db provider: Entity Framework Core in-memory db.
If i swap out the database with a localdb or a regular MS SQL DB, it works just fine. The in-memory db provider has certain limitations, which seems to limit the usage of ProjectTo with Automapper.
Source: https://learn.microsoft.com/en-us/ef/core/testing/
How to setup local db:
private static void SetupLocalDb(DbContextOptionsBuilder builder)
{
builder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=Testing;Trusted_Connection=True;");
}
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();
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.
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.
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;
}