配色: 字号:
数据生成器Bogus的使用以及基于声明的扩展
2017-01-19 | 阅:  转:  |  分享 
  
数据生成器Bogus的使用以及基于声明的扩展



目标

这个工具的目标是能够在项目初期快速搭建一个“数据提供器”,快速的为前端提供数据支撑,从而方便项目定型;当然,或许这不是一个正确的开发流程。不过存在决定方法,这里不讨论理想情况。基于这个目标,目前有两种方式:



基于“仓储”的“伪实现”。由于项目框架中进行了仓储隔离,所以可以考虑为仓储提供一个“数据池”,在忽略业务的情形下快速提供数据。基于IoC的思想,这种实现中,业务层必须不晓得“假数据”的存在。所以不能让和这个组件相关的任何信息、任何代码倾入业务。也就是说,不能使用声明式的方案,得使用类似于EF的Map的方案。

基于“应用程序层”的“伪实现”。第一个方案的缺点是仍然不够快,需要编写一定量的代码。所以第二个方案的特点是,全部基于Attribute声明,快速确定前后端需要传输的数据类型。因为这些定义的数据类型属于DTO,也没有必要去清理这些定义好的Attribute——而且,如果设计得当的话,完全可以将这些Attribute作为数据验证的依据。

选择

总的来说就是两个选择,要么自己实现,要么站在前人的基础上调整。

在Nuget上搜索了下DataGenerater,发现不少的匹配项。找了其中一个下载量比较大的:



Bogushttps://github.com/bchavez/Bogus



细看了下文档,感叹群众的眼睛果然是雪亮的。



扩展

Bogus完全符合[目标]这一节的第一点要求,但是没有发现基于Attribute的使用方式。所以决定自己扩展下。Bogus的配置入口是一个泛型类:Faker<>,配置方法是RuleFor,这个方法包含了2个重载,而且都是两个参数的。第一个参数都是一个MemberAccess的LambdaExpression,这个参数指示了你希望针对哪个属性配置。第二个参数是一个委托,指示了你希望如何返回值。该组件的Faker(非泛型)类型提供了丰富的数据提供方式。这也是这个组件最大的价值所在。以下是摘自GitHub的几个例子:



vartestUsers=newFaker()

//Optional:Callforobjectsthathavecomplexinitialization

.CustomInstantiator(f=>newUser(userIds++,f.Random.Replace("###-##-####")))



//Basicrulesusingbuilt-ingenerators

.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)



//Useanenumoutsidescope.

.RuleFor(u=>u.Gender,f=>f.PickRandom())

//Useamethodoutsidescope.

.RuleFor(u=>u.CartId,f=>Guid.NewGuid())

//Compoundpropertywithcontext,usethefirst/lastnameproperties

.RuleFor(u=>u.FullName,(f,u)=>u.FirstName+""+u.LastName)

//Andcomposabilityofacomplexcollection.

.RuleFor(u=>u.Orders,f=>testOrders.Generate(3).ToList())

//Afterallrulesareappliedfinishwiththefollowingaction

.FinishWith((f,u)=>

{

Console.WriteLine("UserCreated!Id={0}",u.Id);

});



varuser=testUsers.Generate();

基于这个配置的例子,我们的思路也就清晰了。需要自定定义一个Attribute,声明某个属性需要填充数据。运行期间,我们需要分析这个类型的元素据,提取Attribute上的值,然后通过调用Bogus实现的Faker来为类型指定填充规则。而通过不同的Attribute(通常,会设计成继承于同一个基类)我们可以指定不同的数据填充方式。

首先,这里是我们定义的Attribute基类:



namespaceDRapid.Utility.DataFaker.Core

{

[AttributeUsage(AttributeTargets.Property)]

publicabstractclassBogusAttribute:Attribute

{

///

///返回一个指定的值提供器

///


///

publicabstractFuncGetValueProvider();



publicstaticRandomRandom=newRandom();

}

}

这是一个抽象类,因为我们必须要求所有的Attribute指明如何填充某个属性。抽象方法的返回值是一个委托,而且兼容Bogus的委托的定义——这样,我们就可以充分利用Bogus内建的大量功能。例如,下面就是一个随机填充一个姓名的实现:



///

///指示数据填充器使用一个[全名]填充属性

///


publicclassBogusFullNameAttribute:BogusAttribute

{

publicName.GenderGender{get;set;}



publicoverrideFuncGetValueProvider()

{

returnf=>f.Name.FindName(null,null,null,null,Gender);

}

}

接下来我们需要实现自己的调用入口。在这个实现中,有点麻烦的就是要在运行期间进行泛型相关的操作。否则就无法正确的转接到Bogus的基础实现中,所以会稍微用到一些运行时“编译”:



///

///为指定的类型机型假数据配置

///


///

publicstaticobjectConfig(Typetype)

{

vargType=typeof(Faker<>).MakeGenericType(type);

dynamicdGenerator=Activator.CreateInstance(gType,"zh-cn",null);

varproperties=type.GetProperties(BindingFlags.Public

|BindingFlags.Instance

|BindingFlags.SetProperty);

foreach(varpropertyInfoinproperties)

{

varattr=propertyInfo.GetCustomAttribute();

if(attr!=null)

{

varbuilderType=typeof(PropertyExpressionBuilder<,>)

.MakeGenericType(type,propertyInfo.PropertyType);

dynamicbuilder=Activator.CreateInstance(builderType);

varexpression=builder.Build(propertyInfo);

varvalueProvider=attr.GetValueProvider();

varparamExp=Expression.Parameter(typeof(Faker));

varinvokeExp=Expression.Invoke(Expression.Constant(valueProvider),paramExp);

varnullCheckExp=Expression.Equal(Expression.Constant(null),invokeExp);

varconvertExp=Expression.Convert(invokeExp,propertyInfo.PropertyType);

varconditionExp=Expression.Condition(nullCheckExp,

Expression.Default(propertyInfo.PropertyType),

convertExp);

dynamicproviderExp=Expression.Lambda(conditionExp,paramExp);

dGenerator.RuleFor(expression,providerExp.Compile());

}

}

objectexConfigs;

if(_externalConfigs.TryGetValue(type,outexConfigs))

{

dynamicdList=exConfigs;

foreach(vardItemindList)

{

dGenerator.RuleFor(dItem.Item1,dItem.Item2);

}

}

returndGenerator;

}

这里使用了lambda表达式树来在运行期间生成一些代码,同时使用了若干个线程安全的字典来保证只对一个类型配置一次。这个Config方法所做的事情,正如上文所述,就是很明确的两件:1,分析类型的元数据;2,调用Faker<>的RuleFor方法。而大部分的代码是为了做第二件事做准备——构造调用RuleFor方法所需要的参数,仅此而已。以下是完整的实现:



publicclassBogusDataStore

