實體類一般須要和數據庫表進行了ORM映射,當你須要添加新的屬性時,每每同時也須要在數據庫中添加相應的字段並配置好映射關係,同時可能還需對數據訪問組件進行從新編譯和部署纔能有效。而當你開始設計一個通用數據訪問組件後,由於項目需求的不一樣和需求的不斷變化演變,很難不能保證不會再添加額外的屬性和字段。特別是項目部署運行後,添加一個屬性和字段帶來的額外維護的工做量可能要遠遠超過對代碼進行調整的工做量。本文提供了屬性字段擴展的一種思路,在知足核心字段可經過實體類強類型進行訪問的同時,還可經過C# 4.0提供的dynamic特性和Dictionary等技術手段進行字段、屬性的擴展,並對數據訪問的統一封裝,具備通用性強、使用方便、擴展能力強等優勢。html
本文用到了前面提到的ExtensionObject,其是進行屬性擴展原理的核心類,該類繼承自DynamicObject類,並實現了, IDynamicMetaObjectProvider,IDictionary<string,object>等接口。和.NET Framework中ExpandoObject類不一樣的是,繼承自DynamicObject的類能夠添加實例屬性,而ExpandoObject由於被設計爲「sealed」類,所以它只能在運行時動態添加屬性;另外,繼承自DynamicObject的類可實現自定義的對其成員進行管理的一系列方法,所以和ExpandoObject類相比,從DynamicObject類繼承無疑具備更高的靈活性。對ExtensionObject類的實現不清楚的可先看看前面的文章:http://www.cnblogs.com/gyche/p/3223341.html。sql
在YbSoftwareFactory的一些底層數據訪問組件中,例如ConcreteData字典、HierarchyData字典、組織機構實體類、權限實體類、用戶信息實體類、角色定義實體類等均已繼承自ExtensionObject並實現了對應的對擴展的字段進行數據訪問和管理的方法,從實際的運用效果來看,在字段、屬性的擴展上確實是很是的靈活和方便。數據庫
動態屬性擴展的步驟以下:數組
一、首先,經過讓實體類繼承自「ExtensionObject」,由於ExtensionObject繼承自DynamicObject,並實現了IDictionary<string,object>和索引器,這樣實體類就具備了動態屬性的自管理功能,在經過強類型訪問其實例屬性的同時,也能經過dynamic,IDictionary接口和索引器訪問其實例屬性和動態屬性。app
例如定義一個用戶類並添加必要的實例屬性以下:ide
[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 = "19892257@qq.com"; 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;
二、實現對擴展字段的數據庫訪問:單元測試
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
三、爲了更方便使用,咱們在此處可進一步封裝,咱們可在所配置的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):ui
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