zoukankan      html  css  js  c++  java
  • 数据生成器Bogus的使用以及基于声明的扩展

    引言

    最近在整理代码,发现以前写的一个数据填充器写了一半没实现,而偏偏这段时间就要用到类似的功能,所以正好实现下。

    目标

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

    1. 基于“仓储”的“伪实现”。由于项目框架中进行了仓储隔离,所以可以考虑为仓储提供一个“数据池”,在忽略业务的情形下快速提供数据。基于IoC的思想,这种实现中,业务层必须不晓得“假数据”的存在。所以不能让和这个组件相关的任何信息、任何代码倾入业务。也就是说,不能使用声明式的方案,得使用类似于EF的Map的方案。
    2. 基于“应用程序层”的“伪实现”。第一个方案的缺点是仍然不够快,需要编写一定量的代码。所以第二个方案的特点是,全部基于Attribute声明,快速确定前后端需要传输的数据类型。因为这些定义的数据类型属于DTO,也没有必要去清理这些定义好的Attribute——而且,如果设计得当的话,完全可以将这些Attribute作为数据验证的依据。

    选择

    总的来说就是两个选择,要么自己实现,要么站在前人的基础上调整。
    在Nuget上搜索了下Data Generater,发现不少的匹配项。找了其中一个下载量比较大的:

    Bogus https://github.com/bchavez/Bogus

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

    扩展

    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默认的配置方式,从而解决一些无法使用特性配置的问题
    • 实现一个基于内存的存储方式,方便使用
    • 实现一个“随机数据池”,使得可以随机从这个池中提取一个项,作为假数据的一个值

    测试

    1. 对于声明的类型:
    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}

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

    4556ms

    更多

    总的来说,这只是个demo。如果要做的更完善的话,还需要考虑以下几个问题:

    1. 更丰富的内容支持
    2. 本地化支持
      ...
  • 相关阅读:
    2019 web安全基础知识学习
    nc语法和nc木马远程控制主机
    公钥、私钥、hash、数字签名、CA以及验证过程
    A5/1流加密理解和算法实现
    TCP/IP和OSI/RM以及协议端口
    【转】TCP/IP网络协议各层首部
    校园网 虚拟机VMware Linux桥接模式 无法上网 问题
    本地远程查看服务器tomcat 上虚拟机信息
    跨域访问的解决
    混合调用tk.mybatis.mapper 与 自编xml文件 的配置
  • 原文地址:https://www.cnblogs.com/lightluomeng/p/6043275.html
Copyright © 2011-2022 走看看