Code-First 產生 Enumeration Code Table
前言
列舉型別 (Enumeration) 是由一組稱為列舉值清單的具名常數所構成的獨特型別,通常會將一些可列舉資料(如: 評等 甲、乙、丙、丁)宣告為此型態類別,以方便在程式中利用其"具名"特性來增加程式碼的可讀性。很不巧的在Entity Framework中,Code-First在生成實體資料庫時僅將列舉型別當作數字型態,因此在DB資料表中對應該列舉型別屬性的欄位型態將為數字,不具任何顯性意義。
因此筆者希望有一個對應各列舉型別選項的Code Table被生成(如下圖所示),而其他類別中只要使用到此列舉型別作為屬性型態時,都會與此Code Table建立關聯性。如何實現呢? 請參考以下說明。
實作說明
首先舉一個非常簡單的實例。筆者需要建立一個用戶(SysUser),其中需要紀載這個用戶偏好的介面語系(Language);因為網站所提供的語系只有三種,因此使用列舉方式記載。目前所需物件類別就是SysUser以及LanguageEnum,但由於EF不會針對Enum型別生成Code Table,因此將先從LanguageEnum著手進行調整。
網站提供三種語系,因此建立LanguageEnum來表列各語系
public enum LanguageEnum
{
[Description("zh-TW")]
ZhongHua = 1,
[Description("en-US")]
English = 2,
[Description("ja-JP")]
Japanese = 3
}
由於筆者希望各列舉(Enum)物件都可生成對應資料表,因此建立一個通用泛型EnumBase基礎類別;其中Id將放置Enum值,而Name及Description將分別放置列舉型別常數名稱及描述。
public class EnumBase<TEnum> where TEnum : struct
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public virtual TEnum Id { get; set; }
[Required]
[MaxLength(100)]
public virtual string Name { get; set; }
[MaxLength(100)]
public virtual string Description { get; set; }
}
此時,就可以著手建立Code-First使用的Language類別。
public class Language : EnumBase<LanguageEnum>
{
// Extra Properties
// ...
}
再建立測試用SysUser類別,其中包含上述Language資訊(建立關聯)。
public class SysUser
{
// Properties
[Key]
[DisplayName("User Id")]
[Required(ErrorMessage = "Please enter user id")]
[MaxLength(50, ErrorMessage = "The length of member id cannot exceed 50 chars")]
[Description("Logon system by user id")]
public string SysUserId { get; set; }
[DisplayName("Name")]
[Required(ErrorMessage = "Please enter member name")]
[MaxLength(10, ErrorMessage = "The length of user name cannot exceed 10 chars")]
public string Name { get; set; }
// Foreign Key
public LanguageEnum LanguageId { get; set; }
// Navigation Properties
[JsonIgnore]
[DisplayName("Language")]
public virtual Language Language { get; set; }
}
由於需依據Enum列舉值清單來產生預設資料,因此建立SeedEnumData通用泛型方法,以此方法新增列舉項目至資料表中(Id: Enum值, Name: Enum名稱, Description: Enum描述),方便初始化資料庫時叫用。
public class SeedHelper
{
public static void SeedEnumData<TData, TEnum>(IDbSet<TData> items)
where TData : EnumBase<TEnum>
where TEnum : struct
{
// Check if TEnum is Enum
var etype = typeof(TEnum);
if (etype.IsEnum == false)
throw new ArgumentException(string.Format("Type '{0}' must be enum", etype.AssemblyQualifiedName));
// Add each enum item to DB
foreach (TEnum evalue in Enum.GetValues(etype))
{
var item = Activator.CreateInstance<TData>();
item.Id = evalue;
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;
}
}
接著建立Custom DB Initialier來產生資料表預設資料。利用上述SeedEnumData方法,在初始資料庫時於Seed中新增列舉項目至DB。如果對Custom DB Initialier不熟悉可參考Code-First 建立資料庫初始值文章。
public class GekerDbInitializer : DropCreateDatabaseIfModelChanges<GekerDbContext>
{
protected override void Seed(GekerDbContext context)
{
// add languages
SeedHelper.SeedEnumData<Language, LanguageEnum>(context.Languages);
context.SaveChanges();
// add users
context.SysUsers.Add(new SysUser
{
SysUserId = "wasi.chris",
Name = "chris chen",
LanguageId = LanguageEnum.Japanese
});
context.SaveChanges();
}
}
最後就來建立DbContext,其中包含SysUser及Language資料表。由於我們希望在初始資料庫時,依據Enum列舉值清單來產生預設資料,因此會在建構子中設定上述建立的GekerDbInitialier作為DB Initialier。
public class GekerDbContext : DbContext
{
// Coustructors
public GekerDbContext() : base("GekerDbContext")
{
// Initialize DB by Custom DB Initializer
Database.SetInitializer<GekerDbContext>(new GekerDbInitializer());
}
// Properties
public DbSet<SysUser> SysUsers { get; set; }
public DbSet<Language> Languages { get; set; }
}
實作結果
執行程式後,資料庫及各資料表確實被建立。
並且確實產生LanguageEnum所有列舉項目於Language資料表中,達成所需功能。
參考資訊
http://gentlelogic.blogspot.tw/2014/07/ef-code-first-create-table-out-of-enum.html
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !