AutoMapper: Limit number of rows in nested model - c#

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

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

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 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.

Automapper multiple memberOptions

I have a ViewModel that needs data from 2 collections. The 2 collections are members of Indicatiestelling. So to map this I pass an instance of Indicatiestelling.
Each property uses a ValueResolver that gets the right value out of the given collection. To make this work I need to register the ValueResolver for each property and the source for each property. I tried to do this:
Mapper.CreateMap<Model.Indicatiestelling, ClientRechtmatigheidDto>()
.ForMember(dest => dest.HasFactBeoordelenRechtmatigheid, (opt) => { opt.ResolveUsing<IndicatiestellingFactValueResolver>(); opt.MapFrom(src => src.IndicatiestellingFacts); })
.ForMember(dest => dest.HasFactRechtmatig, (opt) => { opt.ResolveUsing<IndicatiestellingFactValueResolver>(); opt.MapFrom(src => src.IndicatiestellingFacts); })
.ForMember(dest => dest.SoortVoorziening, (opt) => { opt.ResolveUsing<IndicatiestellingAnswerValueResolver>(); opt.MapFrom(src => src.IndicatiestellingAnswer); })
.ForMember(dest => dest.ZZP, (opt) => { opt.ResolveUsing<IndicatiestellingAnswerValueResolver>(); opt.MapFrom(src => src.IndicatiestellingAnswer); });
This code doesn't work, I still get mapping errors:
Missing type map configuration or unsupported mapping.
Mapping types: HashSet`1 -> Boolean
I searched for an example/docs about using multiple member options, nothing came up. Is it supported? And ifso, what am I doing wrong here?
I don't think they can be combined in the way you want, but with a little refactoring you can use this:
Mapper.CreateMap<Model.Indicatiestelling, ClientRechtmatigheidDto>()
.ForMember(dest => dest.HasFactBeoordelenRechtmatigheid, opt => opt.ResolveUsing(src => IndicatiestellingFactValueResolver.Resolve(src.IndicatiestellingFacts)))
.ForMember(dest => dest.HasFactRechtmatig, opt => opt.ResolveUsing(src => IndicatiestellingFactValueResolver.Resolve(src.IndicatiestellingFacts)))
.ForMember(dest => dest.SoortVoorziening, opt => opt.ResolveUsing(src => IndicatiestellingAnswerValueResolver.Resolve(src.IndicatiestellingAnswer)))
.ForMember(dest => dest.ZZP, opt => opt.ResolveUsing(src => IndicatiestellingAnswerValueResolver.Resolve(src.IndicatiestellingAnswer)));
(I refactored your IValueResolvers into static methods)
Or you can make your IValueResolvers know which member to look at, e.g. hardcoded:
public class IndicatiestellingFactValueResolver : IValueResolver
{
public ResolutionResult Resolve(ResolutionResult source)
{
var model = (Model.Indicatiestelling)source.Value;
var obj = model.IndicatiestellingFacts;
// calculate with obj
}
}
Mapper.CreateMap<Model.Indicatiestelling, ClientRechtmatigheidDto>()
.ForMember(dest => dest.HasFactBeoordelenRechtmatigheid, opt => opt.ResolveUsing<IndicatiestellingFactValueResolver<Model.Indicatiestelling>>())
// etc
Or using a Func:
public class IndicatiestellingFactValueResolver<TSource> : IValueResolver
{
private Func<TSource, object> selector;
public IndicatiestellingFactValueResolver(Func<TSource, object> selector)
{
this.selector = selector;
}
public ResolutionResult Resolve(ResolutionResult source)
{
var model = (TSource)source.Value;
object obj = selector(model);
// calculate with obj
}
}
Mapper.CreateMap<Model.Indicatiestelling, ClientRechtmatigheidDto>()
.ForMember(dest => dest.HasFactBeoordelenRechtmatigheid,
opt => opt.ResolveUsing<IndicatiestellingFactValueResolver<Model.Indicatiestelling>>()
.ConstructedBy(() => new IndicatiestellingFactValueResolver<Model.Indicatiestelling>(x => x.IndicatiestellingFacts)))
// etc

Categories