为什么AutoMapper没有正确格式化我的日期?

时间:2018-01-05 13:13:24

标签: c# date datetime automapper

我在MVC Core 2.0应用程序中配置了以下映射:

cfg.CreateMap<Person, PersonViewModel>(MemberList.None)
    .ForMember(dest => dest.BirthDate, opt => opt.ResolveUsing(src => src.BirthDate.ToString(AppConstants.DefaultDateFormat)));

cfg.CreateMap<PersonViewModel, Person>(MemberList.None)
    .ForMember(dest => dest.BirthDate,
        opt => opt.ResolveUsing(src => DateTime.ParseExact(src.BirthDate, AppConstants.DefaultDateFormat, CultureInfo.InvariantCulture)));

其中AppConstants.DefaultDateFormatyyyy-MM-ddPersonViewModel中的目标日期字符串显示为1969-12-13T00:00:00

src.BirthDate的类型为DateTime,不可为空,dest.BirthDate的类型为string。我特意将其设为string,以便我可以定义自定义映射来格式化日期。

为什么AutoMapper似乎忽略了我的自定义映射,只是在源日期执行默认ToString()?我是否以某种方式错误地进行自定义映射?

4 个答案:

答案 0 :(得分:2)

从解析器返回的内容会映射到最终目标值(在本例中为DateTime.ToString())。使用类型转换器或值转换器时不会发生这种情况。

答案 1 :(得分:2)

您的配置在与Map方法一起使用时有效,但在与ProjectTo一起使用时失败。

在EF Core 2查询上应用ProjectTo时,AutoMapper版本6.2.0产生上述行为,而新版本(6.2.0,6.2.1和6.2.2在编写本文时) )在执行结果查询时产生运行时异常。

无论AutoMapper版本的行为存在差异,主要问题是您不应将ResolveUsing用于此类场景,如方法说明末尾所示:

//
// Summary:
//     Resolve destination member using a custom value resolver callback. Used instead
//     of MapFrom when not simply redirecting a source member This method cannot be
//     used in conjunction with LINQ query projection
//

以上内容还包含解决方案,尤其适合您的方案。在所有情况和所有上述AutoMapper版本中,只需将ResolveUsing替换为MapFrom即可正常工作:

cfg.CreateMap<Person, PersonViewModel>(MemberList.None)
    .ForMember(dest => dest.BirthDate, opt => opt.MapFrom(src => src.BirthDate.ToString(AppConstants.DefaultDateFormat)));

答案 2 :(得分:2)

虽然最初的问题集中在映射器的问题上,但我想将重点转移到改进设计选择上。

借用DDD(域驱动设计)中的概念,考虑使用值对象来表示视图模型的BirthDate值。

我创建了一个基础对象来覆盖重复的功能

public abstract class ValueObject<T> where T : ValueObject<T> {

    public override bool Equals(object obj) {
        var valueObject = obj as T;

        if (ReferenceEquals(valueObject, null))
            return false;

        return EqualsCore(valueObject);
    }

    private bool EqualsCore(T other) {
        return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
    }

    protected abstract IEnumerable<object> GetEqualityComponents();

    public override int GetHashCode() {
        return GetEqualityComponents()
            .Aggregate(1, (current, obj) => current * 23 + (obj == null ? 0 : obj.GetHashCode()));
    }

    public static bool operator ==(ValueObject<T> left, ValueObject<T> right) {
        if (ReferenceEquals(left, null) && ReferenceEquals(right, null))
            return true;

        if (ReferenceEquals(left, null) || ReferenceEquals(right, null))
            return false;

        return left.Equals(right);
    }

    public static bool operator !=(ValueObject<T> left, ValueObject<T> right) {
        return !(left == right);
    }
}

然后按照所需的模式创建一个BirthDate值对象

public class BirthDate : ValueObject<BirthDate>, IEquatable<BirthDate> {
    DateTime value;

    public BirthDate(DateTime value) {
        this.value = value;
    }

    public static implicit operator BirthDate(DateTime dt) {
        return new BirthDate(dt);
    }

    public static implicit operator BirthDate(string src) {
        return DateTime.ParseExact(src, AppConstants.DefaultDateFormat, CultureInfo.InvariantCulture);
    }

    public static implicit operator DateTime(BirthDate dt) {
        return dt.value;
    }

    public static implicit operator String(BirthDate dt) {
        return dt.ToString();
    }

    public override string ToString() {
        return value.ToString(AppConstants.DefaultDateFormat);
    }

    public bool Equals(BirthDate other) {
        return DateTime.Equals(this.value, other.value);
    }

    protected override IEnumerable<object> GetEqualityComponents() {
        yield return value;
    }

}

请注意允许在所需类型之间转换值对象的隐式运算符。主要是DateTime并格式化string

视图模型将值对象作为属性

public class PersonViewModel {
    //...

    public BirthDate BirthDate { get; set; } <-- note the return type

    //...
}

以下单元测试用于在不同的值对象和所需类型之间进行转换,即使是通过映射器。

[TestClass]
public class BirthDateValueObjectTests {

    public class Person {
        public DateTime BirthDate { get; set; }
    }

    public class PersonViewModel {
        public BirthDate BirthDate { get; set; }
    }

    string date = "1981-05-14";

    static BirthDateValueObjectTests() {
        AutoMapper.Mapper.Initialize(_ => {

        });
    }

    [TestMethod]
    public void BirthDate_Should_Implcit_Convert_From_DateTimeProperty() {
        //Arrange
        var birthDate = DateTime.Parse(date);
        var person = new Person {
            BirthDate = birthDate
        };
        var expected = new BirthDate(birthDate);

        var mapper = AutoMapper.Mapper.Instance;

        //Act
        var actual = mapper.Map<PersonViewModel>(person);

        //Assert
        actual.BirthDate
            .Should().NotBeNull()
            .And.Be(expected);
    }

    [TestMethod]
    public void BirthDate_Should_Implcit_Convert_To_DateTimeProperty() {
        //Arrange
        var birthDate = DateTime.Parse(date);
        var person = new PersonViewModel {
            BirthDate = new BirthDate(birthDate)
        };
        var expected = birthDate;

        var mapper = AutoMapper.Mapper.Instance;

        //Act
        var actual = mapper.Map<Person>(person);

        //Assert
        actual.BirthDate
            .Should().Be(expected);
    }

    [TestMethod]
    public void BirthDate_Should_Implicitly_ConvertTo_DateTime() {
        var expected = DateTime.Parse(date);
        var birthDate = new BirthDate(expected);

        DateTime actual = birthDate;

        actual.Should().Be(expected);
    }

    [TestMethod]
    public void BirthDate_Should_Implicitly_ConvertFrom_DateTime() {
        var birthDate = DateTime.Parse(date);
        var expected = new BirthDate(birthDate);

        BirthDate actual = birthDate;

        actual.Should().Be(expected);
    }

    [TestMethod]
    public void BirthDate_Should_Implicitly_ConvertTo_String() {
        var expected = DateTime.Parse(date);
        var birthDate = new BirthDate(expected);

        string actual = birthDate;

        actual.Should().Be(date);
    }

    [TestMethod]
    public void BirthDate_Should_Implicitly_ConvertFrom_String() {
        var birthDate = DateTime.Parse(date);
        var expected = new BirthDate(birthDate);

        BirthDate actual = date;

        actual.Should().Be(expected);
    }

}

现在映射器可以通过直接映射进行转换,视图模型的BirthDate属性可以在视图中使用所需的格式,因为映射器配置中正在执行的实现问题已被封装在唯一负责出生日期的价值对象中。

虽然这个答案针对的是出生日期,但可以重命名值对象,并更普遍地用于其他日期时间属性,以减少重复代码(DRY)。

答案 3 :(得分:1)

我刚刚使用Automapper 4.1.1和6.2.2进行了检查,它的工作原理就像您打算一样。您的映射转换绝对正确,也许您还有其他错误。 AppConstants.DefaultDateFormat是空的?或配置未执行等