數據生成器Bogus的使用以及基於聲明的擴展

引言

最近在整理代碼,發現之前寫的一個數據填充器寫了一半沒實現,而恰恰這段時間就要用到相似的功能,因此正好實現下。前端

目標

這個工具的目標是可以在項目初期快速搭建一個「數據提供器」,快速的爲前端提供數據支撐,從而方便項目定型;固然,或許這不是一個正確的開發流程。不過存在決定方法,這裏不討論理想狀況。基於這個目標,目前有兩種方式:git

  1. 基於「倉儲」的「僞實現」。因爲項目框架中進行了倉儲隔離,因此能夠考慮爲倉儲提供一個「數據池」,在忽略業務的情形下快速提供數據。基於IoC的思想,這種實現中,業務層必須不曉得「假數據」的存在。因此不能讓和這個組件相關的任何信息、任何代碼傾入業務。也就是說,不能使用聲明式的方案,得使用相似於EF的Map的方案。
  2. 基於「應用程序層」的「僞實現」。第一個方案的缺點是仍然不夠快,須要編寫必定量的代碼。因此第二個方案的特色是,所有基於Attribute聲明,快速肯定先後端須要傳輸的數據類型。由於這些定義的數據類型屬於DTO,也沒有必要去清理這些定義好的Attribute——並且,若是設計得當的話,徹底能夠將這些Attribute做爲數據驗證的依據。

選擇

總的來講就是兩個選擇,要麼本身實現,要麼站在前人的基礎上調整。
在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);
        }
    }

在這個實現中,額外作了如下事情:

  • 兼容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. 本地化支持 ...
相關文章
相關標籤/搜索