检测DTO和实体丢失或配置错误的属性

时间:2017-01-24 08:58:12

标签: c# unit-testing automapper

我们有一个非常常见的场景,我们使用Automapper来映射DTO和实体。正如您所期望的那样,在这两个类中,很多属性都是1 = 1,但有一些例外。

随着类和属性数量的增加,有时开发人员在重命名或删除属性时忘记保持属性同步。

您能否建议我们如何可靠地检测"未映射"属性,最好是自动?

2 个答案:

答案 0 :(得分:3)

对于这样的问题,我建议使用GetUnmappedPropertyNames本身的IMapper方法。代码和测试应该解释下面的想法:

条件     (z.PropertyType.IsValueType || z.PropertyType.IsArray || z.PropertyType == typeof(string))

将从值类型中检测未映射的属性,如int,enum,Guid,DateTime,所有Nullable值类型bool?,Decimal?,Guid?和string。

这样的过滤器让你的测试忽略了实体导航属性的映射:

public virtual Class NavigationProperty {get;set} 
public virtual IList<Class> CollectionNavigationProperty { get; set; }

代码和测试:

[Test]
public void Mapping_Profile_Must_Not_Have_Unmapped_Properties()
{
    var config = new MapperConfiguration(cfg =>
    {
        cfg.AddProfile<TestProfile>();
    });
    var mapper = config.CreateMapper();

    var unmappedProperties = GetUnmappedSimpleProperties(mapper);
    Assert.AreEqual(unmappedProperties.Count, 0);
}

private List<UnmappedProperty> GetUnmappedSimpleProperties(IMapper mapper)
{
    return mapper.ConfigurationProvider.GetAllTypeMaps()
        .SelectMany(m => m.GetUnmappedPropertyNames()
        .Where(x =>
        {
            var z = m.DestinationType.GetProperty(x);
            return z != null && (z.PropertyType.IsValueType || z.PropertyType.IsArray || z.PropertyType == typeof(string));
        })

        .Select(n => new UnmappedProperty
        {
            DestinationTypeName = m.DestinationType.Name,
            PropertyName = n,
            SourceTypeName = m.SourceType.Name
        })).ToList();
}

internal class UnmappedProperty
{
    public string PropertyName { get; set; }
    public string DestinationTypeName { get; set; }
    public string SourceTypeName { get; set; }

    public override string ToString()
    {
        return $"{this.PropertyName}: {this.SourceTypeName}->{this.DestinationTypeName}";
    }
}

在您的服务中进行测试:

  [Test]
    public void Test_Mapping_Profile_Must_Detect_Unmapped_Properties()
    {
        var config = new MapperConfiguration(cfg =>
        {
                cfg.AddProfile<TestMappingProfile>();
        });
        ar mapper = config.CreateMapper();

        var unmappedProperties = GetUnmappedSimpleProperties();
        Assert.AreEqual(unmappedProperties.Count, 12);
    }

    public class TestMappingProfile : Profile
    {
        public TestMappingProfile()
        {
            CreateMap<Source, DestinationValid>();
            CreateMap<Source, DestinationInvalid>();
        }
    } 


    internal class Source
    {
        public string Test1 { get; set; }
        public int Test2 { get; set; }
        public int? Test3 { get; set; }
        public decimal Test4 { get; set; }
        public string[] Test5 { get; set; }

        public Guid Test6 { get; set; }
        public Guid? Test7 { get; set; }
        public TransactionRealm Test8 { get; set; }

        public bool? Test9 { get; set; }
        public bool Test10 { get; set; }

        public DateTime Test11 { get; set; }
        public DateTime? Test12 { get; set; }
    }

    internal class DestinationValid
    {
        public string Test1 { get; set; }
        public int Test2 { get; set; }
        public int? Test3 { get; set; }
        public decimal Test4 { get; set; }
        public string[] Test5 { get; set; }

        public Guid Test6 { get; set; }
        public Guid? Test7 { get; set; }
        public TransactionRealm Test8 { get; set; }

        public bool? Test9 { get; set; }
        public bool Test10 { get; set; }

        public DateTime Test11 { get; set; }
        public DateTime? Test12 { get; set; }
    }

    internal class DestinationInvalid
    {
        public string Test1X { get; set; }
        public int Test2X { get; set; }
        public int? Test3X { get; set; }
        public decimal Test4X { get; set; }
        public string[] Test5X { get; set; }
        public Guid Test6X { get; set; }
        public Guid? Test7X { get; set; }
        public TransactionRealm Test8X { get; set; }
        public bool? Test9X { get; set; }
        public bool Test10X { get; set; }

        public DateTime Test11X { get; set; }
        public DateTime? Test12X { get; set; }
    }

其中TransactionRealm是枚举的示例:

public enum TransactionRealm
{
    Undefined = 0,
    Transaction = 1,
    Fee = 2,
}

答案 1 :(得分:1)

还有MapperConfiguration.AssertConfigurationIsValid()方法的替代方法,可以在单元测试和运行时使用。 AssertConfigurationIsValid()方法抛出异常,详细描述了所有检测到的未映射属性。在业务逻辑中,我建议(为了更好的性能)使用自定义MapperFactory帮助程序在服务静态构造函数中初始化映射器:

public class MyBLL
{
    private static IMapper _mapper;
    static MyBLL()
    {
        _mapper = MapperFactory.CreateMapper<DtoToEntityDefaultProfile>();
    }
}

public static class MapperFactory
{
    public static IMapper CreateMapper<T>() where T : Profile, new()
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.AddProfile<T>();
        });

        /// AssertConfigurationIsValid will detect 
        /// all unmapped properties including f.e Navigation properties, Nested DTO classes etc.
        config.AssertConfigurationIsValid();

        config.CompileMappings();
        return config.CreateMapper();
    }

}