{

privatestaticConcurrentDictionary

_fakers=newConcurrentDictionary();



privatestaticConcurrentDictionary

_fakeLists=newConcurrentDictionary();



privatestaticConcurrentDictionary

_externalConfigs=newConcurrentDictionary();



privatestaticConcurrentDictionary

_randomSource=newConcurrentDictionary();



///

///为指定的类型机型假数据配置

///


///

publicstaticobjectConfig(Typetype)

{

vargType=typeof(Faker<>).MakeGenericType(type);

dynamicdGenerator=Activator.CreateInstance(gType,"zh-cn",null);

varproperties=type.GetProperties(BindingFlags.Public

|BindingFlags.Instance

|BindingFlags.SetProperty);

foreach(varpropertyInfoinproperties)

{

varattr=propertyInfo.GetCustomAttribute();

if(attr!=null)

{

varbuilderType=typeof(PropertyExpressionBuilder<,>)

.MakeGenericType(type,propertyInfo.PropertyType);

dynamicbuilder=Activator.CreateInstance(builderType);

varexpression=builder.Build(propertyInfo);

varvalueProvider=attr.Gewww.tt951.comtValueProvider();

varparamExp=Expression.Parameter(typeof(Faker));

varinvokeExp=Expression.Invoke(Expression.Constant(valueProvider),paramExp);

varnullCheckExp=Expression.Equal(Expression.Constant(null),invokeExp);

varconvertExp=Expression.Convert(invokeExp,propertyInfo.PropertyType);

varconditionExp=Expression.Condition(nullCheckExp,

Expression.Default(propertyInfo.PropertyType),

convertExp);

dynamicproviderExp=Expression.Lambda(conditionExp,paramExp);

dGenerator.RuleFor(expression,providerExp.Compile());

}

}

objectexConfigs;

if(_externalConfigs.TryGetValue(type,outexConfigs))

{

dynamicdList=exConfigs;

foreach(vardItemindList)

{

dGenerator.RuleFor(dItem.Item1,dItem.Item2);

}

}

returndGenerator;

}



///

///为指定的类型生成一个对象并填充数据

///


///

///

publicstaticTGenerate()whereT:class

{

varfaker=_fakers.GetOrAdd(typeof(T),Config);

return(fakerasFaker).IfNotNull(i=>i.Generate());

}



///

///为指定的类型生成一个集合并填充数据

///


///

///

///

publicstaticIListGenerate(intcount)whereT:class

{

varfaker=_fakers.GetOrAdd(typeof(T),Config);

return(fakerasFaker).IfNotNull(i=>i.Generate(count).ToList());

}



///

///为指定的数据生成一个集合,并填充数据,使用指定的key在内存中存储

///


///

///

///

///

///

publicstaticIListGenerateOrGet(intcount,stringkey=null,

boolrefreshAll=false)whereT:class

{

key=key.IsNullOrWhiteSpace()?typeof(T).FullName:key;

//ReSharperdisableonceAssignNullToNotNullAttribute

varlist=(List)_fakeLists.GetOrAdd(key,i=>newList());

lock(list)

{

if(refreshAll)

{

list.Clear();

}

varcountToFill=count-list.Count;

if(countToFill>0)

{

varitems=Generate(countToFill);

list.AddRange(items);

}

}

returnlist;

}



///

///为指定的类型指定特定的值提供器

///


///

///

///

///

publicstaticvoidRuleFor(Expression>exp,

FuncvalueProvider)whereTInstance:class

{

varexConfigs=_externalConfigs.GetOrAdd(typeof(TInstance),

k=>newList>,Func>>());

varconfigList=(List>,

Func>>)exConfigs;

varitem=newTuple>,

Func>(exp,valueProvider);

configList.Add(item);

}



///

///使用指定的键和值配置随机生成器的数据源

///


///

///

publicstaticvoidConfigRandomSet(stringkey,IListrandomSet)

{

_randomSource.TryAdd(key,randomSet);

}



///

///尝试使用指定的键获取一个随机数据源

///


///

publicstaticIListTryGetRandomSet(stringkey)

{

IListresult;

_randomSource.TryGetValue(key,outresult);

returnresult;

}

}

publicclassPropertyExpressionBuilder

{

publicExpression>Build(PropertyInfopropertyInfo)

{

varparam=Expression.Parameter(typeof(TInstance));

varmemberAccess=Expression.MakeMemberAccess(param,propertyInfo);

returnExpression.Lambda>(memberAccess,param);

}

}

在这个实现中,额外做了以下事情:



兼容Bogus默认的配置方式,从而解决一些无法使用特性配置的问题

实现一个基于内存的存储方式,方便使用

实现一个“随机数据池”,使得可以随机从这个池中提取一个项,作为假数据的一个值

测试

对于声明的类型:



publicclassPerson

{

[BogusFullName]

publicstringFakeName{get;set;}



publicstringName

{

get{returnLastName+FirstName;}

}



[BogusFirstName]

publicstringFirstName{get;set;}



[BogusLastName]

publicstringLastName{get;set;}



[BogusRandomCodeText("###-???-")]

publicstringJobCode{get;set;}



[BogusJobType]

publicstringJobType{get;set;}



[BogusJobTitle]

publicstringJobTitle{get;set;}



[BogusRandomInt(MaxValue=100,MinValue=0)]

publicintAge{get;set;}



[BogusRandomItem("genders")]

publicName.GenderGender{get;set;}



[BogusRandomBool]

publicboolHasWife{get;set;}



[BogusRandomDouble]

publicdoubleScore{get;set;}

}

将生成如下结果:



{"FakeName":"MackHackettMD","Name":"HoegerTiana","FirstName":"Tiana","LastName":"Hoeger","JobCode":"666-QTX-YUC","JobType":"Architect","JobTitle":"CentralInteractionsSupervisor","Age":36,"Gender":1,"HasWife":false,"Score":-9.2717476334228441E+307}



对于生成10w个1中所述的类型的对象,将耗时:



4556ms

献花(0)
+1
(本文系thedust79首藏)