实体类通常需要和数据库表进行了ORM映射,当你需要添加新的属性时,往往同时也需要在数据库中添加相应的字段并配置好映射关系,同时可能还需对数据访问组件进行重新编译和部署才能有效。而当你开始设计一个通用数据访问组件后,因为项目需求的不同和需求的不断变化演变,很难不能保证不会再添加额外的属性和字段。特别是项目部署运行后,添加一个属性和字段带来的额外维护的工作量可能要远远超过对代码进行调整的工作量。本文提供了属性字段扩展的一种思路,在满足核心字段可通过实体类强类型进行访问的同时,还可通过C# 4.0提供的dynamic特性和Dictionary等技术手段进行字段、属性的扩展,并对数据访问的统一封装,具有通用性强、使用方便、扩展能力强等优点。
本文用到了前面提到的ExtensionObject,其是进行属性扩展原理的核心类,该类继承自DynamicObject类,并实现了, IDynamicMetaObjectProvider,IDictionary<string,object>等接口。和.NET Framework中ExpandoObject类不同的是,继承自DynamicObject的类可以添加实例属性,而ExpandoObject因为被设计为“sealed”类,因此它只能在运行时动态添加属性;另外,继承自DynamicObject的类可实现自定义的对其成员进行管理的一系列方法,因此和ExpandoObject类相比,从DynamicObject类继承无疑具有更高的灵活性。对ExtensionObject类的实现不清楚的可先看看前面的文章:http://www.cnblogs.com/gyche/p/3223341.html。
在YbSoftwareFactory的一些底层数据访问组件中,例如ConcreteData字典、HierarchyData字典、组织机构实体类、权限实体类、用户信息实体类、角色定义实体类等均已继承自ExtensionObject并实现了对应的对扩展的字段进行数据访问和管理的方法,从实际的运用效果来看,在字段、属性的扩展上确实是非常的灵活和方便。
动态属性扩展的步骤如下:
1、首先,通过让实体类继承自“ExtensionObject”,因为ExtensionObject继承自DynamicObject,并实现了IDictionary<string,object>和索引器,这样实体类就具有了动态属性的自管理功能,在通过强类型访问其实例属性的同时,也能通过dynamic,IDictionary接口和索引器访问其实例属性和动态属性。
例如定义一个用户类并添加必要的实例属性如下:
[Serializable] public class User : ExtensionObject { public Guid UserId { get; set; } public string Email { get; set; } public string Password { get; set; } public string Name { get; set; } public bool Active { get; set; } public DateTime? ExpiresOn { get; set; } public User() : base() { } public User(object instance) : base(instance) { } }
然后,就可通过如下方式进行实例属性和动态属性的访问,是不是非常灵活和方便:
1 var user = new User(); 2 // 通过实例属性进行访问 3 user.UserId = Guid.NewGuid(); 4 user.Password = "YbSofteareFactory"; 5 //通过动态方式进行实例属性的访问 6 dynamic duser = user; 7 duser.Email = "[email protected]"; 8 // 追加动态属性 9 duser.FriendUserName = "YB"; 10 duser.CreatedDate = DateTime.Now; 11 duser.TodayNewsCount = 1; 12 duser.Age = 27.5; 13 duser.LastUpdateId = (Guid?)null; 14 duser.LastUpdatedDate = null; 15 //通过索引器也可进行实例属性和动态属性的访问和追加 16 user["LastUpdatedDate"] = DateTime.Now;
2、实现对扩展字段的数据库访问:
1 #region 加载扩展属性 2 3 /// <summary> 4 /// 为指定的ConcreteData集合加载扩展属性 5 /// </summary> 6 /// <param name="items">待加载的ConcreteData集合</param> 7 public override void LoadExtPropertiesFor(IEnumerable<ConcreteData> items) 8 { 9 //判断是否需要加载 10 //_extFields为需加载的字段名称字符串,如“NewField1,NewField2”,通过config文件进行配置。 11 if (_extFields.Length > 0 && items.Any() ) 12 { 13 //转换为字典,方便后续进行处理 14 var dic = items.ToDictionary(c => c.ConcreteDataId); 15 //组合标识字符串 16 var ids = string.Format("‘{0}‘",string.Join("‘,‘",dic.Keys.ToArray())); 17 using (HostingEnvironment.Impersonate()) 18 using (var db = this.connectionStringSetting.CreateDbConnection()) 19 using (var cmd = 20 this.CreateDbCommand(string.Format("SELECT ConcreteDataId,{0} FROM $TableName WHERE ConcreteDataId IN ({1})",_extFields,ids), db)) 21 { 22 cmd.AddParameterWithValue("@ids", ids); 23 db.Open(); 24 using (var r = cmd.ExecuteReader()) 25 { 26 while (r.Read()) 27 { 28 //获取标识 29 var concreteDataId = r["ConcreteDataId"] as string; 30 //根据字典获取待加载动态属性值的实体 31 var item = dic[concreteDataId]; 32 foreach (var extField in _extFieldArr) 33 { 34 var value = r[extField]; 35 //通过ExtensionObject类的索引器设置动态属性及相应的值 36 item[extField] = value != DBNull.Value ?value:null; 37 } 38 } 39 } 40 } 41 } 42 } 43 44 #endregion 45 46 #region 保存扩展属性 47 48 public override void SaveExtPropertiesFor(IEnumerable<ConcreteData> items) 49 { 50 if (_extFields.Length > 0) 51 { 52 //获取待更新扩展属性的SQL更新语句 53 var updateSql = string.Join(",", _extFieldArr.Select(c => string.Format("{0} = @{0}", c))); 54 55 using (HostingEnvironment.Impersonate()) 56 using (var db = this.connectionStringSetting.CreateDbConnection()) 57 using ( 58 var cmd = 59 this.CreateDbCommand( 60 string.Format("UPDATE $TableName SET {0} WHERE ConcreteDataId = @ConcreteDataId", 61 updateSql), db)) 62 { 63 db.Open(); 64 DbTransaction sqlTransaction = db.BeginTransaction(); 65 cmd.Transaction = sqlTransaction; 66 try 67 { 68 foreach (var item in items) 69 { 70 cmd.Parameters.Clear(); 71 cmd.AddParameterWithValue("@ConcreteDataId",item.ConcreteDataId); 72 foreach (var extField in _extFieldArr) 73 { 74 if (item.Contains(extField, true) && item[extField] != null) 75 { 76 //如果实体的属性包含配置的字段名,则追加更新参数及值 77 cmd.AddParameterWithValue(string.Format("@{0}", extField), 78 item[extField]); 79 } 80 else 81 { 82 //如果实体的属性不包含配置的字段名,则取消对该字段的更新 83 cmd.CommandText = cmd.CommandText.Replace(string.Format("@{0}", extField), "NULL"); 84 } 85 } 86 cmd.ExecuteNonQuery(); 87 } 88 //进行事务提交 89 sqlTransaction.Commit(); 90 } 91 catch (Exception) 92 { 93 sqlTransaction.Rollback(); 94 throw; 95 } 96 } 97 } 98 99 } 100 101 #endregion
3、为了更方便使用,我们在此处可进一步封装,我们可在所配置的Provider初始化时把config文件中配置好的extFields读出来即可,如下是初始化方法的实现:
1 #region Initialize 2 3 public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) 4 { 5 // Validate arguments 6 if (config == null) throw new ArgumentNullException("config"); 7 if (string.IsNullOrEmpty(name)) name = "YbConcreteDataProvider"; 8 if (String.IsNullOrEmpty(config["description"])) 9 { 10 config.Remove("description"); 11 config.Add("description", "Yb concrete data provider"); 12 } 13 //判断是否存在tableName属性 14 if (String.IsNullOrEmpty(config["tableName"])) 15 { 16 config.Remove("tableName"); 17 //添加默认的表名 18 config.Add("tableName", "YbConcreteData"); 19 } 20 //判断是否存在extFields属性 21 if (string.IsNullOrEmpty(config["extFields"])) 22 { 23 config.Remove("extFields"); 24 //不存在则可设置为"",这样将不会对任何扩展的新字段进行访问 25 config.Add("extFields", ""); 26 } 27 // Initialize base class 28 base.Initialize(name, config); 29 30 // Read connection string 31 this.ConnectionStringName = config.GetConfigValue("connectionStringName", null); 32 if (string.IsNullOrWhiteSpace(this.ConnectionStringName)) 33 throw new ConfigurationErrorsException(Resources.Required_connectionStringName_attribute_not_specified); 34 this.connectionStringSetting = ConfigurationManager.ConnectionStrings[this.ConnectionStringName]; 35 if (this.connectionStringSetting == null) 36 throw new ConfigurationErrorsException(string.Format(Resources.Format_connection_string_was_not_found, 37 this.ConnectionStringName)); 38 if (string.IsNullOrEmpty(this.connectionStringSetting.ProviderName)) 39 throw new ConfigurationErrorsException( 40 string.Format( 41 Resources.Format_connection_string_does_not_have_specified_the_providerName_attribute, 42 this.ConnectionStringName)); 43 44 //激发设置连接字符串前的事件处理程序,主要目的是解密连接字符串 45 ConnectionStringChangingEventArgs args = 46 RaiseConnectionStringChangingEvent(connectionStringSetting.ConnectionString); 47 if (args == null) throw new ProviderException(Resources.Connection_string_cannot_be_blank); 48 if (!this.connectionStringSetting.ConnectionString.Equals(args.ConnectionString)) 49 { 50 this.connectionStringSetting = 51 new ConnectionStringSettings(this.ConnectionStringName, args.ConnectionString, 52 this.connectionStringSetting.ProviderName); 53 } 54 if (string.IsNullOrWhiteSpace(connectionStringSetting.ConnectionString)) 55 throw new ProviderException(Resources.Connection_string_cannot_be_blank); 56 57 this.applicationName = config["applicationName"]; 58 //获取配置文件中配置的数据库实际表名 59 this.tableName = config["tableName"]; 60 SecUtility.CheckParameter(ref tableName, true, true, true, 256, "tableName"); 61 //获取配置文件中配置的新扩展的字段名集合 62 _extFields = config.Get("extFields").Trim(); 63 if (!string.IsNullOrEmpty(_extFields)) 64 { 65 //进行字符串分割,转换为字段数组,方便后续的处理 66 _extFieldArr = _extFields.Split(new[] {‘,‘}, StringSplitOptions.RemoveEmptyEntries); 67 _extFieldArr = _extFieldArr.Select(c => c.Trim()).ToArray(); 68 } 69 } 70 71 #endregion
最后看看单元测试代码可进一步理解其调用的具体过程,在数据库中扩展的字段名仅需在config配置文件中设置即可生效,同时在调用方式上进行了统一,最终无需传递扩展的字段名称、类型等参数,在实体对象中也能获取和设置这些新添加的属性的值。单元测试代码如下(此处扩展了三个字段:“NewField1”,“NewField2”,“NewField3”,类型分别为string,bool,DateTime):
1 /// <summary> 2 ///UpdateConcreteData 的测试 3 ///</summary> 4 [TestMethod()] 5 public void ConcreteData_UpdateConcreteDataTest() 6 { 7 ConcreteData concreteData = MyConcreteData; // TODO: 初始化为适当的值 8 bool expected = true; // TODO: 初始化为适当的值 9 bool actual; 10 actual = ConcreteDataApi.UpdateConcreteData(concreteData); 11 Assert.AreEqual(expected, actual); 12 13 //保存扩展属性值为null 14 ConcreteDataApi.SaveExtPropertiesFor(concreteData); 15 ConcreteDataApi.LoadExtPropertiesFor(concreteData); 16 Assert.IsNull(concreteData["NewField1"]); 17 Assert.IsNull(concreteData["NewField2"]); 18 Assert.IsNull(concreteData["NewField3"]); 19 //设置扩展属性值 20 concreteData["NewField1"]="123"; 21 concreteData["NewField2"] = true; 22 concreteData["NewField3"] = DateTime.Now; 23 ConcreteDataApi.SaveExtPropertiesFor(concreteData); 24 var item = ConcreteDataApi.GetConcreteDataWithExtProperties(concreteData.ConcreteDataId); 25 Assert.AreEqual(item["NewField1"],"123"); 26 Assert.AreEqual(item["NewField2"],true); 27 Assert.IsNotNull(item["NewField3"]); 28 29 concreteData["NewField1"] = ""; 30 concreteData["NewField2"] = null; 31 concreteData["NewField3"] = null; 32 ConcreteDataApi.SaveExtPropertiesFor(concreteData); 33 item = ConcreteDataApi.GetConcreteDataWithExtProperties(concreteData.ConcreteDataId); 34 Assert.IsNull(item["NewField1"]); 35 Assert.IsNull(item["NewField2"]); 36 Assert.IsNull(item["NewField3"]); 37 }
通过上述设计,确保了每个数据访问组件默认情况下只需加载必要的字段(即实体类的实例属性),并预留了对新扩展字段的数据访问接口,在提高了灵活性和可扩展性的同时,还兼顾了性能方面的考虑。
下一章将介绍对扩展自ExtensionObject的对象进行Json序列化的具体实现,这样就可让ExtensionObject和MVC实现完美的集成,而无需再进行中间层次的模型转换。
附三:权限模型Demo
YbSoftwareFactory 代码生成插件【十九】:实体类配合数据库表字段进行属性扩展的小技巧