Allow automapper to set an array property to an empty array when the source array is null on a single configured map - automapper

This is a follow up to a previous question:
Automapper sets an array property to a zero-length array rather than null
private class Fiz
{
public string Str { get; set; }
public string[] StrArr { get; set; }
public object[] ObjArr { get; set; }
}
private class Baz
{
public string Str { get; set; }
public string[] StrArr { get; set; }
public object[] ObjArr { get; set; }
}
[TestMethod]
public void ShouldAllowMapArrayToNull()
{
var config = new MapperConfiguration(cfg =>
{
cfg.AllowNullDestinationValues = true;
cfg.CreateMap<Fiz, Baz>().ForMember(b => b.Str, opt => opt.Ignore())
//.ForMember(b => b.StrArr, opt => opt.MapFrom(f => f.StrArr))
//.ForMember(b => b.ObjArr, opt => opt.MapFrom(f => f.ObjArr))
.ForMember(b => b.StrArr, opt => opt.ResolveUsing(f => f.StrArr))
.ForMember(b => b.ObjArr, opt => opt.ResolveUsing(f => f.ObjArr))
});
var mapper = config.CreateMapper();
var fiz = new Fiz
{
Str = null,
StrArr = null,
ObjArr = null
};
var baz = mapper.Map<Fiz, Baz>(fiz);
Assert.AreEqual(null, baz.Str,"string mapping to null");
Assert.AreEqual(null, baz.StrArr,"string arr mapping to null");
Assert.AreEqual(null, baz.ObjArr,"object arr mapping to null");
}
In the example above, baz is as follows: baz.Str == null, baz.StrArr == string[0], baz.ObjArr == object[0]
Despite using the suggested solutions in the original question, there seems to be no way of getting a mapping of the destination array to null, despite the source arrays being null themselves.
Is there a way to solve this (i.e. allow actual mapping of dest parameters to null), or is this a known limitation?
Edit: cfg.AllowNullCollections = true;
Solves this problem (thanks to MosheG), however this will apply to ALL the maps created under the mapper configuration. Is there a way to allow null instead of an empty array on a SINGLE created map?

