Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RuleForType on object graphs #188

Closed
nycdotnet opened this issue Nov 9, 2018 · 3 comments
Closed

RuleForType on object graphs #188

nycdotnet opened this issue Nov 9, 2018 · 3 comments
Labels

Comments

@nycdotnet
Copy link

Thanks for this awesome library. I've noticed that RuleForType does not appear to work on child objects.

We're using the Google.Api.CommonProtos library in our app, and the Money type in this package has a DecimalValue field that allows setting the amount as a native .NET Decimal, but it only supports 9 digits after the decimal point, and it will throw on creation if the DecimalValue is set to a value that isn't supported. This works great at the root level via a RuleForType like this:

.RuleForType(typeof(Money), f => new Money { DecimalValue = f.Finance.Amount() })

However, this same RuleForType is not used when generating the Money values for children.

Is this a bug? I would have expected RuleForType to work for any child objects generated by the AutoFaker.

Demo console app below. You will have to install AutoBogus and Google.Api.CommonProtos NuGet packages.

using AutoBogus;
using Google.Type;
using System.Collections.Generic;

namespace AutoBogusIssue
{
    class Program
    {
        static void Main(string[] args)
        {
            WorksUsingDotNetBuiltinDecimal();
            WorksUsingGoogleMoneyWithRuleForType();
            DoesNotWorkUsingGoogleMoneyWithRuleForTypeWithChildren();
        }

        private static void WorksUsingGoogleMoneyWithRuleForType()
        {
            var childlessFaker = new AutoFaker<Childless>()
                .RuleForType(typeof(Money), f => new Money { DecimalValue = f.Finance.Amount() });
            var fake = childlessFaker.Generate();
        }

        private static void WorksUsingDotNetBuiltinDecimal()
        {
            var parentWithDecimalFaker = new AutoFaker<ParentWithDecimal>();
            var fake = parentWithDecimalFaker.Generate();
        }

        private static void DoesNotWorkUsingGoogleMoneyWithRuleForTypeWithChildren()
        {
            var parentWithMoneyFaker = new AutoFaker<ParentWithMoney>()
                .RuleForType(typeof(Money), f => new Money { DecimalValue = f.Finance.Amount() });
            var fake = parentWithMoneyFaker.Generate();  // this will likely throw because the faker won't use the rule and instead tries to set the properties on the Money type using the built-in .NET property types
        }
    }


    public class Childless
    {
        public Money Amount { get; set; }
    }

    public class ParentWithMoney
    {
        public Money Amount { get; set; }
        public List<ChildWithMoney> Children { get; set; }
    }

    public class ChildWithMoney
    {
        public Money Amount { get; set; }
    }

    public class ParentWithDecimal
    {
        public decimal Amount { get; set; }
        public List<ChildWithDecimal> Children { get; set; }
    }

    public class ChildWithDecimal
    {
        public decimal Amount { get; set; }
    }
}
@bchavez
Copy link
Owner

bchavez commented Nov 9, 2018

Hi Steve,

Thanks for your issue. I appreciate it. Ultimately, though, you'll have to raise the issue with Nick over at AutoBogus if you think AutoFaker's behavior is a problem.

I can only speak for Bogus and how RuleForType works with Faker<T> within Bogus, not so much for AutoBogus. RuleForType is somewhat undocumented, so I'll try to explain more about how it works within the context of Bogus. So, here we go...

Faker<T> only works with objects of type T and the rules you explicitly define for T's properties. The scope of rules defined in Faker<T> for T only apply at an instance level of T. More specifically, if T has properties of type U then .RuleForType(typeof(U), ...) will only be applied at that instance level of T who's properties are of type U.

.RuleForType(typeof(U),...) is not a registration for any and all types under an object graph of T. .RuleForType is only a rule for direct properties on T of type U. Additionally, note that type U is not the same as List<U>.

In other words, .RuleForType is meant to be used in the following situation. Consider having to write:

