Объединение отдельных объектов Entity Framework

У меня есть сценарий, в котором я использую Entity Framework в службе WCF, и изменения происходят в неотслеживаемом экземпляре типа, который сопоставляется с базой данных с помощью кода сначала (нетривиальные обновления и удаления по всему дереву объектов экземпляра ). Когда я пытаюсь присоединить неотслеживаемый экземпляр к контексту, EF распознает только изменения простых типов значений в корневом объекте.

Кто-нибудь знает элегантное решение для этого сценария? Я ищу способ сделать это, используя общий репозиторий и избегая необходимости запускать все дерево объектов экземпляра, управляя состоянием «присоединить/отсоединить» каждого объекта. Я рассматривал возможность использования ValueInjecter или AutoMapper для запуска изменений в полностью гидратированном и отслеживаемом экземпляре «старого» состояния, чтобы контекст мог принять изменения. Кроме того, как Nhibernate справится с этой ситуацией?

Спасибо заранее за ваш вклад!

ОБНОВЛЕНИЕ (31 июля 2012 г.): я обновил код, чтобы он обрабатывал ключи общего типа и некоторые проблемы с вводом данных в прокси-серверах EF. Также добавлены некоторые вспомогательные расширения при работе с типами IEntity. Эта реализация не идеальна, но очень функциональна.

ОБНОВЛЕНИЕ (13 марта 2012 г.): я добавил запрос функции для более чистого слияния в EF. Запрос находится здесь: http://data.uservoice.com/forums/72025-ado-net-entity-framework-ef-feature-suggestions/suggestions/2679160-better-merging-change-tracking.

ОБНОВЛЕНИЕ (12 марта 2012 г.): я разместил свое решение ниже. Он использует FubuCore, ValueInjecter и требует, чтобы объекты были помечены одним из двух интерфейсов, либо IEntity, либо IRRecursiveEntity для рекурсивных классов. Решение будет обрабатывать рекурсивные самосвязанные сущности.

Кроме того, я ссылаюсь на общий репозиторий (репозиторий), который позволяет мне получить ссылку на набор IDbSet, который предоставляет EF. Это может быть заменено любым другим общим или конкретным хранилищем. Наконец, интерфейс 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; }
}

person jdscolam    schedule 09.03.2012    source источник


Ответы (1)


вы можете использовать отдельный объект только как DTO,

и после заполнения объекта из контекста значениями из DTO

с ValueInjecter это будет:

//manually
conObj.InjectFrom(dto);
conObj.RefTypeProp.InjectFrom(dto.RefTypeProp);
...

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

вот Injection, который сделает это автоматически (я сделал это, немного изменив DeepClone Injection с домашней страницы VI)

хитрость здесь в том, что 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

person Omu    schedule 09.03.2012
comment
Итак, в основном берем DTO, а затем ApplyChanges для DTO и всех его дочерних объектов к отслеживаемому объекту, используя приведенный выше код? Кроме того, нужно ли вручную вводить RefTypeProp для каждого сложного свойства и коллекции? Предполагая, что это так, это может не сработать для моего сценария, потому что я хочу автоматизировать весь процесс, поэтому код должен быть только _myRepository.Save(myEntityObject). - person jdscolam; 10.03.2012
comment
можно написать инъекцию, которая будет делать это автоматически, это будет что-то вроде CloneInjection с домашней страницы ВП, за исключением того, что вам не нужно создавать новые объекты - person Omu; 10.03.2012
comment
Я смог найти способ использовать VI для достижения того, что я считаю разумным слиянием, однако в конечном итоге это требует использования отражения. - person jdscolam; 13.03.2012