【问题标题】:Entity Framework Detached Object Merging实体框架分离对象合并
【发布时间】:2012-03-27 00:47:58
【问题描述】:

我有一个场景,我在 WCF 服务中使用实体框架,并且更改发生在通过代码优先映射回数据库的类型的非跟踪实例上(在整个实例的对象树)。当我尝试将未跟踪的实例附加到上下文中时,EF 仅识别根对象上简单值类型的更改。

有人知道这种情况的优雅解决方案吗?我正在寻找一种通过使用通用存储库来做到这一点的方法,并且避免必须遍历实例的整个对象树来管理每个对象的“附加/分离”状态。我考虑过可能使用 ValueInjecter 或 AutoMapper 在“旧”状态的完全水合和跟踪实例上运行更改,以便上下文获取更改。另外,Nhibernate 会如何处理这种情况?

提前感谢您的意见!

更新(2012 年 7 月 31 日):我已更新代码以处理通用类型的键,以及 EF 代理的一些键入问题。在处理 IEntity 类型时还添加了一些辅助扩展。这个实现并不完美,但它非常实用。

更新(2012 年 3 月 13 日):我添加了一个功能请求,以便在 EF 中进行更清晰的合并。请求位于:http://data.uservoice.com/forums/72025-ado-net-entity-framework-ef-feature-suggestions/suggestions/2679160-better-merging-change-tracking

更新(2012 年 3 月 12 日):我在下面发布了我的解决方案。它使用 FubuCoreValueInjecter,并且要求用两个接口之一标记实体IEntity,或IRecursiveEntity 用于递归类。该解决方案将处理递归的自链接实体。

另外,我正在引用一个通用存储库 (Repository),它允许我获取对 EF 公开的 IDbSet 的引用。这可以用任何其他通用或特定存储库代替。最后,IEntity 接口使用 int? id,但是您可以根据需要定义它(Guid/Guid?)。该解决方案本身并不像我想的那么优雅,但是它允许在物理 WCF 服务边界之后更优雅的数据访问代码。

public class DomainMergeInjection : ConventionInjection
{
    private readonly Repository _repository;
    private readonly Dictionary<string, object> _potentialParentObjectDump;
    private readonly Cache<Type, Type> _entityTypesAndKeysCache;

    public DomainMergeInjection(Repository repository)
    {
        _repository = repository;
        _potentialParentObjectDump = new Dictionary<string, object>();
        _entityTypesAndKeysCache = new Cache<Type, Type>();
    }

    protected override bool Match(ConventionInfo c)
    {
        return c.SourceProp.Name == c.TargetProp.Name;
    }

    protected override object SetValue(ConventionInfo c)
    {
        if(c.SourceProp.Value == null)
            return null;

        //for value types and string just return the value as is 
        if(c.SourceProp.Type.IsSimple())
            return c.SourceProp.Value;

        //TODO: Expand on this to handle IList/IEnumerable (i.e. the non-generic collections and arrays).
        //handle arrays
        if(c.SourceProp.Type.IsArray)
        {
            var sourceArray = c.SourceProp.Value as Array;
            // ReSharper disable PossibleNullReferenceException
            var clonedArray = sourceArray.Clone() as Array;
            // ReSharper restore PossibleNullReferenceException

            for(int index = 0; index < sourceArray.Length; index++)
            {
                var sourceValueAtIndex = sourceArray.GetValue(index);

                //Skip null and simple values that would have already been moved in the clone.
                if(sourceValueAtIndex == null || sourceValueAtIndex.GetType().IsSimple())
                    continue;

                // ReSharper disable PossibleNullReferenceException
                clonedArray.SetValue(RetrieveComplexSourceValue(sourceValueAtIndex), index);
                // ReSharper restore PossibleNullReferenceException
            }

            return clonedArray;
        }

        //handle IEnumerable<> also ICollection<> IList<> List<>
        if(c.SourceProp.Type.IsGenericEnumerable())
        {
            var t = c.SourceProp.Type.GetGenericArguments()[0];

            if(t.IsSimple())
                return c.SourceProp.Value;

            var tlist = typeof(List<>).MakeGenericType(t);
            dynamic list = Activator.CreateInstance(tlist);

            var addMethod = tlist.GetMethod("Add");

            foreach(var sourceItem in (IEnumerable)c.SourceProp.Value)
            {
                addMethod.Invoke(list, new[] { RetrieveComplexSourceValue(sourceItem) });
            }

            return list;
        }

        //Get a source value that is in the right state and is tracked if needed.
        var itemStateToInject = RetrieveComplexSourceValue(c.SourceProp.Value);
        return itemStateToInject;
    }

    private object RetrieveComplexSourceValue(object source)
    {
        //If the source is a non-tracked type, or the source is a new value, then return its value.
        if(!source.ImplementsIEntity(_entityTypesAndKeysCache) || source.IsEntityIdNull(_entityTypesAndKeysCache))
            return source;

        object sourceItemFromContext;

        //Handle recursive entities, this could probably be cleaned up.
        if(source.ImplementsIRecursiveEntity())
        {
            var itemKey = source.GetEntityIdString(_entityTypesAndKeysCache) + " " + ObjectContext.GetObjectType(source.GetType());

            //If we have a context item for this key already, just return it.  This solves a recursion problem with self-linking items.
            if(_potentialParentObjectDump.ContainsKey(itemKey))
                return _potentialParentObjectDump[itemKey];

            //Get the source from the context to ensure it is tracked.
            sourceItemFromContext = GetSourceItemFromContext(source);

            //Add the class into the object dump in order to avoid any infinite recursion issues with self-linked objects
            _potentialParentObjectDump.Add(itemKey, sourceItemFromContext);
        }
        else
            //Get the source from the context to ensure it is tracked.
            sourceItemFromContext = GetSourceItemFromContext(source);

        //Recursively use this injection class instance to inject the source state on to the context source state.
        var itemStateToInject = sourceItemFromContext.InjectFrom(this, source);

        return itemStateToInject;
    }

    private object GetSourceItemFromContext(object source)
    {
        if(source == null)
            return null;

        //Using dynamic here to "AutoCast" to an IEntity<>.  We should have one, but it's important to note just in case.
        dynamic sourceEntityValue = source;
        var sourceEntityType = ObjectContext.GetObjectType(source.GetType());
        var sourceKeyType = sourceEntityType.GetEntityKeyType();

        var method = typeof(DomainMergeInjection).GetMethod("GetFromContext", BindingFlags.Instance | BindingFlags.NonPublic);
        var generic = method.MakeGenericMethod(sourceEntityType, sourceKeyType);

        var sourceItemFromContext = generic.Invoke(this, new object[] { new object[] { sourceEntityValue.Id } });
        return sourceItemFromContext;
    }

    // ReSharper disable UnusedMember.Local
    private TItem GetFromContext<TItem, TKey>(object[] keys) where TItem : class, IEntity<TKey>
    // ReSharper restore UnusedMember.Local
    {
        var foundItem = _repository.GetDbSet<TItem>().Find(keys);

        return foundItem;
    }
}

public static class EntityTypeExtensions
{
    /// <summary>
    /// Determines if an object instance implements IEntity.
    /// </summary>
    /// <param name="entity"></param>
    /// <param name="entityCache">A cache to hold types that do implement IEntity.  If the cache does not have the Type and the Type does implement IEntity, it will add the type to the cache along with the </param>
    /// <returns></returns>
    public static bool ImplementsIEntity(this object entity, Cache<Type, Type> entityCache = null)
    {
        //We need to handle getting the proxy type if this is an EF Code-First proxy.
        //Please see for more info: http://msdn.microsoft.com/en-us/library/dd456853.aspx
        var entityType = ObjectContext.GetObjectType(entity.GetType());

        if(entityCache != null && entityCache.Has(entityType))
            return true;

        var implementationOfIEntity = entityType.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof (IEntity<>));

        if(implementationOfIEntity == null)
            return false;

        if(entityCache != null)
        {
            var keyType = implementationOfIEntity.GetGenericArguments()[0];
            entityCache.Fill(entityType, keyType);
        }

        return true;
    }

    /// <summary>
    /// Determines if an object instances implements IRecurisveEntity
    /// </summary>
    /// <param name="entity"></param>
    /// <returns></returns>
    public static bool ImplementsIRecursiveEntity(this object entity)
    {
        //We need to handle getting the proxy type if this is an EF Code-First proxy.
        //Please see for more info: http://msdn.microsoft.com/en-us/library/dd456853.aspx
        var entityType = ObjectContext.GetObjectType(entity.GetType());

        var implementsIRecursiveEntity = entityType.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IRecursiveEntity<>));

        return implementsIRecursiveEntity;
    }

    /// <summary>
    /// Determines whether or not an Entity's Id is null.  Will throw an exception if a type that does not implement IEntity is passed through.
    /// </summary>
    /// <param name="entity"></param>
    /// <param name="entityCache"></param>
    /// <returns></returns>
    public static bool IsEntityIdNull(this object entity, Cache<Type, Type> entityCache = null)
    {
        bool isEntityIdNull = ExecuteEntityIdMethod<bool>("IsEntityIdNull", entity, entityCache);

        return isEntityIdNull;
    }

    /// <summary>
    /// Determines whether or not an Entity's Id is null.  Will throw an exception if a type that does not implement IEntity is passed through.
    /// </summary>
    /// <param name="entity"></param>
    /// <param name="entityCache"></param>
    /// <returns></returns>
    public static string GetEntityIdString(this object entity, Cache<Type, Type> entityCache = null)
    {
        string entityIdString = ExecuteEntityIdMethod<string>("GetEntityIdString", entity, entityCache);

        return entityIdString;
    }

    private static T ExecuteEntityIdMethod<T>(string methodName, object entityInstance, Cache<Type, Type> entityCache = null)
    {
        if(!entityInstance.ImplementsIEntity(entityCache))
            throw new ArgumentException(string.Format("Parameter entity of type {0} does not implement IEntity<>, and so ist not executable for {1}!", entityInstance.GetType(), methodName));

        //We need to handle getting the proxy type if this is an EF Code-First proxy.
        //Please see for more info: http://msdn.microsoft.com/en-us/library/dd456853.aspx
        var entityType = ObjectContext.GetObjectType(entityInstance.GetType());
        var keyType = entityCache != null ? entityCache[entityType] : entityType.GetEntityKeyType();

        var method = typeof(EntityTypeExtensions).GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic);
        var generic = method.MakeGenericMethod(keyType);

        T returnValue = (T)generic.Invoke(null, new[] { entityInstance });

        return returnValue;
    }

    private static string GetEntityIdString<TKey>(IEntity<TKey> entity)
    {
        var entityIdString = entity.Id.ToString();

        return entityIdString;
    }

    private static bool IsEntityIdNull<TKey>(IEntity<TKey> entity)
    {
        //We need to handle getting the proxy type if this is an EF Code-First proxy.
        //Please see for more info: http://msdn.microsoft.com/en-us/library/dd456853.aspx
        var entityType = ObjectContext.GetObjectType(entity.GetType());

        if(entityType.IsPrimitive)
            return false;

        //NOTE:  We know that this entity's type is NOT primitive, therefore we can cleanly test for null, and return properly.
        // ReSharper disable CompareNonConstrainedGenericWithNull
        var entityIdIsNull = entity.Id == null;
        // ReSharper restore CompareNonConstrainedGenericWithNull

        return entityIdIsNull;
    }

    public static Type GetEntityKeyType(this Type typeImplementingIEntity)
    {
        var implementationOfIEntity = typeImplementingIEntity.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEntity<>));

        if(implementationOfIEntity == null)
            throw new ArgumentException(string.Format("Type {0} does not implement IEntity<>", typeImplementingIEntity));

        var keyType = implementationOfIEntity.GetGenericArguments()[0];
        return keyType;
    }
}

public interface IEntity<TKey>
{
    TKey Id { get; set; }
}


public interface IRecursiveEntity<TKey> : IEntity<TKey>
{
    IRecursiveEntity<TKey> Parent { get; }
    IEnumerable<IRecursiveEntity<TKey>> Children { get; }
}

【问题讨论】:

    标签: entity-framework fluent-nhibernate entity-framework-4.3 valueinjecter fubumvc


    【解决方案1】:

    您只能将分离的对象用作 DTO,

    然后用 DTO 中的值从上下文中重新填充对象

    使用 ValueInjecter 这将是:

    //manually
    conObj.InjectFrom(dto);
    conObj.RefTypeProp.InjectFrom(dto.RefTypeProp);
    ...
    
    //or by writing a custom injection:
    conObj.InjectFrom<ApplyChangesInjection>(dto);
    

    这是自动执行此操作的注入,(我通过修改 VI 主页上的 DeepClone Injection 来实现)

    这里的诀窍是注入在 SetValue 方法中使用自己

    public class ApplyChangesInjection : ConventionInjection
    {
        protected override bool Match(ConventionInfo c)
        {
            return c.SourceProp.Name == c.TargetProp.Name;
        }
    
        protected override object SetValue(ConventionInfo c)
        {
            if (c.SourceProp.Value == null) return null;
    
            //for value types and string just return the value as is
            if (c.SourceProp.Type.IsValueType || c.SourceProp.Type == typeof(string))
                return c.SourceProp.Value;
    
            //handle arrays - not impl
            //handle IEnumerable<> also ICollection<> IList<> List<> - not impl
    
            //for simple object types apply the inject using the corresponding source
    
            return c.TargetProp.Value
                .InjectFrom<ApplyChangesInjection>(c.SourceProp.Value);
        }
    }
    

    //注意:我不是在这个注入中处理集合,我只是想让你了解原理, 可以看原文http://valueinjecter.codeplex.com/wikipage?title=Deep%20Cloning&referringTitle=Home

    【讨论】:

    • 所以基本上采用 DTO,然后使用上面的代码将 DTO 及其所有子对象“ApplyChanges”应用于跟踪对象?此外,是否需要为每个复杂的属性和集合手动完成 RefTypeProp 注入?假设确实如此,它可能不适用于我的场景,因为我想自动化整个过程,因此代码只需 _myRepository.Save(myEntityObject)。
    • 可以编写一个自动执行此操作的注入,类似于 VI 主页中的 CloneInjection,但您不需要创建新对象
    • 我能够找到一种方法来使用 VI 来实现我认为合理的合并,但最终需要使用反射。
    猜你喜欢
    • 1970-01-01
    • 2016-12-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-28
    • 2014-04-19
    相关资源
    最近更新 更多