数据生成器Bogus的使用以及基于声明的扩展
引言
最近在整理代码,发现以前写的一个数据填充器写了一半没实现,而偏偏这段时间就要用到类似的功能,所以正好实现下。
目标
这个工具的目标是能够在项目初期快速搭建一个“数据提供器”,快速的为前端提供数据支撑,从而方便项目定型;当然,或许这不是一个正确的开发流程。不过存在决定方法,这里不讨论理想情况。基于这个目标,目前有两种方式:
- 基于“仓储”的“伪实现”。由于项目框架中进行了仓储隔离,所以可以考虑为仓储提供一个“数据池”,在忽略业务的情形下快速提供数据。基于IoC的思想,这种实现中,业务层必须不晓得“假数据”的存在。所以不能让和这个组件相关的任何信息、任何代码倾入业务。也就是说,不能使用声明式的方案,得使用类似于EF的Map的方案。
- 基于“应用程序层”的“伪实现”。第一个方案的缺点是仍然不够快,需要编写一定量的代码。所以第二个方案的特点是,全部基于Attribute声明,快速确定前后端需要传输的数据类型。因为这些定义的数据类型属于DTO,也没有必要去清理这些定义好的Attribute——而且,如果设计得当的话,完全可以将这些Attribute作为数据验证的依据。
选择
总的来说就是两个选择,要么自己实现,要么站在前人的基础上调整。
在Nuget上搜索了下Data Generater,发现不少的匹配项。找了其中一个下载量比较大的:
细看了下文档,感叹群众的眼睛果然是雪亮的。
扩展
Bogus完全符合[目标]这一节的第一点要求,但是没有发现基于Attribute的使用方式。所以决定自己扩展下。Bogus的配置入口是一个泛型类:Faker<>,配置方法是RuleFor,这个方法包含了2个重载,而且都是两个参数的。第一个参数都是一个MemberAccess的Lambda Expression,这个参数指示了你希望针对哪个属性配置。第二个参数是一个委托,指示了你希望如何返回值。该组件的Faker(非泛型)类型提供了丰富的数据提供方式。这也是这个组件最大的价值所在。以下是摘自GitHub的几个例子:
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内建的大量功能。例如,下面就是一个随机填充一个姓名的实现:
/// <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方法所需要的参数,仅此而已。以下是完整的实现:
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);
}
}
在这个实现中,额外做了以下事情:
- 兼容Bogus默认的配置方式,从而解决一些无法使用特性配置的问题
- 实现一个基于内存的存储方式,方便使用
- 实现一个“随机数据池”,使得可以随机从这个池中提取一个项,作为假数据的一个值
测试
- 对于声明的类型:
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。如果要做的更完善的话,还需要考虑以下几个问题:
- 更丰富的内容支持
- 本地化支持
...
数据生成器Bogus的使用以及基于声明的扩展的更多相关文章
- Mockjs,模拟数据生成器
(推荐使用)Mock.js是一款模拟数据生成器,旨在帮助前端攻城师独立于后端进行开发,帮助编写单元测试. 提供了以下模拟功能: 1. 根据数据模板生成模拟数据. 2. 模拟Ajax请求,生成并返回模拟 ...
- ASP.NET MVC 随想录—— 使用ASP.NET Identity实现基于声明的授权,高级篇
在这篇文章中,我将继续ASP.NET Identity 之旅,这也是ASP.NET Identity 三部曲的最后一篇.在本文中,将为大家介绍ASP.NET Identity 的高级功能,它支持声明式 ...
- 【awesome-dotnet-core-learning】(3)-Bogus-假数据生成器
[awesome-dotnet-core-learning](3)-Bogus-假数据生成器 简介 Bogus一个简单而强大的假数据生成器,用于C#,F#和VB.NET.从著名的faker.js移植过 ...
- 让前端独立于后端进行开发,模拟数据生成器Mock.js
让前端独立于后端进行开发,模拟数据生成器Mock.jsMock.js 是一款模拟数据生成器,旨在帮助前端攻城师独立于后端进行开发,帮助编写单元测试. Home · nuysoft/Mock Wiki ...
- 【scikit-learn】06:make_blobs聚类数据生成器
版权声明:本文为博主原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/kevinelstri/article/ ...
- (数据科学学习手札75)基于geopandas的空间数据分析——坐标参考系篇
本文对应代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 在上一篇文章中我们对geopandas中的数据结 ...
- ASP.NET Core 基于声明的访问控制到底是什么鬼?
从ASP.NET 4.x到ASP.NET Core,内置身份验证已从基于角色的访问控制(RBAC)转变为基于声明的访问控制(CBAC). 我们常用的HttpContext.User属性ASP.NET ...
- Java-随机数据生成器(造数据)
概述 简单易用的随机数据生成器.一般用于开发和测试阶段的数据填充.模拟.仿真研究.演示等场景.可以集成到各种类型的java项目中使用. 优点 非常轻量级(不到1M),容易集成,无需过多第三方依赖 简单 ...
- Windows Azure Active Directory (1) 前言 - 基于声明的验证和授权
<Windows Azure Platform 系列文章目录> 在我们介绍整套系统架构之前,我们需要首先定义一些基本的概念. 用户及其属性: 用户值得是要使用某项服务的个体.用户一般都有一 ...
随机推荐
- PS基础学习 1---基本工具
1,选框工具: 选择以后对选框中的内容进行修改 ① Shift + 选框 为正方形 ② 选中后鼠标放在选框中对选择范围进行拖动 ③ 移动工具可以拉着选框中的内容移动 ④ ctrl+D取消选框 ⑤单行选 ...
- Linux 命令 - grep: 正则搜索文本
grep 搜索文本文件中与指定正则表达式匹配的行 命令格式 grep [OPTIONS] PATTERN [FILE...] 命令参数 Generic Program Information --he ...
- iOS开发那些事--性能优化–内存泄露问题的解决(转)
内存泄漏问题的解决 内存泄漏(Memory Leaks)是当一个对象或变量在使用完成后没有释放掉,这个对象一直占有着这块内存,直到应用停止.如果这种对象过多内存就会耗尽,其它的应用就无法运行.这个问题 ...
- CSS之图片关闭
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- svn 服务器的搭建 on Ubuntu
Subversion 如何在Ubuntu下安装配置Subversion服务器.已经配置好,不过没有配置开机自启动, 需要的时候,使用如下命令开启svn服务svnserve -d -r /opt/s ...
- jquery-ui 中treegird 逐步加载
官方网站上没有ajax逐步加载的例子,自己研究了下 js代码 $("#bomStructureTable").treegrid({ url : "systemcontro ...
- JavaScript、jQuery、HTML5、Node.js实例大全-读书笔记1
技术很多,例子很多,只好慢慢学,慢慢实践!!现在学的这本书是[JavaScript实战----JavaScript.jQuery.HTML5.Node.js实例大全] 第 3 章 用 JavaScri ...
- VS 2013的初配置
首先,安装vs2013,安装过程比较简单,也已有教程,在此不赘述.只想说一下,vs2013需要占C盘比较大的空间:所有功能都安装,且装在C盘的话,约需要9G左右,即使安装在其他盘,也需要占C盘6G左右 ...
- CCM加密学习
这几天终于搞定了AES硬件加密工具的使用,几种简单的加密模式也都实验通过了,比较麻烦的一种是CCM模式的加密,它是CTR加密模式和CMAC认证算法的混合使用.本文先介绍CCM模式的原理与基本实现,然后 ...
- 查找-find -grep
find#.#-name#"*pc"#|#xargs#grep#"Flag" “*.pc”设置要找的文件名grep后面是要找的字符串 #是空格