Automapper empty list to empty list - c#

I have the following two classes:
I have two classes
public class SourceClass
{
public Guid Id { get; set; }
public string Provider { get; set; }
}
public class DestinationClass
{
public Guid Id { get; set; }
public List<string> Providers { get; set; }
}
I have the following mappings for my automapper
CreateMap<SourceClass, DestinationClass>()
.ForMember(destinationMember => destinationMember.Providers,
memberOptions => memberOptions.MapFrom(src => new List<string> {src.Provider ?? ""}));
CreateMap<DestinationClass, SourceClass>().ForMember(SourceClass => SourceClass.Provider,
memberOptions => memberOptions.MapFrom(src => src.Providers.FirstOrDefault()));
I wrote some unit tests and can confirm the following behaviour:
When Providers in my destination class is null, it maps to null, which is great. However, I'd like to change my mapping so that if Providers is an empty list, it maps to an empty list and similarly, if Provider is null, I'd want it to map to an empty list instead of a list with an empty string.
Does anyone know how I can go about doing this? I've tried this for my mapping from SourceClass to DestinationClass:
CreateMap<SourceClass, DestinationClass>()
.ForMember(destinationMember => destinationMember.Providers,
memberOptions => memberOptions.MapFrom(src => !string.IsNullOrEmpty(src.Provider) ? new List<string> {src.Provider} : new List<string>()));
but for going the other way, an empty list is mapping to null, instead of an empty list. (I think because of FirstOrDefault() ). Does anyone know how I can work around this?

Tested this mapping with AutoMapper 6.1.1
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<SourceClass, DestinationClass>()
.ForMember(dest => dest.Providers,
opt => opt.MapFrom(src => string.IsNullOrEmpty(src.Provider) ? new List<string>()
: new List<string> { src.Provider }));
cfg.CreateMap<DestinationClass, SourceClass>()
.ForMember(dest => dest.Provider,
opt => opt.MapFrom(src => src.Providers == null ? ""
: src.Providers.FirstOrDefault() ?? ""));
});
If you really want it "inline" that will work. I still think it'd be easier to read if you moved it out into a method you could call, or if you could upgrade AutoMapper to leverage the built in converter mapping. Hopefully this gives you enough information to start.

Yep. I think its because its FirstOrDefaults( ) .. why not use the same ternary operator or an if statement to place your conditions for mapping ?

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

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.

Mapping from DataTable to custom model with property names different than column names

In the application I am working on the data is acquired and returned in data sets. However, I want to make the application model-based, therefore I need to map data sets to my custom model classes. I have come across this way of converting data tables to custom model classes.
As I have no control on the data source that returns data sets, I cannot rename its column names. And still in the model class I want to use properties with names of my own convention.
The case is: say I have a model
class PersonModel
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
and the source data table has columns named Identificator, First_Name, Last_Name. Is there a way to somehow bind corresponding labels and map the content of the data table to a collection of PersonModel?
First you would create the mapping as follows:
Mapper.CreateMap<IDataReader, PersonModel>()
.ForMember(dest => dest.ID, opt => opt.MapFrom(src => src.GetInt32(src.GetOrdinal("Identificator"))))
.ForMember(dest => dest.FirstName , opt => opt.MapFrom(src => src.GetString(src.GetOrdinal("First_Name"))))
.ForMember(dest => dest.LastName , opt => opt.MapFrom(src => src.GetString(src.GetOrdinal("Last_Name"))));
Then in your code you would create a data reader and map it:
// Get the data reader and store in dr variable
var people = Mapper.Map<IDataReader, List<PersonModel>>(dr);
Just an FYI
for AutoMapper 8.0.0 and AutoMapper.Data 3.0.0
var config = new MapperConfiguration(cfg =>
{
cfg.AddDataReaderMapping();
cfg.CreateMap<IDataRecord, PersonModel>()
.ForMember(dest => dest.ID, opt => opt.MapFrom(src => src.GetValue(src.GetOrdinal("Identificator"))))
.ForMember(dest => dest.FirstName, opt => opt.MapFrom(src => src.GetValue(src.GetOrdinal("First_Name"))))
.ForMember(dest => dest.LastName, opt => opt.MapFrom(src => src.GetValue(src.GetOrdinal("Last_Name"))));
});
var mapper = config.CreateMapper();
DataTable dt = GetDataTable();
var people = mapper.Map<IDataReader, List<PersonModel>>(dt.CreateDataReader());
Don't know why but must use CreateMap<IDataRecord instead of CreateMap<IDataReader to make it work
Reference:
https://github.com/AutoMapper/AutoMapper.Data/issues/35
I would recommend the old fashioned way. Build a data access class that would retrieve the data handle the conversion and return your custom objects.
public static List<PersonModel> GetPeople()
{
List<PersonModel> people = new List<PersonModel>();
DataSet data = SomeMethodToGetTheDataSet();
foreach(DataRow in data.Tables[0].Rows)
{
PersonModel person = getPersonFromDataRow();
people.Add(person);
}
return people;
}
private static PersonModel getPersonFromDataRow(DataRow row)
{
PersonModel person = new PersonModel();
person.ID = row.Field<int>("Identificator");
person.FirstName = row.Field<string>("First_Name");
person.LastName = row.Field<string>("Last_Name");
return person;
}
This is just a quick example. you'll need to add error checking and try/catch blocks to prevent null reference exceptions. But this should show you basically how it's done.

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