YbSoftwareFactory 各種插件的基礎類庫中又新增了兩個方便易用的功能:動態屬性擴展與鍵值生成器,本章將分別介紹這兩個非常方便的組件。
一、動態屬性擴展
在實際的開發過程中,你肯定會遇到數據庫字段不夠用的情況,臨時增加一個字段有時是很麻煩的一件事。例如需要修改 SQL 語句、視圖、存儲過程等等,即使你使用的是 ORM 組件,也需要增加和配置映射,每次修改完成后還需反復進行測試,非常的不方便,如果軟件已經為客戶部署好了的話,嘿嘿,不用說,肯定更讓你頭疼;而客戶臨時要添加新的字段的情況卻是非常普遍的。另外,有些對象其實不適合放到一張主表中,即使這是 1:1 的關系,因為直接添加到一張表可能會存在一定的性能問題,例如圖片、文件等信息,某些時候查詢 N 多記錄返回大量信息通常不是合理和明智的做法,在字段數量很多的情況下,對於某些不重要的字段信息保存到其他表中通常是可以提升查詢性能的。
本章介紹的動態屬性擴展功能主要就是解決此類問題,可以靈活、方便的擴展屬性。
注:動態屬性擴展組件主要面向正在開發中的審批流組件而設計的,其目的是為終端用戶提供靈活、方便、易用的屬性自定義的功能。動態屬性擴展組件已集成到數據字典組件、組織機構管理組件中。
本組件具有如下顯著特點:
- 自動完成動態屬性值的加載和保存,通過鍵/值對的方式實現動態擴展屬性的數據庫保存和加載,非常的方便。如果你想玩得更高級點,可以直接從界面綁定一個動態屬性,然后保存到數據庫並能重新加載並綁定到界面上,這一過程無需你像某些軟件類似的對所謂的元數據進行管理和配置,非常靈活。
- 能自動完成屬性類型的轉換,因為字段的屬性值是通過鍵值對的方式保存到指定的數據庫表中,因此需要把數據庫中保存的文本型的屬性值自動轉換成指定的類型(如日期、整數、二進制信息)等。本文介紹的動態屬性擴展功能可完成此類型的轉換。
- 支持對象的序列化,這對於使用 WCF、Web Service、Web API 等類似的技術進行遠程數據交互是很有必要的。
至於具體的實現原理,毫無疑問是利用了 .NET 4.0 的 Dynamic 特性,如下是核心基類的實現代碼:

