实体框架分离的对象合并



我有一个场景,在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,并且要求实体标记为两个接口中的一个IEntityIRecursiveEntity用于递归类)。该解决方案将处理递归的自链接实体。

此外,我正在引用一个通用存储库(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; }
}

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

并且在用DTO 的值从上下文重新填充对象之后

对于ValueInjecter,这将是:

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

这是将自动完成的注入(我通过修改VI主页上的DeepClone注入来完成)

这里的技巧是注入在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=主页

最新更新