Automapper能否使用repository将外键映射到对象?

17

我正在尝试使用Entity Framework Code First CTP4。假设我有:

public class  Parent
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Child
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Parent Mother { get; set; }
}

public class TestContext : DbContext
{
    public DbSet<Parent> Parents { get; set; }
    public DbSet<Child> Children { get; set; }
}

public class ChildEdit
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int MotherId { get; set; }
}

Mapper.CreateMap<Child, ChildEdit>();

将数据映射到编辑模型并不是问题。在我的屏幕上,我通过某些控件(下拉列表、自动完成器等)选择母亲,并将母亲的ID提交回后端:

[HttpPost]
public ActionResult Edit(ChildEdit posted)
{
    var repo = new TestContext();

    var mapped = Mapper.Map<ChildEdit, Child>(posted);  // <------- ???????
}

我应该如何解决最后一个映射?我不想在Child对象中放置Mother_Id。目前我使用这个解决方案,但我希望可以在Automapper中解决。

我该如何解决最后一个映射?我不想将 Mother_Id 放入 Child 对象中。目前我使用这种解决方案,但我希望能在 Automapper 中解决。

        Mapper.CreateMap<ChildEdit, Child>()
            .ForMember(i => i.Mother, opt => opt.Ignore());

        var mapped = Mapper.Map<ChildEdit, Child>(posted);
        mapped.Mother = repo.Parents.Find(posted.MotherId);

编辑 这个方法是有效的,但现在我必须为每个外键都执行这个操作(顺便说一句:上下文将在最终解决方案中注入):

        Mapper.CreateMap<ChildEdit, Child>();
            .ForMember(i => i.Mother,
                       opt => opt.MapFrom(o => 
                              new TestContext().Parents.Find(o.MotherId)
                                         )
                      );

我真正想要的是:

        Mapper.CreateMap<int, Parent>()
            .ForMember(i => i, 
                       opt => opt.MapFrom(o => new TestContext().Parents.Find(o))
                      );

        Mapper.CreateMap<ChildEdit, Child>();

使用Automapper可以实现这个吗?

3个回答

21

首先,我假设您有一个类似于IRepository<T>的存储库接口。

然后创建以下类:

public class EntityConverter<T> : ITypeConverter<int, T>
{
    private readonly IRepository<T> _repository;
    public EntityConverter(IRepository<T> repository)
    {
        _repository = repository;
    }
    public T Convert(ResolutionContext context)
    {
        return _repository.Find(System.Convert.ToInt32(context.SourceValue));       
    }
}

这个类主要用于实现整数和域实体之间的转换。它使用实体的“Id”从存储库中读取该实体。使用IoC容器将IRepository注入到转换器中,稍后会详细介绍。

让我们使用AutoMapper进行配置:

Mapper.CreateMap<int, Mother>().ConvertUsing<EntityConverter<Mother>>();

我建议创建这个“通用”映射,这样如果你在其他类中有对“Mother”的引用,它们会自动映射而无需额外的努力。
关于IRepository的依赖注入,如果你使用Castle Windsor,AutoMapper配置也应该包括:
IWindsorContainer container = CreateContainer();
Mapper.Initialize(map => map.ConstructServicesUsing(container.Resolve));

我曾经使用过这种方法,效果非常不错。


你能演示一下当我想要将ID映射到实体时如何使用你的解决方案吗?我的意思是在我实现了所有内容后如何进行映射? - user2412672
如果存储库的“查找”是异步的,有更好的方法来处理这个问题吗? - Kevin Krumwiede

6

以下是我的操作步骤:(使用ValueInjecter
为了展示其工作原理,我将要求做得稍微大一些


[TestFixture]
public class JohnLandheer
{
    [Test]
    public void Test()
    {
        var child = new Child
        {
            Id = 1,
            Name = "John",
            Mother = new Parent { Id = 3 },
            Father = new Parent { Id = 9 },
            Brother = new Child { Id = 5 },
            Sister = new Child { Id = 7 }
        };
        var childEdit = new ChildEdit();

        childEdit.InjectFrom(child)
                 .InjectFrom<EntityToInt>(child);

        Assert.AreEqual(1, childEdit.Id);
        Assert.AreEqual("John", childEdit.Name);
        Assert.AreEqual(3, childEdit.MotherId);
        Assert.AreEqual(9, childEdit.FatherId);
        Assert.AreEqual(5, childEdit.BrotherId);
        Assert.AreEqual(7, childEdit.SisterId);
        Assert.AreEqual(0, childEdit.Sister2Id);

        var c = new Child();

        c.InjectFrom(childEdit)
            .InjectFrom<IntToEntity>(childEdit);

        Assert.AreEqual(1, c.Id);
        Assert.AreEqual("John", c.Name);
        Assert.AreEqual(3, c.Mother.Id);
        Assert.AreEqual(9, c.Father.Id);
        Assert.AreEqual(5, c.Brother.Id);
        Assert.AreEqual(7, c.Sister.Id);
        Assert.AreEqual(null, c.Sister2);
    }

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

    public class Parent : Entity
    {
        public string Name { get; set; }
    }

    public class Child : Entity
    {
        public string Name { get; set; }
        public Parent Mother { get; set; }
        public Parent Father { get; set; }
        public Child Brother { get; set; }
        public Child Sister { get; set; }
        public Child Sister2 { get; set; }
    }

    public class ChildEdit
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int MotherId { get; set; }
        public int FatherId { get; set; }
        public int BrotherId { get; set; }
        public int SisterId { get; set; }
        public int Sister2Id { get; set; }
    }

    public class EntityToInt : LoopValueInjection
    {
        protected override bool TypesMatch(Type sourceType, Type targetType)
        {
            return sourceType.IsSubclassOf(typeof(Entity)) && targetType == typeof(int);
        }

        protected override string TargetPropName(string sourcePropName)
        {
            return sourcePropName + "Id";
        }

        protected override bool AllowSetValue(object value)
        {
            return value != null;
        }

        protected override object SetValue(object sourcePropertyValue)
        {
            return (sourcePropertyValue as Entity).Id;
        }
    }

    public class IntToEntity : LoopValueInjection
    {
        protected override bool TypesMatch(Type sourceType, Type targetType)
        {
            return sourceType == typeof(int) && targetType.IsSubclassOf(typeof(Entity));
        }

        protected override string TargetPropName(string sourcePropName)
        {
            return sourcePropName.RemoveSuffix("Id");
        }

        protected override bool AllowSetValue(object value)
        {
            return (int)value > 0;
        }

        protected override object SetValue(object sourcePropertyValue)
        {
            // you could as well do repoType = IoC.Resolve(typeof(IRepo<>).MakeGenericType(TargetPropType))
            var repoType =  typeof (Repo<>).MakeGenericType(TargetPropType);
            var repo = Activator.CreateInstance(repoType);
            return repoType.GetMethod("Get").Invoke(repo, new[] {sourcePropertyValue});
        }
    }

    class Repo<T> : IRepo<T> where T : Entity, new()
    {
        public T Get(int id)
        {
            return new T{Id = id};
        }
    }

    private interface IRepo<T>
    {
        T Get(int id);
    }
}

谢谢。我想避免在映射逻辑中创建一个仓库,因为这很耗费资源,但从你的解决方案中我得出结论这并不是真的。对吧?我会尝试在Automapper中实现类似的功能并发布我的解决方案。 - John Landheer
你必须从某个地方获取数据 :), 通常我会使用IoC.Resolve(type),这不会很昂贵,因为IoC容器将提供一个已经创建的实例。 - Omu
在这里,我使用了超级通用的方法,因为我想展示如何使用一个注入处理多个不同类型的属性,但当然也可以不使用MakeGenericType、method.invoke等方法来实现,但很可能需要创建多个注入。您可以查看ValueInjecter的“入门指南”。 - Omu
非常好。我很快就会使用这个技巧。 - John Farrell
这种技术真的很棒:),使用它,您很快就会忘记映射:),您还可以创建一个Mapper类,其中包含buildEntity和buildInput方法,将所有的injectFrom<>捆绑在一起,就这样。我使用ValueInjecter的asp.net-mvc示例中的TinyController类中展示的技术,并且使用IoC实际上有一个实现Ibuilder<TEntity,TInput>的实现,在大多数情况下都会使用它。 - Omu

2

在EF中也可以通过以下方式定义外键:

[ForeignKey("MotherId")]
public virtual Parent Mother { get; set; }
public int MotherId { get; set; }

在这种情况下,不需要进行额外的查询来查找母亲。只需将ViewModel的MotherId分配给Model的MotherId即可。

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