It's by design.
You can change the default if you wish:
var configuration = new MapperConfiguration(cfg => {
cfg.AllowNullCollections = true;
cfg.CreateMap<Source, Destination>();
});
Or change it for one mapping (though, I didn't try this one).
https://docs.automapper.org/en/stable/Lists-and-arrays.html#handling-null-collections

Related

Problems mapping a type that inherits from IEnumerable

I have a problem mapping a property containing a custom list that inherits from IEnumerable (if i remove that inheritance, this example works). I have simplified the problem into this model:
public interface IMyEnumerable<T> : IEnumerable<T> { }
public class MyIEnumerable<T> : IMyEnumerable<T>
{
private readonly IEnumerable<T> _items;
public MyIEnumerable(IEnumerable<T> items)
{
_items = items;
}
public IEnumerator<T> GetEnumerator()
{
return _items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public class Source
{
public List<SourceItem> Items { get; set; }
}
public class Destination
{
public IMyEnumerable<DestinationItem> Items { get; set; }
}
public class SourceItem
{
public string Name { get; set; }
}
public class DestinationItem
{
public string Name { get; set; }
}
Then i try to use is this way:
public class MyResolver : ValueResolver<Source, IMyEnumerable<DestinationItem>>
{
protected override IMyEnumerable<DestinationItem> ResolveCore(Source source)
{
var destinationItems = Mapper.Map<List<SourceItem>, IEnumerable<DestinationItem>>(source.Items);
return new MyIEnumerable<DestinationItem>(destinationItems);
}
}
// Mappings
Mapper.CreateMap<Source, Destination>()
.ForMember(x => x.Items, m => m.ResolveUsing<MyResolver>());
Mapper.CreateMap<SourceItem, DestinationItem>();
// Using the mappings
var source = // not really relevant
var destination = Mapper.Map<Destination>(source);
This gives me the following exception (slightly edited for readability):
Mapping types:
MyIEnumerable`1 -> IMyEnumerable`1
MyIEnumerable`1[[DestinationItem]] -> IMyEnumerable`1[[DestinationItem]]
Destination path:
Destination.Items.Items
Source value:
MyIEnumerable`1[DestinationItem]
----> System.ArgumentException : Object of type System.Collections.Generic.List`1[DestinationItem] cannot be converted to type IMyEnumerable`1[DestinationItem].
Any idea how i can fix the mapping so that i can get this to work?
Assuming the following:
var source = new Source
{
Items = new List<SourceItem>
{
new SourceItem { Name = "foo" },
new SourceItem { Name = "bar" },
new SourceItem { Name = "cow" },
}
};
Then the following work:
// Method 1: Straight up mapping the collections:
Mapper.CreateMap<List<SourceItem>, IMyEnumerable<DestinationItem>>()
.ConstructUsing(list => new MyEnumerable<DestinationItem>(list.ConvertAll(Mapper.Map<SourceItem, DestinationItem>)));
// Method 2: Ignore the property and do it ourselves after the rest of the mapping:
Mapper.CreateMap<Source, Destination>()
.ForMember(q => q.Items, r => r.Ignore())
.AfterMap((s, d) => d.Items = new MyEnumerable<DestinationItem>(
s.Items.Select(Mapper.Map<SourceItem, DestinationItem>)));
Nothing else seems to work due to some combination of covariance and contravariance between List<T>, IEnumerable<T> and IMyEnumerable<T>

Restore < AutoMapper 3.1 null -> empty array behaviour

With AutoMapper <= 3.0, this following test passes.
public class AutoMapperTest
{
static Source source;
static Destination destination;
Establish context = () =>
{
Mapper.Configuration.AllowNullCollections = false;
Mapper.CreateMap<Source, Destination>();
source = new Source { Name = null, Data = null };
};
Because of = () => destination = Mapper.Map<Destination>(source);
It should_map_name_to_null = () => destination.Name.ShouldBeNull();
It should_map_array_to_empty = () => destination.Data.ShouldNotBeNull();
}
public class Source
{
public string Name { get; set; }
public string[] Data { get; set; }
}
public class Destination
{
public string Name { get; set; }
public string[] Data { get; set; }
}
As of version 3.1, the should_map_array_to_empty assertion fails because destination.Data is set to null and not an empty array as previously. Is there a way to restore the previous behaviour, preferably globally as opposed to individually per configured map?
The configuration option Mapper.Configuration.AllowNullCollections = false appears to make no difference in this case regardless of the versions of AutoMapper I've tried.

AutoMapper - Mapping Complex Lists and Dictionaries of System.Object

I have the following classes and I need to map an Agent to an AgentDto.
public class ObjectsAddedToCollectionProperties : Dictionary<string, ObjectList> {}
public class OriginalValuesDictionary : Dictionary<string, Object> {}
public class ObjectChangeTracker()
{
public ObjectsAddedToCollectionProperties ObjectsAddedToCollectionProperties { get; set; }
public OriginalValuesDictionary OriginalValues { get; set; }
}
public class BaseDto
{
public ObjectChangeTracker ChangeTracker { get; set; }
}
public class RolesDto : BaseDto {}
public class AgentDto : BaseDto
{
ICollection<RoleDto> Roles { get; set; }
}
public class ModelBase
{
public ObjectChangeTracker ChangeTracker { get; set; }
}
public class Role : ModelBase {}
public class Agent : ModelBase
{
ICollection<Role> Roles { get; set; }
}
And this is my mapper configuration
Mapper.CreateMap<Agent, AgentDto>().IgnoreAllNonExisitng();
Mapper.CreateMap<Role, RoleDto>().IgnoreAllNonExisitng();
var agent = new Agent();
Mapper.Map<AgentDto>(agent);
When I map Agent to AgentDto Roles are correctly mapped but anything I add to one of the Dictionaries isn't mapped. I assume this is because AutoMapper has no way of telling what the destination type should be and because the destination type is an Object it infers that it can simply copy the source object.
I've created a custom resolver but for some reason it isn't being called
class ChangeTrackerResolver : ValueResolver<ObjectChangeTracker, ObjectChangeTracker>
{
protected override ObjectChangeTracker ResolveCore(ObjectChangeTracker source)
{
var typeMap = Mapper.GetAllTypeMaps();
var destination = new ObjectChangeTracker();
foreach (var keyValuePairs in source.ObjectsAddedToCollectionProperties)
{
var objectList = new ObjectList();
objectList.AddRange(from value in keyValuePairs.Value
let sourceType = value.GetType()
let destinationType = typeMap.Where(map => map.SourceType == sourceType).Single().DestinationType
select Mapper.Map(value, sourceType, destinationType));
destination.ObjectsAddedToCollectionProperties.Add(keyValuePairs.Key, objectList);
}
foreach (var keyValuePairs in source.ObjectsRemovedFromCollectionProperties)
{
var objectList = new ObjectList();
objectList.AddRange(from value in keyValuePairs.Value
let sourceType = value.GetType()
let destinationType = typeMap.Where(map => map.SourceType == sourceType).Single().DestinationType
select Mapper.Map(value, sourceType, destinationType));
destination.ObjectsRemovedFromCollectionProperties.Add(keyValuePairs.Key, objectList);
}
foreach (var keyValuePairs in source.OriginalValues)
{
var sourceType = keyValuePairs.Value.GetType();
var destinationType = typeMap.Where(map => map.SourceType == sourceType).Single().DestinationType;
destination.OriginalValues.Add(keyValuePairs.Key, Mapper.Map(keyValuePairs.Value, sourceType, destinationType));
}
foreach (var keyValuePairs in source.ExtendedProperties)
{
var sourceType = keyValuePairs.Value.GetType();
var destinationType = typeMap.Where(map => map.SourceType == sourceType).Single().DestinationType;
destination.ExtendedProperties.Add(keyValuePairs.Key, Mapper.Map(keyValuePairs.Value, sourceType, destinationType));
}
return destination;
}
}
And this is my mapping
Mapper.CreateMap<ModelBase, BaseDto>()
.Include<Agent, AgentDto>()
.Include<Address, AddressDto>()
.Include<BankAccount, BankAccountDto>()
.Include<Client, ClientDto>()
.Include<Person, PersonDto>()
.Include<Role, RoleDto>()
.Include<TelephoneNumber, TelephoneNumberDto>()
.Include<Height, HeightDto>()
.Include<Weight, WeightDto>()
.IgnoreAllNonExisitng().ForMember(
a => a.ChangeTracker,
opt => opt.ResolveUsing<ChangeTrackerResolver>());
EDIT
I've fixed the problem with my mapping and my Resolver is now getting called but objects in the Dictionaries e.g ObjectsAddedToCollectionProperties are not present in the ChangeTracker passed to the Resolver. Here is my new mapping.
Mapper.CreateMap<Agent, AgentDto>()
.IgnoreAllNonExisitng().ForMember(
a => a.ChangeTracker,
opt => opt.ResolveUsing<ChangeTrackerResolver>().FromMember(a => a.ChangeTracker));

What expression is needed for mapping this property?

I am trying to map two objects that are mostly similar with AutoMapper but one member (AudioSummary) raises the following exception :
The following property on EchoNestModel.AudioSummary cannot be mapped: AudioSummary
Add a custom mapping expression, ignore, add a custom resolver, or modify the destination type EchoNestModel.AudioSummary.
Context:
- Mapping to property AudioSummary from EchoNest.Api.AudioSummary to EchoNestModel.AudioSummary
- Mapping from type EchoNest.Api.TrackProfile to EchoNestModel.Profile
Exception of type 'AutoMapper.AutoMapperConfigurationException' was thrown.
Mapping definition
var map = Mapper.CreateMap<TrackProfile, Profile>();
map.ForMember(dest => dest.ForeignIds, opt => opt.ResolveUsing<ForeignIdResolver>());
map.ForMember(dest => dest.ForeignReleaseIds, opt => opt.ResolveUsing<ForeignReleaseIdResolver>());
map.ForMember(s => s.Media, t => t.Ignore());
map.ForMember(s => s.ProfileId, t => t.Ignore());
map.ForMember(s => s.AudioSummary, t => t.MapFrom(s => s.AudioSummary));
I've added the following two lines but a totally different error occurs :
map.ForMember(s => s.AudioSummary.Profile, t => t.Ignore());
map.ForMember(s => s.AudioSummary.AudioSummaryId, t => t.Ignore());
Expression 's => s.AudioSummary.Profile' 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.
Parameter name: lambdaExpression
How can I successfully map AudioSummary ?
Source object
Target object
EDIT: In general, try AutoMapper.Mapper.AssertConfigurationIsValid();, this will show you all possible problems in your mapper setup.
From the information you provided, it looks like you need to define map for the AudioSummary classes (dest and source) as well:
[TestFixture]
public class MappingTest
{
public class SourceAudioSummary
{
public int Id { get; set; }
public string OtherData { get; set; }
}
public class TrackProfile
{
public string Whatever { get; set; }
public SourceAudioSummary AudioSummary { get; set; }
}
public class DestAudioSummary
{
public int Id { get; set; }
public string OtherData { get; set; }
}
public class Profile
{
public string Whatever { get; set; }
public DestAudioSummary AudioSummary { get; set; }
}
[Test]
public void Mapping()
{
Mapper.CreateMap<SourceAudioSummary, DestAudioSummary>();
Mapper.CreateMap<TrackProfile, Profile>();
var trackProfile = new TrackProfile
{
Whatever = "something",
AudioSummary = new SourceAudioSummary
{
Id = 1,
OtherData = "other"
}
};
var profile = Mapper.Map<TrackProfile, Profile>(trackProfile);
Assert.That(profile.Whatever == "something");
Assert.That(profile.AudioSummary.Id == 1);
Assert.That(profile.AudioSummary.OtherData == "other");
}
}

Can AutoMapper implicitly flatten this mapping?

I am trying to map between two lists of objects. The source type has a complex property of type A; the destination type is a flattened subset of type A plus an additional scalar property that is in the source type.
public class A
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Source
{
public A MyA { get; set; }
public int SomeOtherValue { get; set; }
}
public class Destination
{
public string Name { get; set; }
public int SomeOtherValue { get; set; }
}
If it's not clear, I'd like Source.MyA.Name to map to Destination.Name and Source.SomeOtherValue to map to Destination.SomeOtherValue.
In reality, type A has a dozen or so properties, about which 80% map over to properties of the same name in Destination. I can get things to work if I explicitly spell out the mappings in CreateMap like so:
CreateMap<Source, Destination>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.MyA.Name));
The downside here is I want to avoid having to add a ForMember line for each of A's properties that need to get copied over to Destination. I was hoping I could do something like:
CreateMap<Source, Destination>()
.ForMember(dest => dest, opt => opt.MapFrom(src => src.MyA));
But if I try the above I get a runtime error when the mapping is registered: "Custom configuration for members is only supported for top-level individual members on a type."
Thanks
create mappings between A and Destination, and Source and Destination, and then use AfterMap() to use first mapping in second
Mapper.CreateMap<A, Destination>();
Mapper.CreateMap<Source, Destination>()
.AfterMap((s, d) => Mapper.Map<A, Destination>(s.MyA, d));
then use it like this:
var res = Mapper.Map<Source, Destination>(new Source { SomeOtherValue = 7, MyA = new A { Id = 1, Name = "SomeName" } });
As a workaround you can use custom type converter with additional property in the destination type to avoid recursion.
[TestFixture]
public class MapComplexType
{
[Test]
public void Map()
{
Mapper.CreateMap<A, Destination>();
Mapper.CreateMap<Source, Destination>().ConvertUsing(new TypeConvertor());
var source = new Source
{
MyA = new A
{
Name = "Name"
},
SomeOtherValue = 5
};
var dest = new Destination();
Mapper.Map(source, dest);
Assert.AreEqual(dest.Name, "Name");
}
}
public class TypeConvertor : ITypeConverter<Source, Destination>
{
public Destination Convert(ResolutionContext context)
{
var destination = (Destination) context.DestinationValue;
if (!((Destination)context.DestinationValue).IsMapped || destination == null)
{
destination = destination ?? new Destination();
destination.IsMapped = true; // To avoid recursion
Mapper.Map((Source)context.SourceValue, destination);
destination.IsMapped = false; // If you want to map the same object few times
}
Mapper.Map(((Source)context.SourceValue).MyA, destination);
return (Destination)context.DestinationValue;
}
}
public class A
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Source
{
public A MyA { get; set; }
public int SomeOtherValue { get; set; }
}
public class Destination
{
public string Name { get; set; }
public int SomeOtherValue { get; set; }
// Used only for mapping purposes
internal bool IsMapped { get; set; }
}
Try this,
Mapper.CreateMap<A, Destination>();
Mapper.CreateMap<Source, Destination>()
.ForMember(destination => destination.Name, options => options.MapFrom(source => Mapper.Map<A, Destination>(source.MyA).Name));
var objSource = new Source { SomeOtherValue = 7, MyA = new A { Id = 1, Name = "SomeName" } };
var result = Mapper.Map<Source, Destination>(objSource);

Resources