How to override Automapper Profile - c#

I am using Automapper on my dotnet core project on which I have two profiles. One profile is use for common Mapping and second two override if some mapping is defined on that profile.
Lets say I have one profile:
public class CommonProfile : Profile
{
public CommonProfile(){
CreateMap<Product, ProductDto>()
.ForMember(dest => dest.ProductId, options => options.MapFrom(src => src.Id))
.ForMember(dest => dest.Title, options => options.MapFrom<Description>())
.ForMember(dest => dest.Price, options => options.MapFrom(src => src.Amount))
.AfterMap<ProductAfterMap>()
}
}
Now I have another profile on which some different mapping is defined for same classes with different properties like below:
public class UniqueProfile : Profile
{
public UniqueProfile(){
CreateMap<Product, ProductDto>()
.ForMember(dest => dest.Title, options => options.MapFrom<Name>())
}
}
In the first profile Title was mapped with Description but on the second profile it was mapped with Name. Now I have to use the second mapping for Title Property.
There are some more properties which requires different mapping in unique mapping, also custom resolovers.

You can use the CreateMap method on the UniqueProfile class to override the mapping defined in the CommonProfile class. The CreateMap method in the UniqueProfile class will have higher priority and will be used to perform the mapping.
Here's an example:
public class CommonProfile : Profile
{
public CommonProfile()
{
CreateMap<Product, ProductDto>()
.ForMember(dest => dest.ProductId, options => options.MapFrom(src => src.Id))
.ForMember(dest => dest.Title, options => options.MapFrom<Description>())
.ForMember(dest => dest.Price, options => options.MapFrom(src => src.Amount))
.AfterMap<ProductAfterMap>();
}
}
public class UniqueProfile : Profile
{
public UniqueProfile()
{
CreateMap<Product, ProductDto>()
.ForMember(dest => dest.Title, options => options.MapFrom<Name>())
// Add other custom mapping rules here
.AfterMap<UniqueProductAfterMap>();
}
}
In your application, make sure you are only using the UniqueProfile class while performing the mapping. You can do this by creating an instance of MapperConfiguration and passing in the UniqueProfile class when configuring the Automapper.

Related

Automapper unable to map foreign key properties in unit test project

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

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.

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.

Automapper foces subobjects to be mapped

I am trying to map a response object from webservice to a Class in my project. I thought the Automapper will map even the sub objects automatically, it does not unless and until is forcefully set for the member. Why should i do this ?
Mapper.CreateMap<GetIfpQuoteResponse.Quote, QuoteWSModel>()
.ForMember(dest => dest.CarrierRate, opt => opt.MapFrom(src => src.Carriers))
.ForMember(dest => dest.DroppedCarriers, opt => opt.MapFrom(src => src.DroppedRates))
.ForMember(dest => dest.MemberPlans, opt => opt.MapFrom(src => src.MemberPlans));
Why Wont the automapper map my su bobjects when i mention the class mapping like this
Mapper.CreateMap<GetIfpQuoteResponse.Quote, QuoteWSModel>();
Mapper.CreateMap<GetIfpQuoteResponse.Quote.Carrier, CarrierRateModel>();
Mapper.CreateMap<GetIfpQuoteResponse.Quote.DroppedCarrier, DroppedCarrierModel>();
AutoMapper only map top level object.
If your class is built in the following way it will not work:
Class A
{
B b;
}
Class B
{
}
Class A will not know how to map property B inside class A.
To do this you will need to create a Profile class.
Auto Mapper tutorial

Automapper multiple memberOptions

I have a ViewModel that needs data from 2 collections. The 2 collections are members of Indicatiestelling. So to map this I pass an instance of Indicatiestelling.
Each property uses a ValueResolver that gets the right value out of the given collection. To make this work I need to register the ValueResolver for each property and the source for each property. I tried to do this:
Mapper.CreateMap<Model.Indicatiestelling, ClientRechtmatigheidDto>()
.ForMember(dest => dest.HasFactBeoordelenRechtmatigheid, (opt) => { opt.ResolveUsing<IndicatiestellingFactValueResolver>(); opt.MapFrom(src => src.IndicatiestellingFacts); })
.ForMember(dest => dest.HasFactRechtmatig, (opt) => { opt.ResolveUsing<IndicatiestellingFactValueResolver>(); opt.MapFrom(src => src.IndicatiestellingFacts); })
.ForMember(dest => dest.SoortVoorziening, (opt) => { opt.ResolveUsing<IndicatiestellingAnswerValueResolver>(); opt.MapFrom(src => src.IndicatiestellingAnswer); })
.ForMember(dest => dest.ZZP, (opt) => { opt.ResolveUsing<IndicatiestellingAnswerValueResolver>(); opt.MapFrom(src => src.IndicatiestellingAnswer); });
This code doesn't work, I still get mapping errors:
Missing type map configuration or unsupported mapping.
Mapping types: HashSet`1 -> Boolean
I searched for an example/docs about using multiple member options, nothing came up. Is it supported? And ifso, what am I doing wrong here?
I don't think they can be combined in the way you want, but with a little refactoring you can use this:
Mapper.CreateMap<Model.Indicatiestelling, ClientRechtmatigheidDto>()
.ForMember(dest => dest.HasFactBeoordelenRechtmatigheid, opt => opt.ResolveUsing(src => IndicatiestellingFactValueResolver.Resolve(src.IndicatiestellingFacts)))
.ForMember(dest => dest.HasFactRechtmatig, opt => opt.ResolveUsing(src => IndicatiestellingFactValueResolver.Resolve(src.IndicatiestellingFacts)))
.ForMember(dest => dest.SoortVoorziening, opt => opt.ResolveUsing(src => IndicatiestellingAnswerValueResolver.Resolve(src.IndicatiestellingAnswer)))
.ForMember(dest => dest.ZZP, opt => opt.ResolveUsing(src => IndicatiestellingAnswerValueResolver.Resolve(src.IndicatiestellingAnswer)));
(I refactored your IValueResolvers into static methods)
Or you can make your IValueResolvers know which member to look at, e.g. hardcoded:
public class IndicatiestellingFactValueResolver : IValueResolver
{
public ResolutionResult Resolve(ResolutionResult source)
{
var model = (Model.Indicatiestelling)source.Value;
var obj = model.IndicatiestellingFacts;
// calculate with obj
}
}
Mapper.CreateMap<Model.Indicatiestelling, ClientRechtmatigheidDto>()
.ForMember(dest => dest.HasFactBeoordelenRechtmatigheid, opt => opt.ResolveUsing<IndicatiestellingFactValueResolver<Model.Indicatiestelling>>())
// etc
Or using a Func:
public class IndicatiestellingFactValueResolver<TSource> : IValueResolver
{
private Func<TSource, object> selector;
public IndicatiestellingFactValueResolver(Func<TSource, object> selector)
{
this.selector = selector;
}
public ResolutionResult Resolve(ResolutionResult source)
{
var model = (TSource)source.Value;
object obj = selector(model);
// calculate with obj
}
}
Mapper.CreateMap<Model.Indicatiestelling, ClientRechtmatigheidDto>()
.ForMember(dest => dest.HasFactBeoordelenRechtmatigheid,
opt => opt.ResolveUsing<IndicatiestellingFactValueResolver<Model.Indicatiestelling>>()
.ConstructedBy(() => new IndicatiestellingFactValueResolver<Model.Indicatiestelling>(x => x.IndicatiestellingFacts)))
// etc

Categories