Thursday, July 31, 2014

EF Code First: Create table out of Enum

Imagine you have a number of enums in your code. They are defining static data and you want those enums to be mirrored in database as tables with appropriate data. Here is the tip how to quickly map enums to your tables and to seed them with enum values.

Lets say I have a lot of enums like this:

public enum RatingEnum
{
    [Description("Something really good")]
    Brilliant = 1,
    Good = 2,
    Average = 3,
    Bad = 4,
    [Description("Something really bad")]
    Terrible = 5
}

And all I want is to create for each enum a table like this:

First of all lets define base class for future "enum" entities:

public class EnumBase<TEnum> where TEnum : struct
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public virtual int Id { get; set; }

    [Required]
    [MaxLength(100)]
    public virtual string Name { get; set; }

    [MaxLength(100)]
    public virtual string Description { get; set; }
}

Now create entity itself, derived from EnumBase:

[Table("Ratings")]
public class Rating : EnumBase<RatingEnum>
{
}

What’s left is to write a small helper, which will fill entities with enum data:

public class Helpers
{
    public static void SeedEnumData<TData, TEnum>(IDbSet<TData> items)
        where TData : EnumBase<TEnum>
        where TEnum : struct
    {
        var etype = typeof(TEnum);

        if (!etype.IsEnum)
            throw new Exception(string.Format("Type '{0}' must be enum", etype.AssemblyQualifiedName));

        var ntype = Enum.GetUnderlyingType(etype);

        if (ntype == typeof(long) || ntype == typeof(ulong) || ntype == typeof(uint))
            throw new Exception();

        foreach (TEnum evalue in Enum.GetValues(etype))
        {
            var item = Activator.CreateInstance<TData>();

            item.Id = (int)Convert.ChangeType(evalue, typeof(int));

            if (item.Id <= 0)
                throw new Exception("Enum underlying value must be positive");

            item.Name = Enum.GetName(etype, evalue);

            item.Description = GetEnumDescription(evalue);

            items.Add(item);
        }
    }

    public static string GetEnumDescription<TEnum>(TEnum item)
    {
        Type type = item.GetType();

        var attribute = type.GetField(item.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false).Cast<DescriptionAttribute>().FirstOrDefault();
        return attribute == null ? string.Empty : attribute.Description;
    }
}

That’s it. Lets say test context looks like this:

public class CustomDbContext: DbContext
{
    public IDbSet<Rating> Ratings { get; set; }
}

Now all I need to create tables and seed the data is:

using (var context = new CustomDbContext())
{
    Helpers.SeedEnumData<Rating,RatingEnum>(context.Ratings);
    context.SaveChanges();
}

4 comments :

  1. What more appropriate place to seed the data? seed method? OnModelCreating method?

    ReplyDelete
  2. What about related entities where current enum is a property and no relations created, for example:
    public class Author
    {
    public AuthorId {get;set;}
    public RatingEnum Rating {get;set;}
    ...
    }

    ReplyDelete
    Replies
    1. Unfortunately that's not gonna happen. This helper used mostly to simplify seeding data. In your case you still need a relation and your example above will look like:

      public class Author
      {
      public AuthorId {get;set;}

      public int RatingId{ get; set; }

      [ForeignKey("RatingId")]
      public Rating Rating {get;set;}
      ...
      }

      Delete