Can automapper map a foreign key to an object usin

2019-01-31 14:12发布

问题:

I'm trying out Entity Framework Code first CTP4. Suppose I have:

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>();

Mapping to the Edit model is not a problem. On my screen I select the mother through some control (dropdownlist, autocompleter, etc) and the Id of the mother gets posted in back:

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

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

How should I solve the last mapping? I don't want to put Mother_Id in the Child object. For now I use this solution, but I hope it can be solved in 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);

EDIT This works, but now I have to do that for each foreign key (BTW: context would be injected in final solution):

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

What I'd really like would be:

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

        Mapper.CreateMap<ChildEdit, Child>();

Is that possible with Automapper?

回答1:

First, I'll assume that you have a repository interface like IRepository<T>

Afterwards create the following class:

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));       
    }
}

Basically this class will be used to do all the conversion between an int and a domain entity. It uses the "Id" of the entity to load it from the Repository. The IRepository will be injected into the converter using an IoC container, but more and that later.

Let's configure the AutoMapper mapping using:

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

I suggest creating this "generic" mapping instead so that if you have other references to "Mother" on other classes they're mapped automatically without extra-effort.

Regarding the Dependency Injection for the IRepository, if you're using Castle Windsor, the AutoMapper configuration should also have:

IWindsorContainer container = CreateContainer();
Mapper.Initialize(map => map.ConstructServicesUsing(container.Resolve));

I've used this approach and it works quite well.



回答2:

Here's how I did it: (using ValueInjecter)
I made the requirements a little bigger just to show how it works


[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);
    }
}


回答3:

It's possible to define the foreign key in EF this way as well:

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

In this case, It's not necessary to do an extra query to find the Mother. Just Assign the ViewModel's MotherId to the Model's MotherId.