0

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.

Matteo Mosca
  • 7,380
  • 4
  • 44
  • 80

3 Answers3

2

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;
}
Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443
  • We're using POCOs, persistence ignorant. We're not going to dirty our objects with stuff related to the persistence method, it's just completely wrong for our decoupled design. We need a solution restricted to EF capabilities. – Matteo Mosca Feb 21 '13 at 10:12
  • @MatteoMosca: Your PI POCO classes *already* contain stuff related to persistence. The ID for example. And I am pretty sure you have an entity base class of some kind to not repeat the ID property everywhere. – Daniel Hilgarth Feb 21 '13 at 10:13
  • @MatteoMosca: Anyway, I amended my answer with some additional considerations. – Daniel Hilgarth Feb 21 '13 at 10:14
  • Nope, we don't have a base class for our POCOs. And I don't think that would be appreciated, though I could try to propose that. Anyway, a fix to my method would still be preferred. – Matteo Mosca Feb 21 '13 at 10:17
  • @MatteoMosca: What about the paragraphs I added to my answer? Is this something that could help you? – Daniel Hilgarth Feb 21 '13 at 10:17
  • Our entities are very different. Some have a tinyint Id, some an int. Some have natural keys. The problem is only for tinyints, smallints, ints, etc. which have an identity increment, not for all entities. That's one of the reasons we don't have a base class. How would I implement that logic in my method without having that discriminator on the POCOs? – Matteo Mosca Feb 21 '13 at 10:19
  • @MatteoMosca: OK, so in that case you need to take a step back and actually define your requirements clearly. Look at each entity in isolation and ask yourself: How to determine if this entity is transient? Is it even possible using its PK? – Daniel Hilgarth Feb 21 '13 at 10:23
  • @MatteoMosca: I don't think that you actually can use EF for this if you don't commit the database context to get the newly created IDs for the newly added entities. – Daniel Hilgarth Feb 21 '13 at 10:24
  • I'm considering your last additions. The "id" convention could be put in place, since we're free to call our properties whatever we want and just map them to the right database columns. As for the transient consideration.. well that is not understandable for objects that have a non nullable key, and where the key can't be assigned from code. That's why I think GUIDs or HI-LO are the way to go. I'll have to reason on your ideas with a couple of colleagues. I'll let you know. – Matteo Mosca Feb 21 '13 at 10:28
  • @MatteoMosca: Please take a look at my latest suggestion before starting the discussion. Maybe this is the easiest way to solve it. – Daniel Hilgarth Feb 21 '13 at 10:32
  • 1
    Your "point 4" seems the way to go. I already have a SaveChanges method which persists whatever is pending on the objectcontext. I can add that temporary list and modify the savechanges method to pick the contents of the list and add them to the context just before committing. I'm going to try this, thank you. – Matteo Mosca Feb 21 '13 at 10:46
0

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.

TDaver
  • 7,164
  • 5
  • 47
  • 94
  • The OP already said that this wasn't an option. See the last comment on his question. – Daniel Hilgarth Feb 21 '13 at 10:34
  • Yeah, as I already stated, many tables use the full range of valid integers, going from negative to positive, so this is not an option. And anyway, this would delegate the job to provide fake IDs to the caller, which I want to avoid, because if the caller doesn't do this, he would not have an exception, but a single insert with the values of the last object provided, which would work and be wrong. – Matteo Mosca Feb 21 '13 at 10:40
0

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;
 }
}   
phil soady
  • 11,043
  • 5
  • 50
  • 95