Automapper list to string ignore strange value - c#

I have a destination that is a List<string> and a source that's a string. I setup my config to ignore the List<string> field but I keep getting the List type as the string value
class MyClass
{
string MyList {get;set;}
}
class MyClassDto
{
List<string> MyList {get;set;}
}
//My cfg is like this
cfg.CreateMap<MyClassDto, MyClass>().ForMember(x => x.MyList, opt => opt.Ignore());
//I've mapped this way
ObjectMapper.Map(input, dest);
//and this way
var destClass = ObjectMapper.Map<MyClass>(input);
It doesn't throw an error but it makes my string field this when its empty list:
System.Collections.Generic.List`1[System.String]
Can someone just explain why this is?

From the example you provide, it seems that the mapper you are using does not know of your configuration. In AutoMapper 9 you could use the configuration to create the mapper, e.g.:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<MyClassDto, MyClass>().ForMember(x => x.MyList, opt => opt.Ignore());
});
var mapper = config.CreateMapper();
var dest = mapper.Map<MyClass>(input);
Further information can be found in the docs.

Related

Is this a bug with AutoMapper's PascalCase naming convention regex?

I'm attempting to leverage AutoMapper in order to not have to manually write a lot of code mappings. This appears to be working fine for everything other than this one class:
CreateMap<AccountConnection, AccountConnectionDto>();
CreateMap<Account, AccountDto>();
CreateMap<Address, AddressDto>() // <--- this one
.ForMember(dest => dest.StreetAddress1, opt => opt.MapFrom(src => src.street_address_1))
.ForMember(dest => dest.StreetAddress2, opt => opt.MapFrom(src => src.street_address_2))
.ForMember(dest => dest.StreetAddress3, opt => opt.MapFrom(src => src.street_address_3));
If I don't manually map those 3 members, then when I run config.AssertConfigurationIsValid(); it throws.
Exception Details: AutoMapper.AutoMapperConfigurationException:
Unmapped members were found. Review the types and members below.
Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
For no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters
=================================================
Address -> AddressDto (Destination member list)
Proj.Data.Address -> Proj.API.AddressDto (Destination member list)
Unmapped properties:
StreetAddress1
StreetAddress2
StreetAddress3
I am using the following naming conventions in my profile:
SourceMemberNamingConvention = new LowerUnderscoreNamingConvention();
DestinationMemberNamingConvention = new PascalCaseNamingConvention();
These are the only properties with numbers in the property name so I dug in to the AutoMapper source on GitHub and found the Regex for the PascalCaseNamingConvention that I'm using in my project. It is:
(\p{Lu}+(?=$|\p{Lu}[\p{Ll}0-9])|\p{Lu}?[\p{Ll}0-9]+)
If I throw that in https://regex101.com and then test it against my property name ShippingAddress1 I get two matches, Shipping and Address1.
Ruh-roh! My source property name is shipping_address_1 (Don't ask) so that isn't going to work. Is this because my naming convention is broken, or should the PascalCaseNamingConvention match shipping_address_x to ShippingAddressX? (Went to raise an issue on the AutoMapper github but they ask newcomers to post on SO first, to see if people think it is a legitimate bug or not).
From my testing detailed below, I believe the naming conventions specified are the wrong way around:
public class Address
{
public string StreetAddress1 { get; set; }
}
public class AddressDto
{
public string street_address_1 { get; set; }
}
static void Main(string[] args)
{
// Prints nothing
PerformMappingTest(new PascalCaseNamingConvention(), new LowerUnderscoreNamingConvention());
// Prints "Test"
PerformMappingTest(new LowerUnderscoreNamingConvention(), new PascalCaseNamingConvention());
Console.ReadKey();
}
static void PerformMappingTest(INamingConvention source, INamingConvention destination)
{
var config = new MapperConfiguration(cfg => {
cfg.SourceMemberNamingConvention = source;
cfg.DestinationMemberNamingConvention = destination;
cfg.CreateMap<Address, AddressDto>();
});
var mapper = config.CreateMapper();
var address = new Address { StreetAddress1 = "Test" };
var addressDto = mapper.Map<Address, AddressDto>(address);
Console.WriteLine(addressDto.street_address_1);
}

Automapper empty list to empty list

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 ?

Is it possible to created a collection on the dest based on a collection in source?

Say I have
public class A
{
public List<int> Ids {get;set;}
}
public class B
{
public List<Category> Categories {get;set;}
}
public class Category
{
public string Name {get;set;} //will be blank on map
public int CategoryId {get;set;}
}
var source = new A {...};
var b = mapper.Map<A, B>(source);
so when mapped it will actually create a new collection on the dest but will map the ids based on what's in the source collection, other properties of the dest will be blank because there is nothing to map from.
How to setup the configuration to do this mapping?
You need a combination of ForMember, MapFrom and ForAllOtherMembers:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<A, B>()
.ForMember(dest => dest.Categories, opt => opt.MapFrom(src => src.Ids));
cfg.CreateMap<int, Category>()
.ForMember(dest => dest.CategoryId, opt => opt.MapFrom(src => src))
.ForAllOtherMembers(opt => opt.Ignore());
});
MapFrom will allow you to override the default mapping-by-name that AM normally does. As you can see in line 4, we can say that Ids in the source maps to Categories in the destination class.
But now you need to override how an int gets mapped, since that is the type of thing in Ids. With MapFrom, you don't (necessarily) have to provide a property for the source--the entire source itself can be the thing being mapped. So in line 7, we are mapping the ints that came from the mapping in line 4 and saying that they should map to the destination class' CategoryId property. Finally, we simply tell AM that we don't care to map any remaining properties in the target class by specifying the ForAllOtherMembers option.

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