使用Attribute解决在Entity Framework中无法使用默认时间的问题
相信很多使用过Entity Framework的人都遇到过这个DateTime类型的Entity字段无法使用默认值的问题。
比方说在声明一个Entity的时候。
1 [Table("ctUser")] 2 public class ctUser 3 { 4 [Key] 5 public int UserID { get; set; } 6 7 [Required(ErrorMessage = "电子邮件不能为空!")] 8 [MaxLength(200, ErrorMessage = "电子邮件的字符长度不能超过200!")] 9 public string Email { get; set; } 10 11 [Required(ErrorMessage = "密码不能为空!")] 12 [MaxLength(32, ErrorMessage = "密码的字符长度不能超过32!")] 13 public string Password { get; set; } 14 15 [Required(ErrorMessage = "真实姓名不能为空!")] 16 [MaxLength(20, ErrorMessage = "真实姓名的字符长度不能超过20!")] 17 public string RealName { get; set; } 18 }
为了能够调用对数据库的具体操作我创建了一个简单的继承“DbContext”的数据库操作类型。
1 public class ctDbContext : DbContext 2 { 3 public DbSet<ctUser> Users { get; set; } 4 5 public ctDbContext() 6 : base("DefaultConn") 7 { 8 } 9 }
ctDbContext的实现相当简单,一个构造函数为基类传递了web.config文件中链接字符串的名称。一个DbSet<ctUser>类型的public属性。使用这个类型的时候也非常简单,比方说,当我想要添加一个用户到数据库中时
1 using (ctDbContext ctx = new ctDbContext()) 2 { 3 ctUser user = new ctUser() 4 { 5 Email = "test@163.com", 6 Password = "123456", 7 RealName = "Kevin.Cai" 8 }; 9 10 ctx.Users.Add(user); 11 ctx.SaveChanges(); 12 }
这么几行代码就实现了向数据库中添加一个用户的功能。如果你还没有创建数据库文件,它甚至能直接帮你把数据库文件都生成好。这种东西相当适合我这种懒人。
在很多情况下,我们在设计数据表的时候都会习惯的创建一个“CreateDate”或者“RegisterDate”或者其它任何名字,反正是用来记录当前时间的一个字段。这个在数据库中直接使用的时候是相当简单的,只需要在“Default Value”中使用“GETDATE()”函数即可,我们已经习惯了在创建一条数据的时候不去手动获取时间。但是,对于EF来说,这却是一个问题,因为在我向Entity中添加一个DateTime类型的字段时,如果不设置它,它将使用该类型的默认值。而且在EF中根本没有DefaultValueAttribute这种东西。好吧,既然它没有,我们自己来创建一个。
实际上DefaultValueAttribute的原理还是蛮简单。首先给需要使用默认值的字段加上DefaultValueAttribute,然后创建一条新数据的时候将有DefaultValueAttribute标记的字段值修改一下就可以了。
1 [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 2 public class DefaultValueAttribute : Attribute 3 { 4 public Type Type { get; set; } 5 public string Property { get; set; } 6 7 public DefaultValueAttribute(Type type, string property) 8 { 9 this.Type = type; 10 this.Property = property; 11 } 12 }
在DefaultValueAttribute中包含了两个public字段,这两个字段是为了动态获取值准备的。因为Attribute的编译时特性,我们无法传递动态的对象,我实在不知道还有没有其他的方法传递DateTime.Now这种运行时产生的值,不过这里我暂且使用反射的方法。因为这里没有传递对象,所以请确保Property是静态的。而DateTime的Now属性恰好就是个静态的。
在一般情况下,默认值的适用范围是在向数据库中“添加”一条数据时,并且“未设置”这个值。而我们修改具体值的时间肯定在数据提交之前,数据提交之后当然也可以修改,不过稍微有点性能概念的人我想也是不会在数据提交之后再修改的,而且在数据提交之前修改也并不复杂。按照这些原则我对ctDbContext类型做了一些修改。
1 public class ctDbContext : DbContext 2 { 3 public DbSet<ctUser> Users { get; set; } 4 5 public ctDbContext() 6 : base("DefaultConn") 7 { 8 } 9 10 public override int SaveChanges() 11 { 12 IEnumerable<DbEntityEntry> entityList = this.ChangeTracker.Entries(); 13 foreach (DbEntityEntry entity in entityList) 14 { 15 if (entity.State != System.Data.EntityState.Added) 16 continue; 17 18 DefaultValueAttribute.UpdateValue(entity.Entity); 19 } 20 21 return base.SaveChanges(); 22 } 23 }
新的ctDbContext重写了SaveChanges方法。对所有添加数据的操作做了一次DefaultValueAttribute的UpdateValue操作。UpdateValue做为静态方法需要被添加到DefaultValueAttribute类型中。
1 [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] 2 public class DefaultValueAttribute : Attribute 3 { 4 public Type Type { get; set; } 5 public string Property { get; set; } 6 7 public DefaultValueAttribute(Type type, string property) 8 { 9 this.Type = type; 10 this.Property = property; 11 } 12 13 public static void UpdateValue(object obj) 14 { 15 Type type = obj.GetType(); 16 PropertyInfo[] pInfoList = type.GetProperties(); 17 if (pInfoList == null || pInfoList.Length == 0) 18 return; 19 20 foreach (PropertyInfo pInfo in pInfoList) 21 { 22 DefaultValueAttribute attr = GetAttribute(pInfo); 23 if (attr == null) 24 continue; 25 26 object original = pInfo.GetValue(obj, null); 27 if (!IsDefaultValue(attr.Type, original)) 28 continue; 29 30 object value = attr.GetPropertyValue(); 31 if (value == null) 32 continue; 33 34 pInfo.SetValue(obj, value, null); 35 } 36 } 37 38 private static DefaultValueAttribute GetAttribute(PropertyInfo pInfo) 39 { 40 object[] attrs = pInfo.GetCustomAttributes(typeof(DefaultValueAttribute), true); 41 if (attrs == null || attrs.Length != 1) 42 return null; 43 44 return attrs[0] as DefaultValueAttribute; 45 } 46 47 private static bool IsDefaultValue(Type type, object value) 48 { 49 if (type.IsValueType) 50 return Activator.CreateInstance(type).Equals(value); 51 else 52 return value == null; 53 } 54 55 private object GetPropertyValue() 56 { 57 if (this.Type == null || string.IsNullOrEmpty(this.Property)) 58 return null; 59 60 PropertyInfo pInfo = this.Type.GetProperty(this.Property); 61 return pInfo.GetValue(null, null); 62 } 63 }
新的DefaultValueAttribute复杂了一些。其中IsDefaultValue通过判断值是否是默认值来判断该值是否被修改过。我必须要承认这几条函数的性能不怎么样,可能会有装箱拆箱操作。实际上,使用了反射性能都不怎么样。
当这些都写好了之后我们终于可以使用DefaultValueAttribute了,新的Entity被添加了CreateDate字段并且使用了DefaultValueAttribute特性。
1 [Table("ctUser")] 2 public class ctUser 3 { 4 [Key] 5 public int UserID { get; set; } 6 7 [Required(ErrorMessage = "电子邮件不能为空!")] 8 [MaxLength(200, ErrorMessage = "电子邮件的字符长度不能超过200!")] 9 public string Email { get; set; } 10 11 [Required(ErrorMessage = "密码不能为空!")] 12 [MaxLength(32, ErrorMessage = "密码的字符长度不能超过32!")] 13 public string Password { get; set; } 14 15 [Required(ErrorMessage = "真实姓名不能为空!")] 16 [MaxLength(20, ErrorMessage = "真实姓名的字符长度不能超过20!")] 17 public string RealName { get; set; } 18 19 [DefaultValue(typeof(DateTime), "Now")] 20 public DateTime CreateDate { get; set; } 21 }
就这样,如果你像我一样懒得每次去设置当前时间值的话,Entity Framework终于可以实现了,恼人的overflow异常(数据库和DateTime的取值范围不同)将不再出现。
切记,这是Demo,不是项目中的代码。