RSS

Find primary keys from entities from DbContext

30 Mar

By using DbContext you can use the DbSet.Find method to find entities by the primary key(s). To find entities attached to a DbContext by using the DbSet.Find method you have to provide all entity key values, i.e. primary key columns, that identifies the entity. This is very easy if you have a primary key consisting of only one column and/or if this columns have always the same name (like Id).

context.Set().Find(myEntity.Id);

But in the case when you have different key names and sometime more than one column that defines the key this can be very tricky and error-prone. Not to mention if you change the key-name, or add column to the primary key. You should remember your keys, write and change them everywhere in the code as follows:

context.Set<EntityName>().Find(myEntity.Key1,myEntity.Key2,myEntity.Key3);

Well it works, but is is not beautiful. Another drawback is that you have to put the keys in the exact order as they are defined as foreign key. Therefore following snippet is not equivalent to the previous one.

context.Set<EntityName>().Find(myEntity.Key3,myEntity.Key2,myEntity.Key1);

Therefore, I thought that it might be useful to create a method that retrieves all the foreign key values for a specific entity automatically. In order to do that I have to use the old DatabaseContext from the Entity Framework because the DbContext does not offer such specific methods.

public string[] GetKeyNames(DbContext context) where T : class
{
  ObjectSet objectSet = ((IObjectContextAdapter) context).ObjectContext.CreateObjectSet();
  string[] keyNames = objectSet.EntitySet.ElementType.KeyMembers
                                                     .Select(k => k.Name)
                                                     .ToArray();
  return keyNames;
}

This method allows me to retrieve the name for every primary key. This keys can then be used for the following method that returns an object array with all the entities foreign key values.

public object[] GetKeys(T entity, DbContext context) where T : class
{
  var keyNames = GetKeyNames(context);
  Type type = typeof (T);

  object[] keys = new object[keyNames.Length];
  for (int i = 0; i < keyNames.Length; i++)
  {
    keys[i] = type.GetProperty(keyNames[i]).GetValue(entity, null);
  }
  return keys;
}

So we have a nice and compact way to retrieve the keys for an entity:

var keyValues = GetKeys(myEntity,context1);
var newEntity = context2.Set().Find(keyValues);

With the method GetKeyNames you get immediately the key names for an entity, but this will not work if you have an inheritance hierarchy between the entities.

abstract class A { }
class B : A { }
class C : A { }

If you are now trying following code snippet

B myEntity = [...]
var keyValues = GetKeys(myEntity,context1);

you will get an exception saying:

There are no EntitySets defined for the specified entity type ‘B’. If ‘B’ is a derived type, use the base type instead.

This is because you can create only an EntitySet for the base type of the entity hierarchy. In order to fix that I modified the listing above to following:

public string[] GetKeyNames<T>(DbContext context) where T : class
{
  Type t = typeof(T);

  //retrieve the base type
  while (t.BaseType != typeof(object))
  {
    t = t.BaseType;
  }

  ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;

  //create method CreateObjectSet with the generic parameter of the base-type
  MethodInfo method = typeof(ObjectContext)
                            .GetMethod("CreateObjectSet",Type.EmptyTypes)
                            .MakeGenericMethod(t);
  dynamic objectSet = method.Invoke(objectContext, null);
  IEnumerable<dynamic> keyMembers = objectSet.EntitySet.ElementType.KeyMembers;
  string[] keyNames = keyMembers.Select(k => (string)k.Name).ToArray();
  return keyNames;
}

Because retreiving the primary key values from the ObjectContext is expensive I modified the code above and added caching for the primary key names:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Reflection;

public sealed class EntityKeyHelper
{
  private static readonly Lazy<EntityKeyHelper> LazyInstance = new Lazy<EntityKeyHelper>(() => new EntityKeyHelper());
  private readonly Dictionary<Type, string[]> _dict = new Dictionary<Type,string[]>();
  private EntityKeyHelper() {}

  public static EntityKeyHelper Instance
  {
    get { return LazyInstance.Value; }
  }

  public string[] GetKeyNames<T>(DbContext context) where T : class
  {
    Type t = typeof(T);

    //retreive the base type
    while (t.BaseType != typeof(object))
    {
      t = t.BaseType;
    }

    string[] keys;

    _dict.TryGetValue(t, out keys);
    if (keys != null)
    {
      return keys;
    }

    ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;

    //create method CreateObjectSet with the generic parameter of the base-type
    MethodInfo method = typeof(ObjectContext).GetMethod("CreateObjectSet",Type.EmptyTypes)
                                             .MakeGenericMethod(t);
    dynamic objectSet = method.Invoke(objectContext, null);

    IEnumerable<dynamic> keyMembers = objectSet.EntitySet.ElementType.KeyMembers;
    string[] keyNames = keyMembers.Select(k => (string)k.Name).ToArray();

    _dict.Add(t, keyNames);

    return keyNames;
  }

  public object[] GetKeys<T>(T entity, DbContext context) where T : class
  {
    var keyNames = GetKeyNames<T>(context);
    Type type = typeof (T);

    object[] keys = new object[keyNames.Length];
    for (int i = 0; i < keyNames.Length; i++)
    {
      keys[i] = type.GetProperty(keyNames[i]).GetValue(entity, null);
    }
    return keys;
  }
}
Advertisements
 
4 Comments

Posted by on March 30, 2013 in C-Sharp, EF

 

Tags: ,

4 responses to “Find primary keys from entities from DbContext

  1. Avraham Y. Kahana

    March 30, 2015 at 17:49

    Please post the whole cs source, including the usings.
    Also, make sure it compiles – seems you have a typo (lower case) at line 3, “Lazy” – should it be EntityKeyHelper ?

     
  2. Avraham Y. Kahana

    March 30, 2015 at 17:52

    Still at line 3, the constructor initialization for Lazy isn’t missing the generic type parameter ?

     
    • Michael Mairegger

      March 31, 2015 at 08:26

      Thank you for the feedback. I have corrected the issues and added the `using` statements.

       
  3. Nick Strupat

    December 16, 2015 at 04:42

    Thank you for the code on how to get the names of the primary key properties regardless of column name overriding AND attributes/model-builder. I came up with the following function.

    static Func<TEntity, IEnumerable<KeyValuePair>> GetPrimaryKeyFunc(DbContext context) where TEntity : class {
    IObjectContextAdapter oca = context;
    var keyNames = oca.ObjectContext.CreateObjectSet().EntitySet.ElementType.KeyMembers.Select(x => x.Name);
    var keyProperties = typeof(TEntity).GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(x => keyNames.Contains(x.Name));
    var keyPropertyGetters = keyProperties.Select(x => new { x.Name, Getter = GetValueGetter(x)});
    return entity => keyPropertyGetters.Select(x => new KeyValuePair(x.Name, x.Getter(entity))).ToArray();
    }

     

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: