5

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?

Masoud
  • 8,020
  • 12
  • 62
  • 123
  • How do you "pass a graph"? And why do your entities implement `INotifyPropertyChanged`? – Gert Arnold Jan 04 '13 at 18:44
  • the root object is graph of TEntities, i implement INotifyPropertyChanged to could binding objects to winUI. – Masoud Jan 05 '13 at 04:14
  • @Masoud Is the same `ID` value -1? Perhaps you just need to correctly mark the items for add as opposed to update or something as simple as that. The fact that you have ID collisions is not necessarily an EF concern, it sounds like you are doing a lot of stuff outside of EF. – Adam Houldsworth Dec 09 '13 at 09:15
  • @AdamHouldsworth I used Id and override it in each entity. e.g in Order class the Id return OrderId. – Masoud Dec 09 '13 at 09:34
  • @Masoud Yes but are you setting values into this ID field? – Adam Houldsworth Dec 09 '13 at 09:54
  • @AdamHouldsworth Yeah – Masoud Dec 09 '13 at 10:20
  • @Masoud Then the issue isn't getting EF to understand a different ID, you need to stop assigning duplicate IDs to distinct objects. – Adam Houldsworth Dec 09 '13 at 10:22
  • @AdamHouldsworth I want to all my entities has unique name for their Id's, because I want write a general method to find duplicate entities(that works with this Id). whats your solution? – Masoud Dec 09 '13 at 10:31
  • @AdamHouldsworth Because of Primary Key Convention of EF codefirst, I renamed Id to DatabaseId. – Masoud Dec 09 '13 at 10:54
  • @Masoud Conventions can be changed: http://msdn.microsoft.com/en-gb/data/jj679962.aspx so you can simply remove that convention instead of renaming your properties. – Adam Houldsworth Dec 09 '13 at 10:56

3 Answers3

4

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);
3

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

Community
  • 1
  • 1
Colin
  • 22,328
  • 17
  • 103
  • 197
  • If I change _dbContext.Set().Add(root); to _dbContext.Set().Attach(root); when the control goes to this line, I get the same error, "An object with the same key already exists ...". – Masoud Dec 22 '13 at 09:49
0

I changed my BaseEntity to

public class BaseEntity
{
   public int Id {get; set;}
   public States State { get; set; }
   public bool MustDelete {get; set;} 

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

And also changed following methods in my BaseDomainService<T> class:

public class BaseDomainService<T> where T : class
{
    protected readonly DbContext _dbContext;

    public BaseDomainService(IUnitOfWork uow)
    {
        _dbContext = (DbContext)uow;
    }
    .....


    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>())
        {
            if (FoundAnEntityWithSameKeyInDbContext<TEntity>(entry))
                entry.State = EntityState.Detached;
            else
            {
                BaseEntity stateInfo = entry.Entity;
                if (stateInfo.MustDelete == true)
                    entry.State = EntityState.Detached;
                else
                    entry.State = ConvertState(stateInfo.State);
            }
        }
    }

    private bool FoundAnEntityWithSameKeyInDbContext<TEntity>(DbEntityEntry<BaseEntity> entry) where TEntity : BaseEntity
    {
        var tmp = _dbContext.ChangeTracker.Entries<BaseEntity>().Count(t => t.Entity.Id == entry.Entity.Id && t.Entity.Id != 0 && t.Entity.GetType() == entry.Entity.GetType());
        if (tmp > 1)
            return true;
        return false;
    }
}

So, the problem solved.

Masoud
  • 8,020
  • 12
  • 62
  • 123