Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Finding entities with same key in an object graph for preventing "An object with the same key already exists in the ObjectStateManager" Error

I'm using EF code first for developing my 3 layer WinForm Application, I used disconnected POCOs as my model entities. All my entities inherited from BaseEntity class.

I used disconnected POCOs, so I handle entity's State on client side, and in ApplyChanges() method, I attach my entity graph(e.g An Order with it's OrderLines and Products) to my DbContext and then synch each entity's State with it's client side State.

public class BaseEntity
{

    int _dataBaseId = -1;

    public virtual int DataBaseId // DataBaseId override in each entity to return it's key
    {
        get { return _dataBaseId; }
    } 

    public States State { get; set; }

    public enum States
    {
        Unchanged, 
        Added,
        Modified,
        Deleted
    }
}

So, when I want to save a graph of related entities, I used following methods:

    public static EntityState ConvertState(BaseEntity.States state)
    {
        switch (state)
        {
            case BaseEntity.States.Added:
                return EntityState.Added;
            case BaseEntity.States.Modified:
                return EntityState.Modified;
            case BaseEntity.States.Deleted:
                return EntityState.Deleted;
            default:
                return EntityState.Unchanged;
        }
    }

    public void ApplyChanges<TEntity>(TEntity root) where TEntity : BaseEntity
    {
       _dbContext.Set<TEntity>().Add(root);
        foreach (var entry in _dbContext.ChangeTracker
        .Entries<BaseEntity>())
        {
            BaseEntity stateInfo = entry.Entity;
            entry.State = ConvertState(stateInfo.State);
        }
    }

But if my graph contains 2 or more entities with the same key i give this error :

An object with the same key already exists in the ObjectStateManager...

How can i detect entities with the same keys in my graph(root) and make them unique in my ApplyChanges() method?

like image 399
Masoud Avatar asked Jan 03 '13 11:01

Masoud


2 Answers

There is a way to search the database and check if a record with that same primary key already exists, I don't know if that's what you are looking for, but the code is below:

public static class ObjectSetExtensions
{
    #region Constants

    private const BindingFlags KeyPropertyBindingFlags =
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

    #endregion

    #region Public Methods and Operators

        public static bool RecordExists<TEntity>(
        this ObjectSet<TEntity> set, 
        TEntity entity) where TEntity : class
    {
        Contract.Requires(set != null);
        Contract.Requires(entity != null);

        var expressionParameter = Expression.Parameter(typeof(TEntity));
        var keyProperties = set.GetKeyProperties();

        var matchExpression =
            keyProperties.Select(
                pi =>
                Expression.Equal(
                    Expression.Property(expressionParameter, pi.Last()),
                    Expression.Constant(pi.Last().GetValue(entity, null))))
                .Aggregate<BinaryExpression, Expression>(
                    null,
                    (current, predicate) => (current == null) ? predicate : 
                        Expression.AndAlso(current, predicate));

        var existing =
            set.SingleOrDefault(Expression.Lambda<Func<TEntity, bool>>(
            matchExpression, 
            new[] { expressionParameter }));

        return existing != null;
    }

    #endregion

    #region Methods

    private static IEnumerable<PropertyPathCollection> GetKeyProperties<TEntity>(this ObjectSet<TEntity> objectSet)
        where TEntity : class
    {
        Contract.Requires(objectSet != null);

        var entityType = typeof(TEntity);

        return
            objectSet.EntitySet.ElementType.KeyMembers.Select(
                c => new PropertyPathCollection(entityType.GetProperty(c.Name, KeyPropertyBindingFlags)));
    }

    #endregion
}

public sealed class PropertyPathCollection : IEnumerable<PropertyInfo>
{
    // Fields
    #region Static Fields

    public static readonly PropertyPathCollection Empty = new PropertyPathCollection();

    #endregion

    #region Fields

    private readonly List<PropertyInfo> components;

    #endregion

    // Methods
    #region Constructors and Destructors

    public PropertyPathCollection(IEnumerable<PropertyInfo> components)
    {
        this.components = new List<PropertyInfo>();
        this.components.AddRange(components);
    }

    public PropertyPathCollection(PropertyInfo component)
    {
        this.components = new List<PropertyInfo> { component };
    }

    private PropertyPathCollection()
    {
        this.components = new List<PropertyInfo>();
    }

    #endregion

    #region Public Properties

    public int Count
    {
        get
        {
            return this.components.Count;
        }
    }

    #endregion

    #region Public Indexers

    public PropertyInfo this[int index]
    {
        get
        {
            return this.components[index];
        }
    }

    #endregion

    #region Public Methods and Operators

    public static bool Equals(PropertyPathCollection other)
    {
        if (ReferenceEquals(null, other))
        {
            return false;
        }

        return true;
    }

    public static bool operator ==(PropertyPathCollection left, PropertyPathCollection right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(PropertyPathCollection left, PropertyPathCollection right)
    {
        return !Equals(left, right);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj))
        {
            return false;
        }

        if (ReferenceEquals(this, obj))
        {
            return true;
        }

        if (obj.GetType() != typeof(PropertyPathCollection))
        {
            return false;
        }

        return Equals((PropertyPathCollection)obj);
    }

    public override int GetHashCode()
    {
        return this.components.Aggregate(0, (t, n) => (t + n.GetHashCode()));
    }

    #endregion

    #region Explicit Interface Methods

    IEnumerator<PropertyInfo> IEnumerable<PropertyInfo>.GetEnumerator()
    {
        return this.components.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.components.GetEnumerator();
    }

    #endregion
}

And the usage is like this:

var context = this.DbContext;
var adapter = context as IObjectContextAdapter;
var objectContext = adapter.ObjectContext;

objectContext.CreateObjectSet<TEntity>().RecordExists(instance);
like image 162
Diego Modolo Ribeiro Avatar answered Oct 22 '22 23:10

Diego Modolo Ribeiro


When you call _dbContext.Set<TEntity>().Add(root); it tells the context that all the entities in the graph have a state of EntityState.Added. But 2 entities with the same ID and EntityState.Added are not allowed, and an exception is thrown.

Try changing the line to _dbContext.Set<TEntity>().Attach(root);. That will put the graph into the context with EntityState.Unchanged. Entities that are already in the context in some other state will have their state set to Unchanged.

Now you should be able to go fix the states.

Struck this answer out because Attach causes the same error - as per comment

References:

When to use DbSet<T>.Add() vs DbSet<T>.Attach()

Why does Entity Framework Reinsert Existing Objects into My Database?

Making Do with Absent Foreign Keys

Provide better support for working with disconnected entities

DbSet.Attach method

like image 3
Colin Avatar answered Oct 22 '22 21:10

Colin