使用Entity Framework Core进行读写,而无需自发更改实体对象

问题描述 投票:0回答:1

[EF Core通过设置键和维护导航属性来修改其跟踪的对象。

作为为什么这可能是个问题的示例,假设您启动了一个将实体添加到DbContext的任务。然后,如果您立即枚举同一实体的某些导航属性,而无需等待任务完成,则可以获得InvalidOperationException。当实体在另一个线程中被跟踪时,它可能已经从上下文中拾取了一些其他数据并更改了集合。

我想通过克隆进出EF Core的实体来避免这些问题。但是我也不想编写大量无法维护且容易出错的代码来手工克隆实体。

这是我走了多远:

public static TEntity CloneEntity<TEntity>(this DbContext context, TEntity entity)
    where TEntity : class
{
    if (context == null)
        throw new ArgumentNullException(nameof(context));
    if (entity == null)
        throw new ArgumentNullException(nameof(entity));

    // A map for keeping track of already-cloned objects for circular references.
    var map = new Dictionary<object, object>(ReferenceEqualityComparer.Instance);

    return (TEntity)Recurse(context.Entry(entity));


    object Recurse(EntityEntry entry)
    {
        if (map.TryGetValue(entry.Entity, out var clone))
            return clone;

        clone = entry.CurrentValues.ToObject();
        map.Add(entry.Entity, clone);

        // TODO: Recursively clone and set all the navigation properties.

        return clone;
    }
}

我可能可以弄清楚如何通过反射来解决TODO位,但是EF Core应该已经完成​​了所有这些工作,并且应该已经具有用于有效设置导航属性的编译方法。有没有一种类似于entry.CurrentValues.ToObject()的使用方式?

c# entity-framework entity-framework-core deep-copy entity-framework-core-2.1
1个回答
0
投票

只要您能够自己提供解决方案(我想),我只是在这里花了一些时间。这是一个工作草案。它可能会或可能不会满足您的需求,并且可能会帮助其他人至少找到一些解决方案。

我喜欢表达式,所以使用它们完成了,但是仍然可以纯粹使用反射。

class Program
{
    static void Main(string[] args)
    {
        #region Create data

        using (var context = new ConsoleDbContext())
        {
            context.Add(new Person()
            {
                Id = 1,
                Name = "Relatively Random",
                Animals = new List<Animal>
                    {
                        new Cat {Id= 1, Name = "Relatively" },
                        new Dog {Id = 2, Name = "Random" }
                    },
                Address = new Address
                {
                    Street = "Sesame street",
                    City = "London",
                    Country = "United Kingdom"
                }
            }).State = EntityState.Added;

            context.Add(new Person()
            {
                Id = 2,
                Name = "Random Relatively",
                Animals = new List<Animal>
                    {
                        new Cat {Id= 3, Name = "Relatively" },
                        new Dog {Id = 4, Name = "Random" }
                    }
            });

            #endregion

            #region Save data

            context.SaveChanges();

            #endregion

            // Create empty clone map
            IDictionary<EntityEntry, object> cloneMap = new Dictionary<EntityEntry, object>(context.ChangeTracker.Entries().Count());

            Func<Dog> getDogFunc = () => context.Set<Dog>().Last();

            // Get entry we want to clone
            var dog = context.Entry(getDogFunc());

            // Clone entry
            Dog dogClone = CloneEntity<Dog>(dog, cloneMap);

            // Change name of tracked entity
            dog.Entity.Name = "New name";

            // Compare against entity entry entity value and entity itself(which is non sense as those are same reference, but just to be sure it works
            var result = (dogClone.Name == dog.Entity.Name) || (dogClone.Name == getDogFunc().Name);
        }
    }

    public static TEntity CloneEntity<TEntity>(EntityEntry entityEntry, IDictionary<EntityEntry, object> cloneMap)
        where TEntity : class
    {
        if (entityEntry == null)
        {
            throw new ArgumentNullException(nameof(entityEntry));
        }

        if (cloneMap is null)
        {
            throw new ArgumentNullException(nameof(cloneMap));
        }

        //Try get existing clone
        if (cloneMap.TryGetValue(entityEntry, out var clone))
        {
            return (TEntity)clone;
        }

        var cloneMapConstant = Expression.Constant(cloneMap);
        var entityEntryType = typeof(EntityEntry);
        var bindings = new List<MemberBinding>(typeof(TEntity).GetProperties().Length);
        var entryParameter = Expression.Parameter(entityEntryType, "entry");

        // Create property binding expressions e.g. Name = entry.Entity.Name
        bindings.AddRange(CreatePropertyBinding<TEntity>(ref entityEntry, ref entryParameter));
        // Create reference binding expression e.g. Owner = new Owner() { ..initialization... }
        bindings.AddRange(CreateReferenceBinding(entityEntry, cloneMapConstant));

        // Get existing clone or create new one based on binding expressions created earlier 
        var result = GetOrCreateNewEntity<TEntity>(ref entityEntry, ref entryParameter, ref bindings);

        cloneMap.Add(entityEntry, result);

        return result;
    }

    private static TEntity GetOrCreateNewEntity<TEntity>(ref EntityEntry entityEntry, ref ParameterExpression entryParameter, ref List<MemberBinding> bindings)
        where TEntity : class
    {
        // new TEntity()
        var newEntity = Expression.New(typeof(TEntity));

        // new TEntity() { ...initialization...  }
        var initilizer = Expression.MemberInit(newEntity, bindings);

        // entry => new TEntity() { ...initialization...  }
        var lambda = Expression.Lambda(initilizer, entryParameter);

        // Compile expression to function
        var function = lambda.Compile();

        // Invoke function and cast result
        var result = (TEntity)function.DynamicInvoke(entityEntry);

        // Return result
        return result;
    }

    private static IEnumerable<MemberBinding> CreateCollectionBinding(EntityEntry entityEntry, ConstantExpression cloneMapConstant)
    {
        // Get all collection properties
        var collections = entityEntry
            .Collections;
        //.Where(n => n.IsLoaded);

        var cloneEntityMethod = typeof(Program).GetMethod(nameof(CloneEntity));

        foreach (var collection in collections)
        {
            // ICollection<SomeType>
            var clrType = collection.Metadata.ClrType;

            var elementType = clrType.GenericTypeArguments[0];

            var hashSetType = typeof(HashSet<>).MakeGenericType(elementType);

            // new HashSet<SomeType>()
            var hashSet = Expression.New(hashSetType);

            var converted = Expression.TypeAs(hashSet, clrType);

            var result = Expression.Bind(collection.Metadata.PropertyInfo, converted);

            yield return result;
        }
    }

    private static IEnumerable<EntityEntry> GetCollectionItemEntries(DbContext context, CollectionEntry collection)
    {
        foreach (var item in collection.CurrentValue)
        {
            yield return context.Entry(item);
        }
    }

    private static IEnumerable<MemberBinding> CreateReferenceBinding(EntityEntry entityEntry, ConstantExpression cloneMapConstant)
    {
        var references = entityEntry
            .References
            .Where(n => n.IsLoaded);

        var result = new List<MemberBinding>(references.Count());

        var cloneEntityMethod = typeof(Program).GetMethod(nameof(CloneEntity));

        foreach (var reference in references)
        {
            var referenceEntityEntry = Expression.Constant(reference.EntityEntry.Context.Entry(reference.TargetEntry.Entity));

            var genericCloneEntityMethod = cloneEntityMethod.MakeGenericMethod(reference.Metadata.ClrType);

            var callCloneEntityMethod = Expression.Call(null, genericCloneEntityMethod, new Expression[] { referenceEntityEntry, cloneMapConstant });

            yield return Expression.Bind(reference.Metadata.PropertyInfo, callCloneEntityMethod);
        }
    }

    private static IEnumerable<MemberAssignment> CreatePropertyBinding<TEntity>(ref EntityEntry entityEntry, ref ParameterExpression parameter)
    {
        var entityEntryType = parameter.Type.GetProperty(nameof(EntityEntry.Entity), typeof(object));
        var entityProperty = Expression.MakeMemberAccess(parameter, entityEntryType);
        var convertedEntity = Expression.Convert(entityProperty, typeof(TEntity)); ;

        var result = entityEntry
            .Properties
            .Where(p => p.Metadata.IsShadowProperty() == false)
            .Select(p => Expression.Bind(p.Metadata.PropertyInfo, Expression.MakeMemberAccess(convertedEntity, p.Metadata.PropertyInfo)));

        return result;
    }
}

public class ConsoleDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseInMemoryDatabase("inmemory");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Owned<Address>();

        modelBuilder.Entity<Person>(person =>
        {
            person.HasKey(e => e.Id);
            person.Property(e => e.Name);
            person.OwnsOne(e => e.Address);
        });

        modelBuilder.Entity<Animal>(animal =>
        {
            animal.HasKey(e => e.Id);
            animal.HasDiscriminator();
            animal.Property(e => e.Name);
            animal.HasOne(e => e.Owner)
                .WithMany(e => e.Animals);
        });

        modelBuilder.Entity<Dog>(dog =>
        {
            dog.HasBaseType<Animal>();
        });

        modelBuilder.Entity<Cat>(cat =>
        {
            cat.HasBaseType<Animal>();
        });
    }
}
public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
}

public abstract class Animal : Entity<int>
{
    public string Name { get; set; }
    public Person Owner { get; set; }
}

public class Cat : Animal
{
}

public class Dog : Animal
{
}

public abstract class Entity<TKey>
{
    public TKey Id { get; set; }
}

public class Person : Entity<int>
{
    public Person()
    {
        //Animals = new HashSet<Animal>();
    }

    public string Name { get; set; }
    public Address Address { get; set; }
    public virtual ICollection<Animal> Animals { get; set; }
}
© www.soinside.com 2019 - 2024. All rights reserved.