Restore < AutoMapper 3.1 null -> empty array behaviour - automapper

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.

Related

Mapping null source values into destination

When mapping a source instance to a destination instance, how are null source values handled? It appears that with built in types, a null source value will overwrite a destination value (set it to null). However with a navigation property, a destination value will not be set to null by a null source value, e.g. OuterDest.Innter:
`
public class OuterSource
{
public int Value { get; set; }
public InnerSource? Inner { get; set; }
}
public class InnerSource
{
public int OtherValue { get; set; }
}
public class OuterDest
{
public int Value { get; set; }
public InnerDest? Inner { get; set; }
}
public class InnerDest
{
public int OtherValue { get; set; }
}
`
This test will fail
[TestMethod]
public void NestedTestNullSourceValue()
{
var mappingConfig = new MapperConfiguration(cfg =>
{
cfg.CreateMap<OuterSource, OuterDest>();
cfg.CreateMap<InnerSource, InnerDest>();
});
var mapper = mappingConfig.CreateMapper();
OuterSource source = new()
{
Value = 123
};
OuterDest dest = new()
{
Value = 888,
Inner = new()
{
OtherValue = 999
}
};
mapper.Map(source, dest);
Assert.AreEqual(123, dest.Value);
Assert.IsNull(dest.Inner);
}
Had the same issue (AutoMapper - Map(source, destination) overwrite destination child object with null value from source via configuration), updating to AutoMapper 12.0.1 pre-release solved my issue

IncludeMembers of Automapper not works as expected

According to automapper docs, I can map nested objects to destination using IncludeMembers function. I have issues with next sample.
Code is available on net fiddle, below is quick reference:
How I map:
var source = new CategoryStatus
{
Subgroup = new CategorySubgroup
{
SubgroupCode = "SubgroupCode",
CategoryGroup = new CategoryGroup { GroupCode = "SubgroupCode" }
}
};
var result = Mapper.Map<Dest, CategoryStatus>(source);
My classes:
public class Dest
{
public string SubgroupCode { get; set; }
public string GroupCode { get; set; }
}
public class CategoryStatus
{
public CategorySubgroup Subgroup { get; set; }
}
public class CategorySubgroup
{
public string SubgroupCode { get; set; }
public CategoryGroup CategoryGroup { get; set; }
}
public class CategoryGroup
{
public string GroupCode { get; set; }
}
My Configuration:
var cfg2 = new MapperConfiguration(cfg => {
cfg.CreateMap<CategoryStatus, Dest>()
.IncludeMembers(x => x.Subgroup);
cfg.CreateMap<CategoryGroup, Dest>();
cfg.CreateMap<CategorySubgroup, Dest>()
.IncludeMembers(x => x.CategoryGroup);
});
Error:
[System.ArgumentException: Property 'System.String GroupCode' is not defined for type 'CategorySubgroup']
Any ideas about configuration setup? Automapper version is 10.0.0
Update
Version 9.0.0 works. Possible latest will also work, but it contains some breaking changes for me, so I didn't test it.

Issue merging objects with AutoMapper

I found this post describing how to conditionally copy values over a destination object if they are not null.
Works great except for list members, it always overwrites them with an empty list. I'm not sure if I've just not configured the mapper correctly or if this is a bug. The following program demonstrates the issue.
namespace automapper_test
{
using AutoMapper;
using System;
using System.Collections.Generic;
class Program
{
class Test
{
public int? A { get; set; }
public string B { get; set; }
public Guid? C { get; set; }
public List<Guid> D { get; set; }
}
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg =>
{
cfg.AllowNullCollections = true;
cfg.CreateMap<Test, Test>().ForAllMembers(opt => opt.Condition((src, dest, member) => member != null));
});
var mapper = config.CreateMapper();
var source = new Test { A = 2, C = Guid.Empty };
var target = new Test { A = 1, B = "hello", C = Guid.NewGuid(), D = new List<Guid> { Guid.NewGuid() } };
mapper.Map(source, target);
System.Diagnostics.Debug.Assert(target.D.Count == 1);
}
}
}

Generic Automapper function with custom convension for underscored properties

I simply need to map some auto generated classes from database to domain/viewmodels classes. The autogenerated class may have names like customer_id that I want to be mapped with CustomerId. Somehow I want to register my own convention with auto mapper. I have started with following code however the mapped object properties are not populated:
// Generic method that should map source to target
public static TTarget MapWithUnderScoreConvension(TSource source, TTarget target)
{
Mapper.Initialize(cfg=> cfg.AddProfile<AutoMapperUnderScoreProfile>());
Mapper.CreateMap<TSource, TTarget>();
var mapped = Mapper.Map(source, target);
return mapped;
}
// New underscore profile
public class AutoMapperUnderScoreProfile : Profile
{
public AutoMapperUnderScoreProfile()
{
Mapper.Initialize(configuration => configuration.CreateProfile("UnderScoreProfile", UnderScoreProfile));
Mapper.AssertConfigurationIsValid();
}
private void UnderScoreProfile(IProfileExpression profile)
{
profile.SourceMemberNamingConvention = new PascalCaseNamingConvention();
profile.DestinationMemberNamingConvention = new SourceUnderScoreNamingConvension();
}
}
// Convention class
public class SourceUnderScoreNamingConvension : INamingConvention
{
private readonly string _separatorCharacter="_";
private readonly Regex _splittingExpression = new Regex(#"[\p{Lu}0-9]+(?=_?)");
public Regex SplittingExpression { get { return _splittingExpression;} private set{} }
public string SeparatorCharacter { get { return _separatorCharacter; } private set{} }
}
// Test cases
[TestMethod()]
public void Test_Map_Db_Generated_Class_To_Model()
{
var dbGenerated = new QuestionTypeFromDb()
{
QuestionType_Description = "1",
QuestionType_Id = 1,
QuestionType_Is_Default = true,
QuestionType_Is_TimeBased = true,
QuestionType_Sequence = 1,
QuestionType_Time_In_Seconds = 1
};
var mappedObject = AutoMapperHelper<QuestionTypeFromDb, QuestionType>
.MapWithUnderScoreConvension(dbGenerated, new QuestionType());
mappedObject.QuestionTypeId.Should().Be(dbGenerated.QuestionType_Id);
mappedObject.QuestionTypeDescription.Should().Be(dbGenerated.QuestionType_Description);
mappedObject.TimeInSeconds.Should().Be(dbGenerated.QuestionType_Time_In_Seconds);
mappedObject.QuestionTypeSequence.Should().Be(dbGenerated.QuestionType_Sequence);
}
public class TestQuestionWithAnswerType
{
public int QuestionTypeId { get; set; }
public string QuestionTypeDescription { get; set; }
public int QuestionTypeSequence { get; set; }
public bool QuestionTypeIsTimeBased { get; set; }
public int? QuestionTypeTimeInSeconds { get; set; }
public bool QuestionTypeIsDefault { get; set; }
}
any comments will be appreciated.
Update
I have found that the following workaround works:
I simply replaced used this -> to replace 'underscore' with nothing (Mapper.Initialize(c => c.ReplaceMemberName("_", ""));
public static TTarget MapWithUnderScoreConvension(TSource source, TTarget target)
{
Mapper.Initialize(c => c.ReplaceMemberName("_", ""));
//Mapper.Initialize(cfg => cfg.AddProfile<AutoMapperUnderScoreProfile>());
Mapper.CreateMap<TSource, TTarget>();
var mapped = Mapper.Map(source, target);
return mapped;
}
Your regex needs to be changed to : [\p{L}}0-9]+(?=_?)
This will take care of Customer_Id, CUSTOMER_ID, customer_id
The {L} unicode category includes Lu, Lt, Ll, Lm and Lo characters as mentioned here.
Answer is added in the Update section of the question. Basically the solution for me was very simple -> Mapper.Initialize(c => c.ReplaceMemberName("_", ""));

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