最近在整理代碼,發現之前寫的一個數據填充器寫了一半沒實現,而恰恰這段時間就要用到相似的功能,因此正好實現下。前端
這個工具的目標是可以在項目初期快速搭建一個「數據提供器」,快速的爲前端提供數據支撐,從而方便項目定型;固然,或許這不是一個正確的開發流程。不過存在決定方法,這裏不討論理想狀況。基於這個目標,目前有兩種方式:git
總的來講就是兩個選擇,要麼本身實現,要麼站在前人的基礎上調整。
在Nuget上搜索了下Data Generater,發現很多的匹配項。找了其中一個下載量比較大的:github
Bogus https://github.com/bchavez/Bogusexpress
細看了下文檔,感嘆羣衆的眼睛果真是雪亮的。後端
Bogus徹底符合[目標]這一節的第一點要求,可是沒有發現基於Attribute的使用方式。因此決定本身擴展下。Bogus的配置入口是一個泛型類:Faker<>,配置方法是RuleFor,這個方法包含了2個重載,並且都是兩個參數的。第一個參數都是一個MemberAccess的Lambda Expression,這個參數指示了你但願針對哪一個屬性配置。第二個參數是一個委託,指示了你但願如何返回值。該組件的Faker(非泛型)類型提供了豐富的數據提供方式。這也是這個組件最大的價值所在。如下是摘自GitHub的幾個例子:api
var testUsers = new Faker<User>() //Optional: Call for objects that have complex initialization .CustomInstantiator(f => new User(userIds++, f.Random.Replace("###-##-####"))) //Basic rules using built-in generators .RuleFor(u => u.FirstName, f => f.Name.FirstName()) .RuleFor(u => u.LastName, f => f.Name.LastName()) .RuleFor(u => u.Avatar, f => f.Internet.Avatar()) .RuleFor(u => u.UserName, (f, u) => f.Internet.UserName(u.FirstName, u.LastName)) .RuleFor(u => u.Email, (f, u) => f.Internet.Email(u.FirstName, u.LastName)) .RuleFor(u => u.SomethingUnique, f => $"Value {f.UniqueIndex}") .RuleFor(u => u.SomeGuid, Guid.NewGuid) //Use an enum outside scope. .RuleFor(u => u.Gender, f => f.PickRandom<Gender>()) //Use a method outside scope. .RuleFor(u => u.CartId, f => Guid.NewGuid()) //Compound property with context, use the first/last name properties .RuleFor(u => u.FullName, (f, u) => u.FirstName + " " + u.LastName) //And composability of a complex collection. .RuleFor(u => u.Orders, f => testOrders.Generate(3).ToList()) //After all rules are applied finish with the following action .FinishWith((f, u) => { Console.WriteLine("User Created! Id={0}", u.Id); }); var user = testUsers.Generate();
基於這個配置的例子,咱們的思路也就清晰了。須要自定定義一個Attribute,聲明某個屬性須要填充數據。運行期間,咱們須要分析這個類型的元素據,提取Attribute上的值,而後經過調用Bogus實現的Faker來爲類型指定填充規則。而經過不一樣的Attribute(一般,會設計成繼承於同一個基類)咱們能夠指定不一樣的數據填充方式。
首先,這裏是咱們定義的Attribute基類:安全
namespace DRapid.Utility.DataFaker.Core { [AttributeUsage(AttributeTargets.Property)] public abstract class BogusAttribute : Attribute { /// <summary> /// 返回一個指定的值提供器 /// </summary> /// <returns></returns> public abstract Func<Faker, object> GetValueProvider(); public static Random Random = new Random(); } }
這是一個抽象類,由於咱們必需要求全部的Attribute指明如何填充某個屬性。抽象方法的返回值是一個委託,並且兼容Bogus的委託的定義——這樣,咱們就能夠充分利用Bogus內建的大量功能。例如,下面就是一個隨機填充一個姓名的實現:app
/// <summary> /// 指示數據填充器使用一個[全名]填充屬性 /// </summary> public class BogusFullNameAttribute : BogusAttribute { public Name.Gender Gender { get; set; } public override Func<Faker, object> GetValueProvider() { return f => f.Name.FindName(null, null, null, null, Gender); } }
接下來咱們須要實現本身的調用入口。在這個實現中,有點麻煩的就是要在運行期間進行泛型相關的操做。不然就沒法正確的轉接到Bogus的基礎實現中,因此會稍微用到一些運行時「編譯」:框架
/// <summary> /// 爲指定的類型機型假數據配置 /// </summary> /// <param name="type"></param> public static object Config(Type type) { var gType = typeof (Faker<>).MakeGenericType(type); dynamic dGenerator = Activator.CreateInstance(gType, "zh-cn", null); var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty); foreach (var propertyInfo in properties) { var attr = propertyInfo.GetCustomAttribute<BogusAttribute>(); if (attr != null) { var builderType = typeof (PropertyExpressionBuilder<,>) .MakeGenericType(type, propertyInfo.PropertyType); dynamic builder = Activator.CreateInstance(builderType); var expression = builder.Build(propertyInfo); var valueProvider = attr.GetValueProvider(); var paramExp = Expression.Parameter(typeof (Faker)); var invokeExp = Expression.Invoke(Expression.Constant(valueProvider), paramExp); var nullCheckExp = Expression.Equal(Expression.Constant(null), invokeExp); var convertExp = Expression.Convert(invokeExp, propertyInfo.PropertyType); var conditionExp = Expression.Condition(nullCheckExp, Expression.Default(propertyInfo.PropertyType), convertExp); dynamic providerExp = Expression.Lambda(conditionExp, paramExp); dGenerator.RuleFor(expression, providerExp.Compile()); } } object exConfigs; if (_externalConfigs.TryGetValue(type, out exConfigs)) { dynamic dList = exConfigs; foreach (var dItem in dList) { dGenerator.RuleFor(dItem.Item1, dItem.Item2); } } return dGenerator; }
這裏使用了lambda表達式樹來在運行期間生成一些代碼,同時使用了若干個線程安全的字典來保證只對一個類型配置一次。這個Config方法所作的事情,正如上文所述,就是很明確的兩件:1,分析類型的元數據;2,調用Faker<>的RuleFor方法。而大部分的代碼是爲了作第二件事作準備——構造調用RuleFor方法所須要的參數,僅此而已。如下是完整的實現:dom
public class BogusDataStore { private static ConcurrentDictionary<Type, object> _fakers = new ConcurrentDictionary<Type, object>(); private static ConcurrentDictionary<string, object> _fakeLists = new ConcurrentDictionary<string, object>(); private static ConcurrentDictionary<Type, object> _externalConfigs = new ConcurrentDictionary<Type, object>(); private static ConcurrentDictionary<string, IList> _randomSource = new ConcurrentDictionary<string, IList>(); /// <summary> /// 爲指定的類型機型假數據配置 /// </summary> /// <param name="type"></param> public static object Config(Type type) { var gType = typeof (Faker<>).MakeGenericType(type); dynamic dGenerator = Activator.CreateInstance(gType, "zh-cn", null); var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty); foreach (var propertyInfo in properties) { var attr = propertyInfo.GetCustomAttribute<BogusAttribute>(); if (attr != null) { var builderType = typeof (PropertyExpressionBuilder<,>) .MakeGenericType(type, propertyInfo.PropertyType); dynamic builder = Activator.CreateInstance(builderType); var expression = builder.Build(propertyInfo); var valueProvider = attr.GetValueProvider(); var paramExp = Expression.Parameter(typeof (Faker)); var invokeExp = Expression.Invoke(Expression.Constant(valueProvider), paramExp); var nullCheckExp = Expression.Equal(Expression.Constant(null), invokeExp); var convertExp = Expression.Convert(invokeExp, propertyInfo.PropertyType); var conditionExp = Expression.Condition(nullCheckExp, Expression.Default(propertyInfo.PropertyType), convertExp); dynamic providerExp = Expression.Lambda(conditionExp, paramExp); dGenerator.RuleFor(expression, providerExp.Compile()); } } object exConfigs; if (_externalConfigs.TryGetValue(type, out exConfigs)) { dynamic dList = exConfigs; foreach (var dItem in dList) { dGenerator.RuleFor(dItem.Item1, dItem.Item2); } } return dGenerator; } /// <summary> /// 爲指定的類型生成一個對象並填充數據 /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public static T Generate<T>() where T : class { var faker = _fakers.GetOrAdd(typeof (T), Config); return (faker as Faker<T>).IfNotNull(i => i.Generate()); } /// <summary> /// 爲指定的類型生成一個集合並填充數據 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="count"></param> /// <returns></returns> public static IList<T> Generate<T>(int count) where T : class { var faker = _fakers.GetOrAdd(typeof (T), Config); return (faker as Faker<T>).IfNotNull(i => i.Generate(count).ToList()); } /// <summary> /// 爲指定的數據生成一個集合,並填充數據,使用指定的key在內存中存儲 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="count"></param> /// <param name="key"></param> /// <param name="refreshAll"></param> /// <returns></returns> public static IList<T> GenerateOrGet<T>(int count, string key = null, bool refreshAll = false) where T : class { key = key.IsNullOrWhiteSpace() ? typeof (T).FullName : key; // ReSharper disable once AssignNullToNotNullAttribute var list = (List<T>) _fakeLists.GetOrAdd(key, i => new List<T>()); lock (list) { if (refreshAll) { list.Clear(); } var countToFill = count - list.Count; if (countToFill > 0) { var items = Generate<T>(countToFill); list.AddRange(items); } } return list; } /// <summary> /// 爲指定的類型指定特定的值提供器 /// </summary> /// <typeparam name="TInstance"></typeparam> /// <typeparam name="TProperty"></typeparam> /// <param name="exp"></param> /// <param name="valueProvider"></param> public static void RuleFor<TInstance, TProperty>(Expression<Func<TInstance, TProperty>> exp, Func<Faker, TInstance, TProperty> valueProvider) where TInstance : class { var exConfigs = _externalConfigs.GetOrAdd(typeof (TInstance), k => new List<Tuple<Expression<Func<TInstance, TProperty>>, Func<Faker, TInstance, TProperty>>>()); var configList = (List<Tuple<Expression<Func<TInstance, TProperty>>, Func<Faker, TInstance, TProperty>>>) exConfigs; var item = new Tuple<Expression<Func<TInstance, TProperty>>, Func<Faker, TInstance, TProperty>>(exp, valueProvider); configList.Add(item); } /// <summary> /// 使用指定的鍵和值配置隨機生成器的數據源 /// </summary> /// <param name="key"></param> /// <param name="randomSet"></param> public static void ConfigRandomSet(string key, IList randomSet) { _randomSource.TryAdd(key, randomSet); } /// <summary> /// 嘗試使用指定的鍵獲取一個隨機數據源 /// </summary> /// <param name="key"></param> public static IList TryGetRandomSet(string key) { IList result; _randomSource.TryGetValue(key, out result); return result; } }
public class PropertyExpressionBuilder<TInstance, TProperty> { public Expression<Func<TInstance, TProperty>> Build(PropertyInfo propertyInfo) { var param = Expression.Parameter(typeof (TInstance)); var memberAccess = Expression.MakeMemberAccess(param, propertyInfo); return Expression.Lambda<Func<TInstance, TProperty>>(memberAccess, param); } }
在這個實現中,額外作了如下事情:
public class Person { [BogusFullName] public string FakeName { get; set; } public string Name { get { return LastName + FirstName; } } [BogusFirstName] public string FirstName { get; set; } [BogusLastName] public string LastName { get; set; } [BogusRandomCodeText("###-???-***")] public string JobCode { get; set; } [BogusJobType] public string JobType { get; set; } [BogusJobTitle] public string JobTitle { get; set; } [BogusRandomInt(MaxValue = 100, MinValue = 0)] public int Age { get; set; } [BogusRandomItem("genders")] public Name.Gender Gender { get; set; } [BogusRandomBool] public bool HasWife { get; set; } [BogusRandomDouble] public double Score { get; set; } }
將生成以下結果:
{"FakeName":"Mack Hackett MD","Name":"HoegerTiana","FirstName":"Tiana","LastName":"Hoeger","JobCode":"666-QTX-YUC","JobType":"Architect","JobTitle":"Central Interactions Supervisor","Age":36,"Gender":1,"HasWife":false,"Score":-9.2717476334228441E+307}
對於生成10w個1中所述的類型的對象,將耗時:
4556ms
總的來講,這只是個demo。若是要作的更完善的話,還須要考慮如下幾個問題: