YbSoftwareFactory 代码生成插件【十五】:Show 一下最新的动态属性扩展功能与键值生成器功能
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 }
目前的开发重心将逐渐向审批流的开发过渡,未来的审批流组件将由表单设计器、流程设计器和审批流底层组件三大部分组成,具有灵活、简单、易用的特点,如下是流程设计器的预览界面: