Can AutoMapper map to a different destination property when one matches source? - c#

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

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

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

Init Ilist in a automapper in the same line

I seem to have some issue init a IList defined in a class, using automapper, and then adding a element to this list.
var config = new MapperConfiguration(.ForMember(dest => dest.Catalogs = new List<dto>);
can this not be done in the same line?
First thing you should note is that AutoMapper will create collections automatically if it kowns how to map the elements of these collections.
For example, given the following classes:
public class Source {
public IList<SourceObj> SourceCollection {get; set;}
}
public class Destination {
public DestDto[] DestinationCollection {get; set;}
}
The collections will be mapped correctly with this configuration:
CreateMap<SourceObj, DestDto>();
CreateMap<Source, Destination>()
.ForMember(dest => dest.DestinationCollection, opt => opt.MapFrom(src => src.SourceCollection));
Check if this mapping works:
var destDto = Mapper.Map<Destination>(sourceObj);
Assert.IsNotNull(destDto.DestinationCollection);
If you always want to set the destination collection to an empty list, you can do that with ResolveUsing:
CreateMap<Source, Destination>()
.ForMember(dest => dest.DestinationCollection, opt => opt.ResolveUsing(src => new List<DestDto>()));

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 convention based mapping for collection

I have a project where I am trying to map a dictionary to a ViewModel.NamedProperty. I am trying to use an AutoMapper custom resolver to perform the mapping based on a convention. My convention is that if the named property exists for the source dictionary key then map a property from the dictionary's value. Here are my example classes:
class User
{
string Name {get;set;}
Dictionary<string, AccountProp> CustomProperties {get;set;}
}
class AccountProp
{
string PropertyValue {get;set;}
//Some other properties
}
class UserViewModel
{
string Name {get;set;}
DateTime LastLogin {get;set;}
string City {get;set}
}
var user = new User()
{
Name = "Bob"
};
user.CustomProperties.Add("LastLogin", new AccountProp(){PropertyValue = DateTime.Now};
user.CustomProperties.Add("City", new AccountProp(){PropertyValue = "SomeWhere"};
I want to map the User CustomProperties dictionary to the flattened UserViewModel by convention for all properties and I do not want to specify each property individually for the mapping.
What is the best way to go about this? I was thinking Custom value resolver but it seems that I have to specify each member I want to map individually. If I wanted to do that I would just manually perform the mapping without AutoMapper.
Below is code that serve the purpose. Not sure whether it is good or not.
Mapper.CreateMap<User, UserViewModel>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name)) // Optional
.ForMember(dest => dest.City, opt => opt.MapFrom(src => src.CustomProperties.FirstOrDefault(x => x.Key == "City").Value.PropertyValue.ToString())) // Handle null
.ForMember(dest => dest.LastLogin, opt => opt.MapFrom(src => Convert.ToDateTime(src.CustomProperties.FirstOrDefault(x => x.Key == "LastLogin").Value.PropertyValue))); //Handle null
I ended up creating a custom type converter to deal with this scenario and it works great:
public class ObjectToPropertyTypeConverter<TFromEntity> : ITypeConverter<TFromEntity, HashSet<Property>>
{
//perform custom conversion here
}
I then implemented the Custom mapping as follows:
AutoMapper.Mapper.CreateMap<MyViewModel, HashSet<Property>>()
.ConvertUsing<ObjectToPropertyTypeConverter<MyViewModel>>();

Categories