Question

Some time ago, I wanted to implement a method that was able to determine whether do an insert or an update on a given entity, so I didn't have to expose "Insert" and "Update" methods, but just a simple "InsertOrUpdate".

The part of code that finds out if the entity is new or not, is this:

    public virtual T GetEntityByPrimaryKey<T>(T entity) where T : class
    {
        var entityType = entity.GetType();
        var objectSet = ((IObjectContextAdapter)this.DatabaseContext).ObjectContext.CreateObjectSet<T>();
        var keyNames = objectSet.EntitySet.ElementType.KeyMembers.Select(edmMember => edmMember.Name);
        var keyValues = keyNames.Select(name => entityType.GetProperty(name).GetValue(entity, null)).ToArray();

        return this.DatabaseContext.Set<T>().Find(keyValues);
    }

And the InsertOrUpdate method is this:

    public virtual T InsertOrUpdate<T>(T entity) where T : class
    {
        var databaseEntity = this.GetEntityByPrimaryKey(entity);

        if (databaseEntity == null)
        {
            var entry = this.DatabaseContext.Entry(entity);

            entry.State = EntityState.Added;

            databaseEntity = entry.Entity;
        }
        else
        {
            this.DatabaseContext.Entry(databaseEntity).CurrentValues.SetValues(entity);
        }

        return databaseEntity;
    }

Now, this approach works wonders as long as the "primary key" of the object is determined by the code. Valid examples are GUIDs, HI-LO algorithms, natural keys, etc.

This, however, is horribly broken for the "database generated identity" scenario, and the reason is quite simple: since my "Id" in the code will be 0 for all the objects I'm going to insert, the method will consider them the same. If I'm adding 10 objects, the first will result "new", but the next nine will result "already existing". This is due to the fact that the "Find" method of EF reads data from the objectcontext and only if it's not present it goes down to the database to make the query.

After the first object, an entity of the given type with Id 0 will be tracked. Successive calls will result in an "update", and this is just wrong.

Now, I know that database generated ids are evil and absolutely not good for any ORM, but I'm stuck with those and I need to either fix this method or remove it entirely and fall back to separate "Insert" and "Update" methods and delegate to the caller the task to determine what to do. Since we have a highly decoupled solution I'd rather avoid to do this.

If anyone can help and find a way to fix the GetEntityByPrimaryKey method, that would be awesome.

Thanks.

Was it helpful?

Solution

I have the following suggestions:

1:
I would add something like a IsTransient property to the entities. It returns true if the PK is 0, otherwise it returns false.
You could use this property to change your method as follows:

  1. IsTransient == true? -> Insert
  2. IsTransient == false? -> Your existing code with the database check

Make that property virtual and you will even be able to support entities with "strange" PK by overriding IsTransient.

2:
If you don't like adding this to your entities, you still could create an extension method that encapsulates this logic. Or even add that check directly into your InsertOrUpdate.

As you don't have a common base class for your entities those suggestions will become a bit tedious. You basically would have to have one extension method per entity.

3:
If you have a convention in place for the PK, you could use dynamic to access the ID property:

dynamic dynamicEntity = entity;
if(dynamicEntity.Id == 0)
{
    // Insert
}
else
{
    // Current code.
}

4:
Seeing that adding a transient entity to the context breaks things for all following transient items, it might be a good idea to add the transient items to a list instead of the context.
Only add them to the context when it is going to be committed. I am sure there is a hook for this, which you can use:

List<object> _newEntities;

private override OnCommit()
{
    foreach(var newEntity in newEntities)
        DatabaseContext.Entry(newEntity).State = EntityState.Added;
}

public virtual T InsertOrUpdate<T>(T entity) where T : class
{
    var databaseEntity = this.GetEntityByPrimaryKey(entity);

    if (databaseEntity == null)
        _newEntities.Add(entity);
    else
        this.DatabaseContext.Entry(databaseEntity).CurrentValues.SetValues(entity);

    return databaseEntity;
}

OTHER TIPS

since my "Id" in the code will be 0 for all the objects I'm going to insert

It seems you're expecting uniqueness on the keys, when you're not providing them. Would it be possible to initialize them to a unique, negative number? (something that is not a valid value for actual db entries)

I had a similiar problem (that was with self-tracking objects being able to tell if two not-yet-inserted child objects are the same key-wise or not...), and this solved it.

Given your POCOs are free of DB filth, you must have used Fluent API to declare the DB Generated Key info. So perhaps the DbSets on the CONTEXT contain this DB Generated flag.

I Use an Extension to get All the POCO used in a Context. Perhaps with enough reflection you can find a property or attribute that is useful as the DB generated flag. The rest is then already clear.

Perhaps this a useful starting point:

 public static List<string> GetModelNames(this DbContext context ) {
      var model = new List<string>();
      var propList = context.GetType().GetProperties();
      foreach (var propertyInfo in propList)
      {
      if (propertyInfo.PropertyType.GetTypeInfo().Name.StartsWith("DbSet"))
      {
          model.Add(propertyInfo.Name);
          var innerProps = propertyInfo.GetType().GetProperties(); // added to snoop around in debug mode , can your find anything useful?
      }
      }


      return model;
  }
 public static List<string> GetModelTypes(this DbContext context)
 {
     var model = new List<string>();
     var propList = context.GetType().GetProperties();
     foreach (var propertyInfo in propList)
     {
         if (propertyInfo.PropertyType.GetTypeInfo().Name.StartsWith("DbSet"   ))
         {
             model.Add(propertyInfo.PropertyType.GenericTypeArguments[0].Name);
         }
     }


     return model;
 }
}   
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top