AutoMapper - Map child property hiding base class property - c#

I'm encountering a quite stupid issue while trying to map a class to a derived class using AutoMapper with C#.
These are my classes:
class BaseParent {
public string Name { get; set; }
public BaseChild Child { get; set; }
}
class BaseChild {
public int Age { get; set; }
}
class DerivedParent : BaseParent {
public new DerivedChild Child { get; set; }
}
class DerivedChild : BaseChild { }
In particular, what I'm trying to achieve is that all the properties of the mapped class are correctly set. The issue is that the Child property of the mapped class is not set and remains null.
This is the mapping configuration I'm using:
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<BaseChild, DerivedChild>();
cfg.CreateMap<BaseParent, DerivedParent>()
.ForMember(dest => dest.Child, opt => opt.MapFrom(src => src.Child));
});
Any help is appreciated.
Thanks
EDIT
Actually is not correct to say that Child property remains null.
Debugging the code I can see that there are 2 child properties with the same name because of the new modifier used to hide the parent one.
Anyway, the property I need is still null.

Are you sure you're looking at it correctly?
Given the following config:
// Arrange
var mapper = new Mapper(new MapperConfiguration(cfg => {
cfg.CreateMap<BaseChild, DerivedChild>();
cfg.CreateMap<BaseParent, DerivedParent>()
.ForMember(dest => dest.Child, opt => opt.MapFrom(src => src.Child));
}));
var baseParent = new BaseParent { Name = "A", Child = new BaseChild { Age = 1 } };
// Act
var derived = mapper.Map<DerivedParent>(baseParent);
I can assure you that:
derived.Child is certainly not null
It is of type DerivedChild
The hidden BaseChild is null
Which you can see in the following (see working Fiddle):
// Assert
Assert.IsNotNull(derived);
Assert.IsInstanceOf<DerivedParent>(derived);
// The 'new' Child property is not null
Assert.IsNotNull(derived.Child);
Assert.IsInstanceOf<DerivedChild>(derived.Child);
// The hidden property should be null
Assert.IsNull(((BaseParent)derived).Child);

Related

AutoMapper does not ignore List when mapping from source to existing destination

I'm having a small problem with AutoMapper. I have isolated the issue I am facing, if it actually is an issue and not just a misunderstanding.
Here are the classes I am working with:
public class DemoEntity
{
public List<string> Items { get; set; }
public string Name { get; set; }
}
public class DemoDto
{
public List<string> Items { get; set; }
public string Name { get; set; }
}
public class DemoProfile : Profile
{
public DemoProfile()
{
CreateMap<DemoDto, DemoEntity>()
.ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null));
}
}
In the Dependency Injection part (which seems to be in Program.cs for .NET 6, but is in Startup.cs in my main project), I have this code which I have read that should help allow nullable collections:
builder.Services.AddAutoMapper(configAction => { configAction.AllowNullCollections = true; }, typeof(Program));
Here is my test-code:
var dto = new DemoDto();
var entity = new DemoEntity()
{
Items = new List<string>() { "Some existing item" },
Name = "Existing name"
};
// Works as expected
var newEntity = _mapper.Map<DemoEntity>(dto);
// Sets the entity.Items to an empty list
_mapper.Map(dto, entity);
As you can see in the DemoProfile constructor, I set a condition to only map if srcMember != null, which works for the Name property. With the AllowNullCollections in the service registration, I can map to a new object with a null list (would be an empty list without the AllowNullCollections part).
My expected outcome would be AutoMapper to see that dto.Items is null, and not touch the entity.Items property during the mapping, and leave it with 1 string in the list. The actual outcome is that entity.Items is a list with 0 items. Name property is ignored.
Am I missing something? How can I adjust my code to work so that AutoMapper ignores a list that is null during mapping on an existing destination?
You can look for PreCondition to prevent the mapping from source when the source's member (with the array, List) is null or empty.
CreateMap<DemoDto, DemoEntity>()
.ForMember(dest => dest.Items, opt => opt.PreCondition((source, dest) =>
{
return source.Items != null && source.Items.Count > 0;
}))
.ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null));
Sample demo on .NET Fiddle

