Init Ilist in a automapper in the same line - c#

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

Related

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

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.

AutoMapper: Limit number of rows in nested model

I have a viewModel with lots of properties and a lot of collections of other viewModels
public ICollection<ListView> Elements { get; set; }
Can I specify somewhere that AutoMapper only takes the first 10 elements of this collection?
You could do a resolve using in your map. This would give you an opportunity to put in your own expression like so:
YourClassConstructorOrWhatever(){
AutoMapper.Mapper.CreateMap<YourSourceObject, YourDestObject>()
.ForMember(dest => dest.Elements, opt => opt.ResolveUsing(src =>
{
var result = new List<YourMapObject>();
foreach (var element in src.Elements.Take(10))
{
result.Add(Mapper.Map<YourMapObject>(element));
}
return result;
}));
}
Resolve using in your mapping configuration allows you to specify how you want to map one thing to another.
More info and examples can be found here:
https://github.com/AutoMapper/AutoMapper/wiki/Custom-value-resolvers
Mapper.Initialize(cfg => {
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Total,
opt => opt.ResolveUsing<CustomResolver, decimal>(src => src.SubTotal));
cfg.CreateMap<OtherSource, OtherDest>()
.ForMember(dest => dest.OtherTotal, opt => opt.ResolveUsing<CustomResolver, decimal>(src => src.OtherSubTotal));
});
public class CustomResolver : IMemberValueResolver<object, object, decimal, decimal> {
public decimal Resolve(object source, object destination, decimal sourceMember, decimal destinationMember, ResolutionContext context) {
// your mapper logic here
}
}
You could also use custom resolvers to do this:
https://github.com/AutoMapper/AutoMapper/wiki/Custom-value-resolvers#customizing-the-source-value-supplied-to-the-resolver

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

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