2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Dynamic;
5 using System.Reflection;
6
7 namespace Yb.Data.Provider
8 {
9 [Serializable]
10 public class ExtensionObject: DynamicObject, IDynamicMetaObjectProvider
11 {
12 object _instance;
13
14 Type _instanceType;
15 PropertyInfo[] _cacheInstancePropertyInfos;
16 IEnumerable<PropertyInfo> _instancePropertyInfos
17 {
18 get
19 {
20 if (_cacheInstancePropertyInfos == null && _instance != null)
21 _cacheInstancePropertyInfos = _instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
22 return _cacheInstancePropertyInfos;
23 }
24 }
25
26 public ExtensionObject()
27 {
28 Initialize( this);
29 }
30
31 /// <remarks>
32 /// You can pass in null here if you don't want to
33 /// check native properties and only check the Dictionary!
34 /// </remarks>
35 /// <param name="instance"></param>
36 public ExtensionObject( object instance)
37 {
38 Initialize(instance);
39 }
40
41
42 protected virtual void Initialize( object instance)
43 {
44 _instance = instance;
45 if (instance != null)
46 _instanceType = instance.GetType();
47 }
48
49 /// <param name="binder"></param>
50 /// <param name="result"></param>
51 /// <returns></returns>
52 public override bool TryGetMember(GetMemberBinder binder, out object result)
53 {
54 result = null;
55
56 // first check the Properties collection for member
57 if (Properties.Keys.Contains(binder.Name))
58 {
59 result = Properties[binder.Name];
60 return true;
61 }
62
63
64 // Next check for Public properties via Reflection
65 if (_instance != null)
66 {
67 try
68 {
69 return GetProperty(_instance, binder.Name, out result);
70 }
71 catch (Exception)
72 { }
73 }
74
75 // failed to retrieve a property
76 return false;
77 }
78
79 /// <param name="binder"></param>
80 /// <param name="value"></param>
81 /// <returns></returns>
82 public override bool TrySetMember(SetMemberBinder binder, object value)
83 {
84
85 // first check to see if there's a native property to set
86 if (_instance != null)
87 {
88 try
89 {
90 bool result = SetProperty(_instance, binder.Name, value);
91 if (result)
92 return true;
93 }
94 catch { }
95 }
96
97 // no match - set or add to dictionary
98 Properties[binder.Name] = value;
99 return true;
100 }
101
102 /// <param name="binder"></param>
103 /// <param name="args"></param>
104 /// <param name="result"></param>
105 /// <returns></returns>
106 public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
107 {
108 if (_instance != null)
109 {
110 try
111 {
112 // check instance passed in for methods to invoke
113 if (InvokeMethod(_instance, binder.Name, args, out result))
114 return true;
115 }
116 catch (Exception)
117 { }
118 }
119
120 result = null;
121 return false;
122 }
123
124 /// <param name="instance"></param>
125 /// <param name="name"></param>
126 /// <param name="result"></param>
127 /// <returns></returns>
128 protected bool GetProperty( object instance, string name, out object result)
129 {
130 if (instance == null)
131 instance = this;
132
133 var miArray = _instanceType.GetMember(name, BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Instance);
134 if (miArray != null && miArray.Length > 0)
135 {
136 var mi = miArray[ 0];
137 if (mi.MemberType == MemberTypes.Property)
138 {
139 result = ((PropertyInfo)mi).GetValue(instance, null);
140 return true;
141 }
142 }
143
144 result = null;
145 return false;
146 }
147
148 /// <param name="instance"></param>
149 /// <param name="name"></param>
150 /// <param name="value"></param>
151 /// <returns></returns>
152 protected bool SetProperty( object instance, string name, object value)
153 {
154 if (instance == null)
155 instance = this;
156
157 var miArray = _instanceType.GetMember(name, BindingFlags.Public | BindingFlags.SetProperty | BindingFlags.Instance);
158 if (miArray != null && miArray.Length > 0)
159 {
160 var mi = miArray[ 0];
161 if (mi.MemberType == MemberTypes.Property)
162 {
163 ((PropertyInfo)mi).SetValue(_instance, value, null);
164 return true;
165 }
166 }
167 return false;
168 }
169
170 /// <param name="instance"></param>
171 /// <param name="name"></param>
172 /// <param name="args"></param>
173 /// <param name="result"></param>
174 /// <returns></returns>
175 protected bool InvokeMethod( object instance, string name, object[] args, out object result)
176 {
177 if (instance == null)
178 instance = this;
179
180 // Look at the instanceType
181 var miArray = _instanceType.GetMember(name,
182 BindingFlags.InvokeMethod |
183 BindingFlags.Public | BindingFlags.Instance);
184
185 if (miArray != null && miArray.Length > 0)
186 {
187 var mi = miArray[ 0] as MethodInfo;
188 result = mi.Invoke(_instance, args);
189 return true;
190 }
191
192 result = null;
193 return false;
194 }
195
196 public object this[ string key]
197 {
198 get
199 {
200 try
201 {
202 // try to get from properties collection first
203 return Properties[key];
204 }
205 catch (KeyNotFoundException ex)
206 {
207 // try reflection on instanceType
208 object result = null;
209 if (GetProperty(_instance, key, out result))
210 return result;
211
212 // nope doesn't exist
213 throw;
214 }
215 }
216 set
217 {
218 if (Properties.ContainsKey(key))
219 {
220 Properties[key] = value;
221 return;
222 }
223
224 // check instance for existance of type first
225 var miArray = _instanceType.GetMember(key, BindingFlags.Public | BindingFlags.GetProperty);
226 if (miArray != null && miArray.Length > 0)
227 SetProperty(_instance, key, value);
228 else
229 Properties[key] = value;
230 }
231 }
232
233 /// <param name="includeInstanceProperties"></param>
234 /// <returns></returns>
235 public IEnumerable<KeyValuePair< string, object>> GetProperties( bool includeInstanceProperties = false)
236 {
237 if (includeInstanceProperties && _instance != null)
238 {
239 foreach ( var prop in this._instancePropertyInfos)
240 yield return new KeyValuePair< string, object>(prop.Name, prop.GetValue(_instance, null));
241 }
242
243 foreach ( var key in this.Properties.Keys)
244 yield return new KeyValuePair< string, object>(key, this.Properties[key]);
245
246 }
247
248 /// <param name="item"></param>
249 /// <param name="includeInstanceProperties"></param>
250 /// <returns></returns>
251 public bool Contains(KeyValuePair< string, object> item, bool includeInstanceProperties = false)
252 {
253 bool res = Properties.ContainsKey(item.Key);
254 if (res)
255 return true;
256
257 if (includeInstanceProperties && _instance != null)
258 {
259 foreach ( var prop in this._instancePropertyInfos)
260 {
261 if (prop.Name == item.Key)
262 return true;
263 }
264 }
265
266 return false;
267 }
268 /// <param name="key"></param>
269 /// <returns></returns>
270 public bool Contains( string key, bool includeInstanceProperties = false)
271 {
272 bool res = Properties.ContainsKey(key);
273 if (res)
274 return true;
275
276 if (includeInstanceProperties && _instance != null)
277 {
278 foreach ( var prop in this._instancePropertyInfos)
279 {
280 if (prop.Name == key)
281 return true;
282 }
283 }
284
285 return false;
286 }
287
288 }
289 }
具體的使用,僅需繼承該對象即可。為了更好的說明具體用法,請查看如下已測試通過的單元測試代碼:

2 public class User : ExtensionObject
3 {
4 public Guid UserId { get; set; }
5 public string Email { get; set; }
6 public string Password { get; set; }
7 public string Name { get; set; }
8 public bool Active { get; set; }
9 public DateTime? ExpiresOn { get; set; }
10
11 public User()
12 : base()
13 { }
14
15 // only required if you want to mix in seperate instance
16 public User( object instance)
17 : base(instance)
18 {
19 }
20 }
21
22 /// <summary>
23 /// ExtensionData 的測試
24 /// </summary>
25 [TestMethod()]
26 public void ExtensionObjectTest()
27 {
28 // 清空數據庫存儲的屬性值,方便進行測試
29 ExtensionDataApi.ClearExtensionDataOfApplication();
30
31 var user = new User();
32 // 設置已有屬性
33 dynamic duser = user;
34 user.UserId = Guid.NewGuid();
35 duser.Email = " 19892257@qq.com ";
36 user.Password = " YbSofteareFactory ";
37
38 // 設置動態屬性
39 duser.FriendUserName = " YB ";
40 duser.CreatedDate = DateTime.Now;
41 duser.TodayNewsCount = 1;
42 duser.Age = 27.5;
43 duser.LastUpdateId = (Guid?) null;
44 duser.LastUpdatedDate= null;
45
46 // 動態屬性值保存
47 ExtensionDataApi.SaveExtensionObject(user.UserId,user);
48
49 // 從數據庫中加載屬性值
50 var obj = user.LoadExtensionData(user.UserId);
51
52 // 測試是否加載正確
53 Assert.AreEqual(obj.FriendUserName, " YB ");
54 Assert.IsNotNull(obj.CreatedDate);
55 Assert.AreEqual(obj.TodayNewsCount, 1);
56 Assert.AreEqual(obj.Age, 27.5);
57 Assert.IsNull(obj.LastUpdateId);
58 Assert.IsNull(obj.LastUpdatedDate);
59
60 var items = ExtensionDataApi.FindExtensionDataBy(user.UserId.ToString(), user);
61 // 測試保存的動態屬性數
62 Assert.IsTrue(items.Count() == 6);
63
64 // 修改動態屬性值
65 duser.Age = 28;
66 // 新增動態屬性
67 duser.Tag = null;
68 duser.NewProperty = 12;
69 // 使用擴展方法進行保存動態屬性值至數據庫
70 user.SaveExtensionData(user.UserId);
71
72 items = ExtensionDataApi.FindExtensionDataBy(user.UserId.ToString(), user);
73 // 判斷保存的屬性數量是否正確
74 Assert.IsTrue(items.Count() == 8);
75
76 // 使用擴展方法動態從數據庫中加載屬性
77 obj = user.LoadExtensionData(user.UserId);
78
79 Assert.AreEqual(obj.Tag, null);
80 Assert.AreEqual(obj.NewProperty, 12);
81
82 duser.ComplexObject = user;
83
84 // 設置新值
85 duser.Tag = true;
86 ExtensionDataApi.SaveExtensionObject(user.UserId, user);
87 obj = ExtensionDataApi.LoadExtensionObject(user.UserId, user);
88 // 驗證加載的屬性新值是否正確
89 Assert.IsTrue(obj.Tag);
90
91 // 返回對象數組的屬性字典方法測試
92 var dic = ExtensionDataApi.FindExtensionDataDictionaryBy( new string[]{user.UserId.ToString()}, user.GetType().FullName);
93 Assert.IsTrue(dic.Count> 0);
94
95 // byte[] 測試,對可方便存儲文件、圖片等內容
96 duser.Image = new byte[] { 2, 255, 241, 236, 16, 19, 128, 32, 90};
97 ExtensionDataApi.SaveExtensionObject(user.UserId, user);
98 obj = ExtensionDataApi.LoadExtensionObject(user.UserId, user);
99 Assert.AreEqual(obj.Image.Length, 9);
100 Assert.AreEqual(obj.Image[ 8], 90);
101
102 // Json 序列化測試,對 Web Api 等非常重要
103 string json = JsonConvert.SerializeObject(duser, Formatting.Indented, new JsonSerializerSettings
104 {
105 TypeNameHandling = TypeNameHandling.All,
106 TypeNameAssemblyFormat = FormatterAssemblyStyle.Full
107 });
108 Assert.IsNotNull(json);
109 json = JsonConvert.SerializeObject(user);
110 Assert.IsNotNull(json);
111 }
二、鍵值生成器
鍵值的生成看似簡單,其實實現起來卻並不容易,因為這里面有並發性、生成效率等等方面的考慮。同時,對鍵值的管理也是非常重要的,試想想,不同位置的兩個客戶端同時生成了相同的鍵值是什么后果吧。
本章要介紹的鍵值生成器組件非常靈活和高效,它具有如下非常實用的功能:
- 支持絕大多數情況下指定格式的鍵值生成,例如可指定前綴、后綴、客戶端應用程序編號(多客戶端下非常有用)、日期(例如yyyy、yyyyMM、yyyyMMdd、yyyyMMddHH等)以及流水號長度。
- 支持批量生成鍵值,一次可以生成指定數量的鍵值組。
- 在滿足特定性能的前提下,可有效解決常見的並發情況,有效防止鍵值沖突。
對於具體的使用方式,同樣還是來看看已通過測試的部分單元測試代碼:

2 /// GetNextID 的測試
3 /// </summary>
4 [TestMethod()]
5 public void GetNextIDTest()
6 {
7 IdGeneratorApi.ClearAllIdGenerator();
8
9 var user = new User();
10
11 // 生成類似 U-01-201308-001格式的ID,%A表示輸出客戶端編號,%D表示輸出日期時間
12 var idGen = new IdGenerator()
13 {
14 Type = typeof (User).FullName,
15 DateFormat = " yyyyMM ",
16 GenFormat = " U-%A-%D- ",
17 Id = Guid.NewGuid(),
18 StartValue = 1,
19 NextValue = 1,
20 ValueLength = 3
21 };
22 // API基本方法測試
23 IdGeneratorApi.SaveOrUpdateIdGenerator(idGen);
24 var item = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
25 Assert.IsNotNull(item);
26 item = IdGeneratorApi.GetIdGeneratorBy(user);
27 Assert.IsNotNull(item);
28 item = IdGeneratorApi.GetIdGeneratorBy( " not exist's record ");
29 Assert.IsNull(item);
30 // API基本方法測試
31 Assert.IsTrue(IdGeneratorApi.IdGeneratorExists(user));
32 Assert.IsFalse(IdGeneratorApi.IdGeneratorExists( " dkakd_test_a "));
33
34 // 生成ID號
35 var str = IdGeneratorApi.GetNextID(user);
36 Assert.AreEqual( " U-02-201308-001 ", str);
37 str = IdGeneratorApi.GetNextID(user);
38 Assert.AreEqual( " U-02-201308-002 ", str);
39
40 idGen = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
41 // 無需生成日期,當前生成的ID號類似於U-02--003
42 idGen.DateFormat = string.Empty;
43
44 IdGeneratorApi.SaveOrUpdateIdGenerator(idGen);
45 idGen = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
46
47 // 生成下一ID號
48 str = IdGeneratorApi.GetNextID(user);
49 Assert.AreEqual( " U-02--003 ", str);
50 str = IdGeneratorApi.GetNextID(user);
51 Assert.AreEqual( " U-02--004 ", str);
52
53 idGen = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
54 // 如下代碼修改生成的ID號類似於U-0005-
55 idGen.DateFormat = " yyyyMM ";
56 // 未設置%D,將不再輸出日期
57 idGen.GenFormat = " U-%v- ";
58 // 修改生成編號的長度為4
59 idGen.ValueLength = 4;
60 IdGeneratorApi.SaveOrUpdateIdGenerator(idGen);
61
62 str = IdGeneratorApi.GetNextID(user);
63 Assert.AreEqual( " U-0005- ", str);
64 str = IdGeneratorApi.GetNextID(user);
65 Assert.AreEqual( " U-0006- ", str);
66
67 // API基本方法測試
68 IdGeneratorApi.DeleteIdGenerator(idGen);
69 item = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
70 Assert.IsNull(item);
71 item = IdGeneratorApi.GetIdGeneratorBy(user);
72 Assert.IsNull(item);
73
74 IdGeneratorApi.ClearAllIdGeneratorOfApplication();
75 }
76
77 /// <summary>
78 /// GetNextGroupID 的測試,批量生產ID號
79 /// </summary>
80 [TestMethod()]
81 public void GetNextGroupIDTest()
82 {
83 IdGeneratorApi.ClearAllIdGeneratorOfApplication();
84
85 var user = new User();
86
87 var idGen = new IdGenerator()
88 {
89 Type = typeof(User).FullName,
90 DateFormat = " yyyyMM ",
91 GenFormat = " U-%a-%D-%v ",
92 Id = Guid.NewGuid(),
93 StartValue = 1,
94 NextValue = 1,
95 ValueLength = 3
96 };
97
98 IdGeneratorApi.SaveOrUpdateIdGenerator(idGen);
99
100 // 批量生成3個ID號
101 var str = IdGeneratorApi.GetNextGroupID(user, 3);
102 Assert.IsTrue(str.Length== 3);
103 Assert.IsTrue(str[ 0]== " U-02-201308-001 ");
104 Assert.IsTrue(str[ 1]== " U-02-201308-002 ");
105 Assert.IsTrue(str[ 2]== " U-02-201308-003 ");
106
107 idGen = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
108 // 如下修改將生成類似於T0004的ID,將忽略日期和客戶端編號
109 idGen.GenFormat = " T%v ";
110 idGen.ValueLength = 4;
111 IdGeneratorApi.SaveOrUpdateIdGenerator(idGen);
112
113 str = IdGeneratorApi.GetNextGroupID(user, 2);
114 Assert.IsTrue(str.Length== 2);
115 Assert.IsTrue(str[ 0]== " T0004 ");
116 Assert.IsTrue(str[ 1]== " T0005 ");
117
118 idGen = IdGeneratorApi.GetIdGeneratorBy(idGen.Id);
119 // 修改生成的ID格式
120 idGen.DateFormat = " yyyy ";
121 // 生成類似於01-0010/2013的ID號,%a為客戶端編號,%v為流水號,%d將輸出日期時間,此處為年份
122 idGen.GenFormat = " %a-%v/%d ";
123 // 指明流水號長度為4,類似於0001
124 idGen.ValueLength = 4;
125 IdGeneratorApi.SaveOrUpdateIdGenerator(idGen);
126
127 str = IdGeneratorApi.GetNextGroupID(user, 2);
128 Assert.IsTrue(str.Length== 2);
129 Assert.IsTrue(str[ 0]== " 02-0001/2013 ");
130 Assert.IsTrue(str[ 1]== " 02-0002/2013 ");
131
132 IdGeneratorApi.ClearAllIdGenerator();
133 }
134
135 public class User
136 {
137 public string Id { get; set; }
138 public string UserName { get; set; }
139 }
目前的開發重心將逐漸向審批流的開發過渡,未來的審批流組件將由表單設計器、流程設計器和審批流底層組件三大部分組成,具有靈活、簡單、易用的特點,如下是流程設計器的預覽界面: