This afternoon I Built a mechanism for mapping Enums to Lookup Tables in EF Code First - using complex types and implicit operators. The steps are as follows:
Step 1 – the Enum
// Avoid 0 values – SQL INT default value
public enum JobInstanceStateEnum : int
{
Ok = 1,
MissingResources = 2,
DataFailure = 3,
Aborted = 4
}
Step 2 – the POCO for generating the Lookup Table
// As we are using id as FK, and it must match the enum, overide convention of identity keygen
public class JobInstanceStateLookup
{
[DatabaseGenerated(DatabaseGenerationOption.None)]
public int JobInstanceStateLookupId { get; set; }
public string Value { get; set; }
}
Step 3 – the POCO for generating the Entity Table
// Entity
public class JobInstance
{
public int JobInstanceId { get; set; } // convention PK
public JobInstanceState Status { get; set; } // enum ComplexType mapper
public string blah { get; set; }
}
Step 4 – the ComplexType POCO for mapping the enum
// Map
[ComplexType]
public class JobInstanceState
{
// FK + navigation property (must be virtual)
[ForeignKey("JobInstanceStateLookup")]
public int JobInstanceStateLookupId { get; set; }
public virtual JobInstanceStateLookup JobInstanceStateLookup { get; set; }
// ctors
public JobInstanceState() : this(JobInstanceStateEnum.Ok) {}
public JobInstanceState(JobInstanceStateEnum value)
{
JobInstanceStateLookupId = (int)value;
}
// implicit operators for auto casting the complex type to enum val
public static implicit operator JobInstanceStateEnum(JobInstanceState type)
{
return (JobInstanceStateEnum)type.JobInstanceStateLookupId;
}
public static implicit operator JobInstanceState(JobInstanceStateEnum type)
{
return new JobInstanceState(type);
}
}
Step 5 – DBContext and DBSets
// DbContext – the Generator
public class SpitfireContext : DbContext
{
public DbSet<JobInstanceStateLookup> JobInstanceStateLookup { get; set; }
public DbSet<JobInstance> JobInstances { get; set; }
…
}
Step 6 – DB Initializer
// Initialize the DB – iterate over enum to populate lookup table
public class Initializer : IDatabaseInitializer<MyContext>
{
public void InitializeDatabase(MyContext context)
{
if (!context.Database.Exists() || !context.Database.CompatibleWithModel(false))
{
context.Database.Delete();
context.Database.Create();
var jobInstanceStateList = EnumExtensions.ConvertEnumToDictionary<JobInstanceStateEnum>().ToList();
jobInstanceStateList.ForEach(kvp => context.JobInstanceStateLookup.Add(
new JobInstanceStateLookup()
{
JobInstanceStateLookupId = kvp.Value,
Value = kvp.Key
}));
context.SaveChanges();
}
}
}
Step 7 - Unit Test
[TestMethod]
public void TestComplexTypeWithEnumToLookup()
{
// Arrange:
DbDatabase.SetInitializer(new MyContext.Initializer());
var db = new MyContext();
var ji = new JobInstance()
{
Blah = "xxx",
Status = JobInstanceStateEnum.Aborted
};
ji.Status = JobInstanceStateEnum.MissingResources; // The power of implicit operators
// Act:
db.JobInstances.Add(ji);
db.SaveChanges();
// The type 'JobInstanceState' is mapped as a complex type.
// The Set method, DbSet objects, and DbEntityEntry objects can only be used with entity types, not complex types.
// so theres no easy way to navigate to db.JobInstances.First().Status.JobInstanceStateLookup
// which is good (albiet a leaky abstraction) because Code First doesnt support RO !
var ji2 = db.JobInstances.First().Status;
var enumString = JobInstanceStateEnum.MissingResources.ToString();
var lookup = db.JobInstanceStateLookup.Where(l => l.Value == enumString).FirstOrDefault();
// Assert:
Assert.IsTrue(db.JobInstances.Count() > 0);
Assert.AreEqual(ji2.JobInstanceStateLookupId, lookup.JobInstanceStateLookupId);
}
Enjoy!