C# Automapper inner list from another inner list of different type

I am trying to map something similar to this model:
class Source {
...
SubSource subSource;
}
class SubSource {
...
List<SourceListItem> list;
SomeInfo someInfo;
...
}
class SomeInfo {
string name;
...
}
class SourceModel {
...
SomeInfoModel someInfoModel;
...
}
class SomeInfoModel {
string name;
List<SourceListItemModel> list;
...
}
What I need is to map "SubSource.List" to "SomeInfoModel.List". I am able to map every other property correctly, but the list is always empty after the mapping and no errors happen during execution.
I have the following configuration for the mappings:
CreateMap<SourceListItem, SourceListItemModel>()
CreateMap<SomeInfo, SomeInfoModel>()
CreateMap<Source, SourceModel>()
...
.ForPath(dest => dest.someInfoModel.list, opt => opt.MapFrom(src =>
src.subSource.list))
...
Executing your code, the call to configuration.AssertConfigurationIsValid(); throws an exception where is clearly described the issue you are running into
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
SomeInfo -> SomeInfoModel (Destination member list) SomeInfo ->
SomeInfoModel (Destination member list)
Unmapped properties: list
You can refer to the documentation
Tried to reproduce, but for me the mapping of list property works.
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<SourceListItem, SourceListItemModel>();
cfg.CreateMap<SomeInfo, SomeInfoModel>();
cfg.CreateMap<Source, SourceModel>()
.ForPath(dest => dest.someInfoModel.list, opt => opt.MapFrom(src =>
src.subSource.list));
});
var source = new Source()
{
subSource = new SubSource()
{
list = new List<SourceListItem>()
{
new SourceListItem() { Text1 = "text1" },
new SourceListItem() { Text1 = "text2" },
}
}
};
var mapper = new Mapper(config);
var model = mapper.Map<SourceModel>(source);
The model used:
public class SourceListItem
{
public string Text1 { get; set; }
}
public class SourceListItemModel
{
public string Text1 { get; set; }
}

automapper mapping certain bool property based on enum value

The destination class has a list of bools. The DTO that gets mapped to the destination class has 1 enum property. depending on what the enum is some of the bools in the destination class should be set. how to achieve it in automapper?
.ForMember() won't work because i would have to do enum logic check for each of the bool property.
I want to do a mapping this.CreateMap<DestinationDTO, Destination>() where depending on what the payout is Property1 or Property2 or Property3 gets set.
See below:
public class Destination
{
public bool? Property1{get; set;}
public bool? Property2{get; set;}
public bool? Property3{get;set;}
}
public class DestinationDTO
{
public Enum Payout{get; set;}
}
public Enum Payout
{
Proration = 1,
Recurrent = 2,
Lumpsum = 3
}
If the DestinationDTO.Payout == Payout.Proration, I want to set Destination entity class's Property1 to be true, similarly depending on what payout it is, I might want to set another Property in the entity class. Can I do this in automapper when mapping the DestinationDTO to Destination entity class?
You can do this by using ForMember expressions:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<DestinationDTO, Destination>()
.ForMember(d => d.Property1,
m => m.MapFrom(d => d.Payout == Payout.Proration ? true : default(bool?)))
.ForMember(d => d.Property2,
m => m.MapFrom(d => d.Payout == Payout.Recurrent ? true : default(bool?)))
.ForMember(d => d.Property3,
m => m.MapFrom(d => d.Payout == Payout.Lumpsum ? true : default(bool?)));
});
var mapper = config.CreateMapper();
var dtos = new[]
{
new DestinationDTO { Payout = Payout.Proration },
new DestinationDTO { Payout = Payout.Recurrent },
new DestinationDTO { Payout = Payout.Lumpsum },
};
var destinations = dtos.Select(d => mapper.Map<Destination>(d));
Off-topic: I'd prefer non-nullable booleans. Then you can remove the ? true : default(bool?) parts and a Destination still tells the truth in all of its properties.

Find out mapped property using AutoMapper

I'm looking to 'map' errors when persisting DTOs back to the property in a viewmodel corresponding to the DTO property that caused the error, PersonDto.PreferedName causes a DB error, say Cannot be null, and this property maps to the viemodel PersonViewModel.Name, I want to be able to display an error message in the langiage of the view, e.g. "Name is required".
Is there any way I can query Mapper or some other AutoMapper object to find which viewmodel property PreferedName maps to?
It looks like you are getting the errors from the ModelState but to answer the original question you can use the following
using AutoMapper;
using System;
using System.Linq;
namespace ConsoleApplicationAutoMapper
{
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg => cfg.CreateMap<Order, OrderDto>()
.ForMember(dest => dest.DtoMyProperty, x => x.MapFrom(y => y.MyProperty)
));
Order order = new Order() { MyProperty = 12 };
var typeMaps = config.CreateMapper()
.ConfigurationProvider
.GetAllTypeMaps()
.Where(t => t.SourceType == typeof(Order) && t.DestinationType == typeof(OrderDto))
.Single()
.GetPropertyMaps();
foreach (var map in typeMaps)
Console.WriteLine(map.SourceMember.Name + "->" + map.DestinationProperty.Name);
Console.ReadLine();
}
}
public class Order
{
public int MyProperty { get; set; }
}
public class OrderDto
{
public int DtoMyProperty { get; set; }
}
}
As per comments, you can annotate your viewmodel with [Required] (System.ComponentModel.DataAnnotations.RequiredAttribute) and check in your controller action for ModelState.IsValid.
Serializing the contents of ModelState.Values and returning to your WPF front end will allow you to display any validation errors in whatever way suits.

Can these models be represented in EF7?

