Thursday, March 21, 2013

Lookup table design for Entity Framework Code First

I’ve been learning Entity Framework Code First and would like to share a design I developed for Lookup tables.  The project requirement is to make reporting as easy as possible for the client because they will be writing all of their own queries and reports, however I want to leverage lookup tables in my entities and also protect the client from data issues do to misspellings.  Therefore I designed my lookup tables to have the value field also be the PK/FK and I added an enumerator field to support compile integrity in my code.  Note that Entity Framework 5 is required for enumerator support in Code First.

I defined a common base class for all of my lookup table POCO classes:

  1. public abstract class LookupModel<TEnum> : ILookupModel
  2. {
  3.     [Key, MaxLength(50)]
  4.     public string Value { get; set; }
  5.  
  6.     public TEnum ValueCode { get; set; }
  7. }

Here is an example implementation for a Gender lookup POCO class:

  1. [Table("Genders")]
  2. public class GenderLookup : LookupModel<Gender>
  3. {
  4.     public GenderLookup() {}
  5. }

I defined a Gender enum with some extension methods for sorting and for translating between the database values and the enumerated values.

  1. public enum Gender
  2. {
  3.     NA = 0, Male, Female
  4. }
  5.  
  6. public static class GenderExtensions
  7. {
  8.     public static int SortOrder(this Gender value)
  9.     {
  10.         switch (value)
  11.         {
  12.             case Gender.Male:
  13.                 return 1;
  14.             case Gender.Female:
  15.                 return 2;
  16.             case Gender.NA:
  17.                 return int.MinValue;
  18.             default:
  19.                 throw new ArgumentException("Invalid Gender value '" + value.ToString() + "'");
  20.         }
  21.     }
  22.     public static string ToDatabaseValue(this Gender value)
  23.     {
  24.         switch (value)
  25.         {
  26.             case Gender.Male:
  27.                 return "Male";
  28.             case Gender.Female:
  29.                 return "Female";
  30.             case Gender.NA:
  31.                 return null;
  32.             default:
  33.                 throw new ArgumentException("Invalid Gender value '" + value.ToString() + "'");
  34.         }
  35.     }
  36.     public static Gender ToGenderFromDatabaseValue(this string value)
  37.     {
  38.         foreach (Gender key in Enum.GetValues(typeof(Gender)))
  39.         {
  40.             if (string.Compare(value, key.ToDatabaseValue(), true) == 0)
  41.                 return key;
  42.         }
  43.         return Gender.NA;
  44.     }
  45. }

Seeding the Genders table is made simple by looping through the Gender enumerator:

  1. internal static List<GenderLookup> SeedGenders(Context context)
  2. {
  3.     List<GenderLookup> luList = new List<GenderLookup>();
  4.     foreach (Gender key in Enum.GetValues(typeof(Gender)))
  5.     {
  6.         if (key != Gender.NA)
  7.             luList.Add(new GenderLookup() { Value = key.ToDatabaseValue(), ValueCode = key });
  8.     }
  9.     context.Genders.AddOrUpdate(lu => lu.Value, luList.ToArray());
  10.     return luList;
  11. }

Including the enumerated property on my POCO makes LINQ queries a breeze:

  1. private static GenderLookup GetMaleGender(System.Data.Entity.DbContext context)
  2. {
  3.     return context.Set<GenderLookup>().First(l => l.ValueCode == Gender.Male);
  4. }

7 comments:

  1. I've been searching around for a decent answer to this problem. Your solution looks awesome!

    Can you provide an example of how you would go about using the gender lookup in another POCO (e.g. a Person)? How to set the gender, search by gender etc.

    ReplyDelete
    Replies
    1. I fail to see where the "GenderExtensions" come into play.
      I'm guessing my "Person" POCO look something like:

      public partial class Person {
      public int ID { get; set; }
      public string Name { get; set; }
      public Gender Gender { get; set; }
      }

      So I can still use the Gender enum to search:
      var person = (context.Donations.Where(d => d.Gender == Gender.Male)).FirstOrDefault();

      And the Lookup table "Genders" is there more as a visual reference.

      Delete
  2. You are correct that the GenderExtensions aren't necessary, but I have found the extension methods to be handy under a variety of circumstances - such as seeding the lookup table data or doing string comparisons of lookup values that have been populated into a dropdown list or other user control. If you want to use the Lookup table entities then your Person POCO might look like this:

    public partial class Person {
    public int ID { get; set; }
    public string Name { get; set; }
    public virtual GenderLookup Gender { get; set; }
    }

    var person = (context.Donations.Where(d => d.Gender.ValueCode == Gender.Male)).FirstOrDefault();

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
  3. Thanks a mill for your reply, extremely helpful though I'm not quite there yet!

    Are you sure that GenderLookup should be used in the Person POCO?
    When I try the following:

    context.Persons.Add(new Person() { Name = "Joe Bloggs", Gender = new GenderLookup() { Value = Gender.Male.ToDatabaseValue(), ValueCode = Gender.Male });

    EF tries to insert the Gender value of Person into the Gender table:
    "Violation of PRIMARY KEY constraint 'PK_dbo.Genders'. Cannot insert duplicate key in object 'dbo.Genders'. The duplicate key value is (Male). The statement has been terminated."

    ReplyDelete
    Replies
    1. Apologies for the spamming, EF n00b. All sorted. Thanks a million for the blog, extremely helpful.

      Delete
    2. I just posted some code that hopefully will answer your remaining questions about relating Person to GenderLookup using code-first. http://codemeek.blogspot.com/2013/05/entity-model-design-and-configuration.html I'm glad this is helpful to you and no worries on your questions, I've been there so many times myself.

      Delete