Strict type validation of properties in Automapper

Viewed 1288

Is it possible to set some kind of global configuration that would cause mapping or validation process fail when some properties of mapped objects have same name, but have different types?

Source/destination types

public class UserData1
{
    public int Id { get; set; }
}

public class UserData2
{
    public string Id { get; set; }
}

Mapping configuration

Mapper.Initialize(cfg =>
{
    cfg.CreateMap<UserData1, UserData2>();
});

Expected behavior

Either AssertConfigurationIsValid or Map should trigger some kind of validation exception when types of mapping properties are not same.

Actual behavior

int property is mapped to string without exception.

Steps to reproduce

// Passes OK
 Mapper.Configuration.AssertConfigurationIsValid();

 // Mapping is successful
 var user2 = Mapper.Map<UserData2>(new UserData1 { Id = 156 });
2 Answers

There is no build in solution available, but it can be achieved using custom configuration.

Something like this:

Mapper.Initialize(cfg => {
    cfg.CreateMap<UserData1, UserData2>();
    ...
    cfg.ForAllMaps((typeMap, mappingExpr) =>
    {
        foreach (var map in typeMap.PropertyMaps) {
            var sourcePropInfo = map.SourceMember as PropertyInfo;
            var destPropInfo = map.DestinationMember as PropertyInfo;

            if (sourcePropInfo == null || destPropInfo == null) continue;

            if (sourcePropInfo.PropertyType != destPropInfo.PropertyType)
                throw new Exception("Wrong property type!");
        }
    });
});

As reference was used this old post and updated to work with new version of automapper https://stackoverflow.com/a/38080647/1703620

Using Artyom's answer as inspiration, I wrote a unit test to constrain these implicit type conversions to the few scenarios that we're okay with.

The approach is to look up all property mappings that don't have an explicit mapping, and filter out the few scenarios that we want to allow.

an XUnit test:

        [Fact]
        public void AllMappedPropertiesAreSameTypeOrAreMappedExplicitly()
        {
            ServiceCollection theCollection = new ServiceCollection();

            theCollection.AddMssAutoMapper();

            IMapper theMapper = BuildProductionMapper();

            //Store all explicit mappings for easy lookup
            System.Collections.Generic.HashSet<(Type SourceType, Type DestType)> theExplicitMappings =
                theMapper.ConfigurationProvider.GetAllTypeMaps()
                .Select( map => (map.SourceType, map.DestinationType) )
                .ToHashSet();

            var theIllegalMaps =
            from typeMap in theMapper.ConfigurationProvider.GetAllTypeMaps()
            from propMap in typeMap.PropertyMaps
            let sourceType = propMap.SourceType
            let destType = propMap.DestinationType
            let bothTypes = new[] { sourceType, destType }
            where sourceType != null && destType != null
            where sourceType != destType

            //Anything that's explicitly mapped is permitted
            where !theExplicitMappings.Contains( (sourceType, destType) )

            //enums to string and vice versa is permitted
            where !( sourceType.IsEnum || sourceType == typeof( string ) && destType.IsEnum || destType == typeof( string ) )

            //mapping from one collection to another is okay
            where !bothTypes.All( type => type.IsAssignableTo( typeof( IEnumerable ) ) )
            select new
            {
                SourceType = typeMap.SourceType,
                DestinationType = typeMap.DestinationType,
                SourceMemberName = propMap.SourceMember.Name,
                DestMemberName = propMap.DestinationMember.Name,
                SourceMemberType = sourceType,
                DestinationMemberType = destType
            };
            var illegalMapsList = theIllegalMaps.ToList();
            foreach( var illegalMap in illegalMapsList )
            {
                Console.Out.WriteLine( $"Found disallowed property mapping from '{illegalMap.SourceType}.{illegalMap.SourceMemberName}' to '{illegalMap.DestinationType}.{illegalMap.DestMemberName}'" );
                Console.Out.WriteLine( $"Property name: {illegalMap.SourceMemberName}" );
                Console.Out.WriteLine( $"implicit mapping from {illegalMap.SourceMemberType} to {illegalMap.DestinationMemberType} is not allowed." );
                Console.Out.WriteLine( $"Please map these types explicitly." );
            }

            if( illegalMapsList.Any() )
            {
                throw new Exception( "One or more ambiguous mappings were found that need to be handled explicitly.  See console output for details." );
            }
        }
Related