I'm trying to use some classes from another assembly in my own project as entities that I can persist using EF7, rather than writing a series of very similar classes that are more database-friendly.
Simplified versions look like this:
interface IMediaFile
{
string Uri { get; }
string Title { get; set; }
}
class CMediaFile : IMediaFile
{
public CMediaFile() { }
public string Uri { get; set; }
public string Title { get; set; }
}
//The following types are in my project and have full control over.
interface IPlaylistEntry
{
IMediaFile MediaFile { get; }
}
class CPlaylistEntry<T> : IPlaylistEntry where T : IMediaFile
{
public CPlaylistEntry() { }
public T MediaFile { get; set; }
}
There are multiple implementations of IMediaFile, I am showing only one. My PlaylistEntry class takes a generic argument to enable different traits for those various implementations, and I just work with the IPlaylistEntry.
So I've started to model it like so:
var mediaFile = _modelBuilder.Entity<CMediaFile>();
mediaFile.Key(e => e.Uri);
mediaFile.Index(e => e.Uri);
mediaFile.Property(e => e.Title).MaxLength(256).Required();
var mediaFilePlaylistEntry = _modelBuilder.Entity<CPlaylistEntry<CMediaFile>>();
mediaFilePlaylistEntry.Key(e => e.MediaFile);
mediaFilePlaylistEntry.Reference(e => e.MediaFile).InverseReference();
As a simple test, I ignore the CPlaylistEntry<> and just do:
dbContext.Set<CMediaFile>().Add(new CMediaFile() { Uri = "irrelevant", Title = "" });
dbContext.SaveChanges()
This throws:
NotSupportedException: The 'MediaFile' on entity type 'CPlaylistEntry' does not have a value set and no value generator is available for properties of type 'CMediaFile'. Either set a value for the property before adding the entity or configure a value generator for properties of type 'CMediaFile'`
I don't even understand this exception, and I don't see why CPlaylistEntry is appearing when I'm only trying to store a CMediaFile entity. I'm guessing this is related to my model definition - specifically defining the primary key of the CPlaylistEntry as not a simple type, but a complex type - another entity. However I would expect EF to be smart enough to work out that it all boils down to a string Uri, because that complex type has its own primary key declared already, and I have declared the property as a foreign key to that type.
Is it possible to model these classes in EF without radically redesigning them to look closer to what corresponding database tables might be? I've worked with EF6 database-first in the past, so this is my first attempt into a code-first pattern, and I'm really hoping that I can isolate the mess that a database might look like to just my model definition, and keep "clean" classes that I interact with in .NET.
If more explanation of these types and their relationship is required, just ask - I'm attempting to keep this brief.
Doubt this is currently supported (unsure if it eventually will or not).| I've tried to recreate your model with slight changes and when trying to create the database I get:
System.NotSupportedException: The property 'PlaylistEntry`1MediaFile'
cannot be mapped because it is of type 'MediaFile' which is currently
not supported.
Update 1
I think that the fact that you are putting MediaFile as a key is creating problems. I've done a few changes to your model. I hope this will not break anything negative on your end:
public interface IPlaylistEntry<T>
where T : IMediaFile
{
T MediaFile { get; set; }
}
public class PlaylistEntry<T> : IPlaylistEntry<T>
where T : IMediaFile
{
public int Id { get; set; }
public string PlaylistInfo { get; set; } //added for testing purposes
public T MediaFile { get; set; }
}
Mappings:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.ForSqlServer().UseIdentity();
builder.Entity<MediaFile>().ForRelational().Table("MediaFiles");
builder.Entity<MediaFile>().Key(e => e.Uri);
builder.Entity<MediaFile>().Index(e => e.Uri);
builder.Entity<MediaFile>().Property(e => e.Title).MaxLength(256).Required();
builder.Entity<PlaylistEntry<MediaFile>>().ForRelational().Table("MediaFileEntries");
builder.Entity<PlaylistEntry<MediaFile>>().Key(e => e.Id);
builder.Entity<PlaylistEntry<MediaFile>>().Reference(e => e.MediaFile).InverseReference();
}
Usage:
var mediaFile = new MediaFile() {Uri = "irrelevant", Title = ""};
context.Set<MediaFile>().Add(mediaFile);
context.SaveChanges();
context.Set<PlaylistEntry<MediaFile>>().Add(new PlaylistEntry<MediaFile>
{
MediaFile = mediaFile,
PlaylistInfo = "test"
});
context.SaveChanges();
This works and saves the correct data to the database.
You can retrieve the data using:
var playlistEntryFromDb = context.Set<PlaylistEntry<MediaFile>>()
.Include(plemf => plemf.MediaFile).ToList();
Update 2
Since you do not want to have an identity as key, you can add a Uri property to your playlistentry class that will be used for the relationship between PlaylistEntry and MediaFile.
public class PlaylistEntry<T> : IPlaylistEntry<T>
where T : IMediaFile
{
public string Uri { get; set; }
public string PlaylistInfo { get; set; }
public T MediaFile { get; set; }
}
Here is what the mapping in this case would look like:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<MediaFile>().ForRelational().Table("MediaFiles");
builder.Entity<MediaFile>().Key(e => e.Uri);
builder.Entity<MediaFile>().Index(e => e.Uri);
builder.Entity<MediaFile>().Property(e => e.Title).MaxLength(256).Required();
builder.Entity<PlaylistEntry<MediaFile>>().ForRelational().Table("MediaFileEntries");
builder.Entity<PlaylistEntry<MediaFile>>().Key(e => e.Uri);
builder.Entity<PlaylistEntry<MediaFile>>().Reference(e => e.MediaFile).InverseReference().ForeignKey<PlaylistEntry<MediaFile>>(e => e.Uri);
}
Usage to insert data stays the same:
var mediaFile = new MediaFile() { Uri = "irrelevant", Title = "" };
context.Set<MediaFile>().Add(mediaFile);
context.SaveChanges();
context.Set<PlaylistEntry<MediaFile>>().Add(new PlaylistEntry<MediaFile>
{
MediaFile = mediaFile,
PlaylistInfo = "test"
});
context.SaveChanges();
This code above will put "irrelevant" in the PlaylistEntry Uri property since it is used as the foreign key.
And to retrieve data:
var mediaFiles = context.Set<PlaylistEntry<MediaFile>>().Include(x => x.MediaFile).ToList();
The join will occur on the Uri field in both tables.

Categories