class T {
   decimal Temperature1;
   decimal Temperature2;
   decimal Temperature3;
   decimal Temperature4;
   decimal Temperature5;
   decimal Temperature6;
   decimal Temperature7;
}
Faker<T>()
   .RuleFor(t => t.Temperature1, f => f.Random.Deimcal())
   .RuleFor(t => t.Temperature2, f => f.Random.Deimcal())
   .RuleFor(t => t.Temperature3, f => f.Random.Deimcal())
   .RuleFor(t => t.Temperature4, f => f.Random.Deimcal())
   .RuleFor(t => t.Temperature5, f => f.Random.Deimcal())
   .RuleFor(t => t.Temperature6, f => f.Random.Deimcal())
   .RuleFor(t => t.Temperature7, f => f.Random.Deimcal())

Instead you can write:

Faker<T>()
   .RuleForType(typeof(decimal), f => f.Random.Deimcal())

Additionally, Bogus won't fill a collection of type List<U> because fundamentally, List<U> is not the same as U. A collection of List<U> is a completely different type than U itself. Bogus won't make any assumption on how to create a List<U> if it's not told explicitly how to. List<U> brings forth a new dimension of assertions that Bogus can't make assumptions about. Is an empty List<U> valid? How about one U in List<U>? How about 1 billion U in List<U>? I don't know. That's for you to decide.

If you have deeply nested objects or graphs of type V=List<U>, W, X from T then you need to define how that object graph should be built from T with a rule that explicitly tells Bogus how to do so.

For better or worse, Bogus is pretty dumb and mechanical, it wont do anything unless it is told to. 😄


So then the question we're all wondering is, how do you make your third situation work?

One way, manually:

private static void Manually(){
   var moneyFaker = new AutoFaker<Money>()
                      .RuleFor(x => x.DecimalValue, f => f.Finance.Amount() );
                      
   var childMoneyFaker = new AutoFaker<ChildWithMoney>()
                           .RuleFor( x => x.Amount, f => moneyFaker.Generate());
   
   var parentWithMoneyFaker = new Faker<ParentWithMoney>()
         .RuleFor( x => x.Amount, f => moneyFaker.Generate() )
         .RuleFor( x => x.Children, f => childMoneyFaker.Generate(3) );
   
   parentWithMoneyFaker.Generate();
}

Sometimes, manual work is no fun. I'm lazy, I want the computer to do it for me. What if we want to have "registration" like behavior when some type U is created in an object graph. It's a reasonable demand but it requires us digging a little deeper in how things work.

So, getting more crazy, you'll probably need to create your own binder:

private static void GetLazyCrazy()
{
   var parentWithMoneyFaker = new AutoFaker<ParentWithMoney>(new CustomBinder());
   var fake = parentWithMoneyFaker.Generate();
   fake.Dump();
}

public class CustomBinder : AutoBinder
{
   public override TType CreateInstance<TType>(AutoGenerateContext context)
   {
      if (typeof(TType) == typeof(Money)) return (TType)MakeMoney(); //wish it was this easy
      return base.CreateInstance<TType>(context);
   }

   public override void PopulateInstance<TType>(object instance, AutoGenerateContext context, IEnumerable<MemberInfo> members = null)
   {
      if (typeof(TType) == typeof(Money)) return; //already populated from create instance.
      base.PopulateInstance<TType>(instance, context, members);
   }

   public static object MakeMoney() => //the life, ay?
      new AutoFaker<Money>()
         .RuleFor(x => x.DecimalValue, f => f.Finance.Amount())
         .Generate();
}

(you could probably generalize this into some kind of registration object for your application to make it extensible)

Hopefully my answer is clear and gives you a way out of your predicament. Perhaps (and probably) Nick has a few better ideas.

I do wish using Bogus to automagically create object graphs was much easier, and that's what #8 and #74 are all about; but the actual implementation and problem is a difficult one. Some day we might have the situation covered, but for now, this is how you'll have to approach the problem AFAIK.

If you have any more questions, please feel free to continue to ask.

Thanks,
Brian

🤔 ✨ "Do you ponder the manner of things... yeah yeah... like glitter and gold..."

@nycdotnet
Copy link
Author

Thanks for such an outstanding answer. In the meantime I've done more or less what you've suggested. I'd like to submit a PR to crystallize your response in the docs - that cool?

@bchavez
Copy link
Owner

bchavez commented Nov 9, 2018

Hi Steve,

Sure, sounds fine. Send in a PR when you're ready. 😸

Thanks,
Brian

🏖️ 🎺 Beach Boys - Good Vibrations (Nick Warren bootleg)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants