Automapper - 如何映射到构造函数参数而不是属性设置器

132

在目标属性是私有的情况下,我可能希望通过目标对象的构造函数映射到该对象。如何使用Automapper实现此功能?

5个回答

175

使用ConstructUsing,可以在映射过程中指定要使用的构造函数。但其他属性将根据约定自动映射。

还要注意,这与ConvertUsing不同,因为ConvertUsing不会继续通过约定进行映射,而是完全掌控映射。

Mapper.CreateMap<ObjectFrom, ObjectTo>()
    .ConstructUsing(x => new ObjectTo(arg0, arg1, etc));

...

using AutoMapper;
using NUnit.Framework;

namespace UnitTests
{
    [TestFixture]
    public class Tester
    {
        [Test]
        public void Test_ConstructUsing()
        {
            Mapper.CreateMap<ObjectFrom, ObjectTo>()
                .ConstructUsing(x => new ObjectTo(x.Name));

            var from = new ObjectFrom { Name = "Jon", Age = 25 };

            ObjectTo to = Mapper.Map<ObjectFrom, ObjectTo>(from);

            Assert.That(to.Name, Is.EqualTo(from.Name));
            Assert.That(to.Age, Is.EqualTo(from.Age));
        }
    }

    public class ObjectFrom
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

    public class ObjectTo
    {
        private readonly string _name;

        public ObjectTo(string name)
        {
            _name = name;
        }

        public string Name
        {
            get { return _name; }
        }

        public int Age { get; set; }
    }
}

9
非常感谢您提供的例子 Jon。 "ConstructUsing" 很棒!它使我能够保持我的 DTO 不可变,并将设置器标记为私有。 - Daniel
3
这对我很有用;AutoMapper目前不支持所有参数均为可选的构造函数,因此我只需使用 .ConstructUsing(x => new MyClass())。 - David Keaveny
1
尝试了类似的东西 Mapper.CreateMap<Order, OrderViewModel>().ConstructUsing(x => new OrderViewModel(this)); ... 出现“调用不明确”的编译器错误。 - Chris Klepeis
如果您在目标对象上使用只读字段而不是私有设置器,则两种方法都会抛出“表达式必须是可写的参数名称:left”的异常。 - Facio Ratio
2
如果我需要传递比string更复杂的东西怎么办?如果ObjectFrom包含了必须传递给ObjectTo构造函数的ChildObjectFrom类型属性,该怎么办? - Gabrielius
显示剩余6条评论

17

最佳实践是使用AutoMapper文档中记录的方法:http://docs.automapper.org/en/stable/Construction.html

public class SourceDto
{
        public SourceDto(int valueParamSomeOtherName)
        {
            Value = valueParamSomeOtherName;
        }

        public int Value { get; }
}

Mapper.Initialize(cfg => cfg.CreateMap<Source, SourceDto>()
  .ForCtorParam(
    "valueParamSomeOtherName", 
    opt => opt.MapFrom(src => src.Value)
  )
);

1
不幸的是,在这里没有办法使用 nameof 来避免脆弱的硬编码魔术字符串,而且(除非 AutoMapper 现在有一个自定义的 Roslyn 规则),如果您省略参数或指定不存在的参数,您将不会得到编译时错误。 - Dai

13

您应该使用Map方法来设置目标。例如:

Mapper.CreateMap<ObjectFrom, ObjectTo>()

var from = new ObjectFrom { Name = "Jon", Age = 25 };

var to = Mapper.Map(from, new ObjectTo(param1));

7
在编写本答案时,如果属性与构造函数参数匹配,则AutoMapper将自动执行此操作(只需使用简单的CreateMap<>()调用)。当然,如果事情不匹配,则使用.ConstructUsing(...)是正确的方式。
public class PersonViewModel
{
    public int Id { get; set; }

    public string Name { get; set; }
}

public class Person
{
    public Person (int id, string name)
    {
        Id = id;
        Name = name;
    }

    public int Id { get; }

    public string Name { get; }
}

public class PersonProfile : Profile
{
    public PersonProfile()
    {
        CreateMap<PersonViewModel, Person>();
    }
}

注意:这假定您正在使用 Profiles 来设置您的 AutoMapper 映射。
当像下面这样使用时,它会产生正确的对象:
var model = new PersonViewModel
{
    Id = 1
    Name = "John Smith"
}

// will correctly call the (id, name) constructor of Person
_mapper.Map<Person>(model);

您可以在官方GitHub上的维基中了解有关自动映射器构建的更多信息。


看起来 CreateMap<> 应该是 PersonViewModel 而不是 PersonProfile。 同样,在第二个代码块中,PersonModel 应该是 PersonViewModel。 - Ben Sampica

5

个人而言,我在使用 AutoMapper 时更喜欢尽可能地明确,以避免未来出现任何潜在的错误。

如果您只是按照正确的顺序一个接一个地传递参数调用 ConstructUsing 方法,那么您可能会在某一天面临错误。

如果开发人员颠倒了两个字符串参数或在一些现有可选参数之前添加了一个新的可选参数,会怎样呢?这将导致映射错误,其中一个属性没有被映射到它应该映射到的目标属性。 因此,我更喜欢在实例化对象时使用命名参数来定义我的映射。

以下是 ConstructUsing 方法的不同签名:

TMappingExpression ConstructUsing(Func<TSource, ResolutionContext, TDestination> ctor);
TMappingExpression ConstructUsing(Expression<Func<TSource, TDestination>> ctor);

在这种情况下,我们希望使用第一个选项,因为在表达式树中无法使用命名参数(否则会出现编译错误 an expression tree may not contain a named argument specification)。

以下是使用方法:

 CreateMap<FromType, ToType>()
    .ConstructUsing((src, res) =>
    {
        return new ToType(
            foo: src.MyFoo,
            bar: res.Mapper.Map<BarModel>(src.MyBar),
        );
    });

请注意函数的第二个参数resResolution Context,它允许您使用已经注册的映射。
但需要注意的是,使用构造函数声明映射存在一个缺点。如果你的类没有公共setter(只读属性或private set),你将无法使用Map方法的以下重载:
TDestination Map<TSource, TDestination>(TSource source, TDestination destination);

这个重载在使用EF Core更新实体时非常方便。

mapper.Map(updateModel, existingEntity);
await dbContext.SaveChangesAsync();

谢天谢地,EF Core 还有另一种更新实体的方式


Reg “如果开发人员颠倒了两个字符串参数怎么办” 我的理解是参数的名称很重要,而不是顺序。 - LosManos
2
今天学到的一件事:将映射器放在“ConstructUsing”内部。 - LosManos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接