【问题标题】:Why does AutoMapper not format my date properly?为什么 AutoMapper 不能正确格式化我的日期?
【发布时间】:2018-01-05 13:13:24
【问题描述】:

我在 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.BirthDateDateTime 类型,不可为空,dest.BirthDatestring 类型。我特地把它设为string,这样我就可以定义一个自定义映射来格式化日期。

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

【问题讨论】:

  • 一个更好的问题是你为什么首先将日期转换为字符串...... ;)
  • @DavidG 正如我在问题中明确指出的那样,这样我就可以对其进行格式化以进行显示,例如我通常会做BirthDate.ToString("yyyy-MM-dd")
  • 好吧,我不会说“清楚地”,但是,这仍然是个坏主意。您的模型不应定义格式,将其保留在 UI 层中。
  • @DavidG 它是在视图模型中格式化的——我会说这几乎是在 UI 层中,不是吗?视图模型在多个地方使用,所以我真的不想在我的 Razor 标记中格式化日期。
  • 你能展示使用 AutoMapper 将Person 转换为PersonViewModel 的代码吗?

标签: c# date datetime automapper


【解决方案1】:

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

借用 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)。

【讨论】:

  • 理论上不错,但实际上只适用于Map(OP 代码也适用),而ProjectTo 则惨遭失败。
  • 非常好的和信息丰富的答案,我很想尝试一段时间,但是对于我需要它的项目来说太复杂了,这是一个快速而简单的概念证明。我在调用该日期值对象Birthdate 时也遇到了问题,因为我想将它用于所有日期,并将其称为像DateValueObject 这样的笨拙但具有描述性的东西。
【解决方案2】:

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

ProjectTo 应用于 EF Core 2 查询时,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
//

上面还包含解决方案,特别是针对您的方案。只需将 ResolveUsing 替换为 MapFrom 即可在所有情况下以及所有上述 AutoMapper 版本中正常工作:

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

【讨论】:

  • 我有完全相同的问题,即使我使用 MapFrom,它也不会将日期转换为所需的格式,而是转换为我系统的默认格式。我正在使用 Automapper v 10.0。简而言之,我添加的映射没有任何效果。我的代码就像 CreateMap() .ForMember(d => d.ExpiryDateUtc, opts => opts.MapFrom(s => s.ExpiryDateUtc.ToString("MM-dd-yyyy HH:mm:ss" )));有什么意见吗?
  • 没关系,我找到了原因。这是因为我的映射被其他对 IncludeMember 方法的 CreateMap 调用覆盖。
【解决方案3】:

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

【讨论】:

  • 我用的是哪个?类型转换器,还是值转换器?
  • 我会尝试一个类型转换器,看看效果如何。
【解决方案4】:

我刚刚检查了 Automapper 4.1.1 和 6.2.2,它的工作方式与您的预期一样。您的映射转换绝对正确,也许您还有其他错误。 AppConstants.DefaultDateFormat 为空?或配置未执行等

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-21
    • 2019-11-24
    相关资源
    最近更新 更多