Automapper unable to map foreign key properties in unit test project - c#

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

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 to do a partial map from source in Automapper

I am trying to map only 2 out of 4 properties from an object to the destination type. In my case DeletedBy and DeletedDate, where as DeletedDate will simply be set to the current UTC date.
public class DeleteCommand : IRequest
{
public string CodePath { get; set; }
[JsonIgnore]
public Guid? DeletedBy { get; set; }
[IgnoreMap]
public DeleteMode DeleteMode { get; set; } = DeleteMode.Soft;
}
This is my current configuration:
CreateMap<DeleteCommand, Asset>(MemberList.Source)
.ForMember(x => x.DeletedDate, opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(x => x.DeletedBy, opt => opt.MapFrom(src => src.DeletedBy));
Running a unit test against this specific mapper configuration gives me 2 errors for a missing mapping:
[Fact]
public void MapperConfigShouldBeValid()
{
_config.AssertConfigurationIsValid();
}
Unmapped properties:
DeletedDate
DeleteMode
This is confusing me, since the Date is explicitly defined and the DeleteMode is set to be ignored by default. If possible I want to avoid to create another dto to be mapped from a first dto and then to the entity, to be soft-deleted, by setting the audit fields.
Things I've tried so far:
IgnoreMapAttribute as shown above
ForSourceMember(), seems to not support an Ignore method for a source property.
This can be solved by removing MemberList.Source from argument list of CreateMap() and ignoring all remaining unmapped destination members.
CreateMap<DeleteCommand, Asset>()
.ForMember(x => x.DeletedDate, opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(x => x.DeletedBy, opt => opt.MapFrom(src => src.DeletedBy))
.ForAllOtherMembers(x => x.Ignore())
Same could be achieved by having CreateMap(MemberList.None). This doesn't even require explicitly ignoring all other destination members.
Removing DeletedDate as a property solved 50% of my issues, since I don't need it on the source any more.
The other one was updating the map with ForSourceMember(x => x.DeleteMode, y => x.DoNotValidate())
This then also works in a quick unit test:
[Fact]
public void DeleteMapShouldSetAuditFields()
{
var asset = new Asset();
var cmd = new DeleteCommand
{
DeletedBy = Guid.NewGuid()
};
_mapper.Map(cmd, asset);
Assert.NotNull(asset.DeletedBy);
Assert.NotNull(asset.DeletedDate);
Assert.Equal(cmd.DeletedBy, asset.DeletedBy);
}

Automapper expression mapping

I am trying to perform the following Automapper mapping for an OrderBy:
Expression<Func<ServerObject, object>> serverQueryable = x => x.TestEnumKVP.Value;
Mapper.Map<Expression<Func<ServerObject, object>>, Expression<Func<DatabaseObject, object>>(serverQueryable)
I want to map the ServerObject expression to a DatabaseObject expression
ServerObject defined as:
public class ServerObject
{
public KeyValuePairEx TestEnumKVP { get; set; }
}
KeyValuePairEx is a wrapper for the Enumeration which stores the Int16 value and the string value:
public enum TestEnum : Int16 { Test1, Test2, Test3 }
public class KeyValuePairEx
{
internal KeyValuePairEx(TestEnum key, string value) { }
public TestEnum Key { get; set; }
public string Value { get; set; }
}
DatabaseObject defined as:
public class DatabaseObject
{
public string TestEnumId { get; set; }
}
The Mapping I have is:
AutoMapper.Mapper.Initialize(config =>
{
config.CreateMap<DatabaseObject, ServerObject>().ForMember(dest => dest.TestEnumKVP.Value, opt => opt.MapFrom(src => src.TestEnumId));
});
The mapping fails with:
'Expression 'dest => dest.TestEnumKVP.Value' must resolve to top-level member and not any child object's properties. Use a custom resolver on the child type or the AfterMap option instead.'
I need ServerObject.TestEnumKVP.Value to Map to DatabaseObject.TestEnumId. I am aware that Expression mappings are reversed - hence why the Map is from DatabaseObject to ServerObject. I have spent many hours on this and am at a loss as to how to get the mapping to work!
NB. I am using AutoMapper 6.1.1
Any help would be appreciated!
Thank you Lucian, I followed the github link and the solution offered by Blaise has worked. See below:
CreateMap<DatabaseObject, ServerObject>().ForMember(dest => dest.TestEnumKVP, opt => opt.MapFrom(src => src));
CreateMap<DatabaseObject, KeyValuePairEx>().ForMember(dest => dest.Value, opt => opt.MapFrom(src => src.TestEnumId));
I was starting to look for at workarounds so delighted it was possible and that the solution was so clean and concise.
Thanks again!
The error and the solution are right there in the message. Forget about all the expression stuff. The ForMember is broken. Try ForPath instead.
Expression mapping now supports ForPath. See https://github.com/AutoMapper/AutoMapper/issues/2293.

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 map to a different destination property when one matches source?

For example, suppose I have the following...
public class TheSource
{
public string WrittenDate { get; set; }
}
public class TheDestination
{
public string CreateDate { get; set; }
public DateTime WrittenDate { get; set;}
}
and I have the mapping as such...
Mapper.CreateMap<TheSource, TheDestination>()
.ForMember(dest => dest.CreateDate, opt => opt.MapFrom(src => src.WrittenDate));
Question: Is the Automapper trying to map the TheSource.WrittenDate to TheDestination.WrittenDate instead of TheDestination.CreateDate as I specified in the .ForMember?
-- I ask this because I am getting an AutoMapper DateTime exception from the CreateMap line above.
Is the Automapper trying to map the TheSource.WrittenDate to TheDestination.WrittenDate instead of TheDestination.CreateDate as I specified in the .ForMember?
Not instead of TheDestination.CreateDate.
Automapper will map src.WrittenDate to dest.CreateDate because you specified that explicitly.
And it will map src.WrittenDate to dest.WrittenDate because by convention, if you don't specify otherwise, properties with the same name will be mapped to each other when you create the map.
To override this behavior, you can explcitly tell Automapper to ignore dest.WrittenDate like this:
Mapper.CreateMap<TheSource, TheDestination>()
.ForMember(dest => dest.CreateDate, opt => opt.MapFrom(src => src.WrittenDate))
.ForMember(dest => dest.WrittenDate, opt => opt.Ignore());

Categories