bchavez / Bogus

:card_index: A simple fake data generator for C#, F#, and VB.NET. Based on and ported from the famed faker.js.
Other
8.68k stars 495 forks source link

Set value for private readonly ICollection<T> #367

Closed vlamai closed 3 years ago

vlamai commented 3 years ago

Version Information

Software Version(s)
Bogus NuGet Package 33.0.2
.NET Full Framework? 4.7
Windows OS? 10

What locale are you using with Bogus?

en

What's the problem?

Don't know how to set value for private collection

What possible solutions have you considered?

I try use reflection to set value for this field.

  entity.GetType()
     .GetField(name, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
     .SetValue(entity, val);

Do you have sample code to show what you're trying to do?

First i try to customize Facker

    public class DataFaker<T> : Faker<T> where T : class
    {
//Try to use binder
        public DataFaker(IBinder backingFieldBinder): base(binder: backingFieldBinder)
        {
        }

        public DataFaker()
        {
        }
// Try to set private fileds
        public DataFaker<T> UsingPrivate()
        {
            return base.CustomInstantiator(f => Activator.CreateInstance(typeof(T), nonPublic: true) as T)
                    as DataFaker<T>;
        }
//Try to create custom rule
        public virtual Faker<T> RuleForPrivate<TProperty>(Expression<Func<T, TProperty>> property, TProperty value)
        {
            var propName = PropertyName.For(property);
            propName = propName.First()
                               .ToString()
                               .ToLower() +
                    propName.Substring(1);
            return AddRule(propName, (f, t) => value);
        }
    }

Sample classes

        public class Foo
        {
            public Guid Id { get; protected set; }
        }

        public class Bar
        {
            private readonly ICollection<Foo> someCollection = new HashSet<Foo>();

            public ICollection<Foo> SomeCollection => someCollection;
        }

Sample test

        [Fact]
        public void testToAddFoo()
        {
            var fooList = new DataFaker<Foo>().UsingPrivate()
                                             .RuleFor(x => x.Id, f => f.Random.Guid())
                                             .Generate(1);
            var barObject = new DataFaker<Bar>().UsingPrivate()
                                                .RuleFor(x => x.SomeCollection, fooList)
                                                .Generate();
            testOutputHelper.WriteLine(barObject.SomeCollection.Count.ToString()); // 0
        }

Try to use custom IBinder, but i'm don't understned how it works. someCollection if result dictionary

    public class BackingFieldBinder : IBinder
    {
        public Dictionary<string, MemberInfo> GetMembers(Type t)
        {
            var availableFieldsForFakerT = new Dictionary<string, MemberInfo>();
            var bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
            var allMembers = t.GetMembers(bindingFlags);
            var allBackingFields = allMembers
                                  .OfType<FieldInfo>()
                                  .ToList();

            foreach( var backingField in allBackingFields){
                var fieldName = backingField.Name.First()
                                            .ToString()
                                            .ToLower() +
                        backingField.Name.Substring(1);
                availableFieldsForFakerT.Add(fieldName, backingField);
            }
            return availableFieldsForFakerT;
        }
    }

Test with custom IBinder

 [Fact]
        public void testToAddFoo()
        {
            var backingFieldBinder = new BackingFieldBinder();
            var fooList = new DataFaker<Foo>().UsingPrivate()
                                             .RuleFor(x => x.Id, f => f.Random.Guid())
                                             .Generate(1);
            var barObject = new DataFaker<Bar>(backingFieldBinder).UsingPrivate()
                                                                  .RuleFor(x => x.SomeCollection, fooList)
                                                                  .Generate();
            testOutputHelper.WriteLine(barObject.SomeCollection.Count.ToString()); // 0
        }
bchavez commented 3 years ago

Hi @vlamai,

Your Foo class will work by default. No changes or modifications are required for public protected set properties as shown below:

void Main()
{
   var fakeThings = new Faker<Foo>()
      .RuleFor(x => x.Id, f => f.Random.Guid() );

   var thing = fakeThings.Generate();
   thing.Dump();
}
public class Foo
{
   public Guid Id { get; protected set; }
}

image

However, setting a private field via reflection is slightly harder. You'll need to use a PrivateBinder class to reflect over type T and extract the "private fields" of T into the default discovery dictionary as shown below:

void Main()
{
   var fooFaker = new Faker<Foo>()
      .RuleFor(x => x.Id, f => f.Random.Guid() );

   var fakeFoos = fooFaker.Generate(10);

   var privateBinder = new PrivateBinder();
   var barFaker = new Faker<Bar>(binder: privateBinder)
                      .RuleFor("foos", _ => fakeFoos);

   barFaker.Generate().Dump();
}

public class PrivateBinder : Bogus.Binder
{
   public override Dictionary<string, MemberInfo> GetMembers(Type t)
   {
      var members = base.GetMembers(t);

      var privateBindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
      var allPrivateMembers = t.GetMembers(privateBindingFlags)
                               .OfType<FieldInfo>()
                               .Where( fi => fi.IsPrivate )
                               .Where( fi => !fi.GetCustomAttributes(typeof(CompilerGeneratedAttribute)).Any() )
                               .ToArray();

      foreach( var privateField in allPrivateMembers ){
         members.TryAdd(privateField.Name, privateField);
      }
      return members;
   }
}

public class Bar
{
   private readonly ICollection<Foo> foos = new HashSet<Foo>();
   public ICollection<Foo> SomeCollection => foos;
}
public class Foo
{
   public Guid Id { get; protected set; }
}

image

And that should do it. Check Issue #213 for even more ideas. :sweat_smile:

Thanks, Brian

vlamai commented 3 years ago

thanks for answer @bchavez but in my imagination i set rule as

.RuleFor(x => x.SomeCollection, fooList)

in Binder it transform it to someCollection just by lower case first letter and set it

if usging string

.RuleFor("foos", _ => fakeFoos);

any rename will break all test where it used

Sorry if I'm asking a stupid question.

vlamai commented 3 years ago

Sample code that put values to private collection by name of public

    public class PrivateBinder : Binder
    {
        public override Dictionary<string, MemberInfo> GetMembers(Type t)
        {
            var members = base.GetMembers(t);

            var privateBindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
            var allPrivateMembers = t.GetMembers(privateBindingFlags)
                                     .OfType<FieldInfo>()
                                     .Where( fi => fi.IsPrivate )
                                     .Where( fi => !fi.GetCustomAttributes(typeof(CompilerGeneratedAttribute)).Any() )
                                     .ToArray();

            foreach( var privateField in allPrivateMembers ){
                members.Add(privateField.Name, privateField);
            }
            return members;
        }
    }

public class DataFaker<T> : Faker<T> where T : class
    {
        public DataFaker(IBinder backingFieldBinder): base(binder: backingFieldBinder)
        {
        }

        public DataFaker()
        {
        }

        public DataFaker<T> UsingPrivate()
        {
            return base.CustomInstantiator(f => Activator.CreateInstance(typeof(T), nonPublic: true) as T)
                    as DataFaker<T>;
        }

        public virtual Faker<T> RuleForPrivate<TProperty>(Expression<Func<T, TProperty>> property, TProperty value)
        {
            var propName = PropertyName.For(property);
            propName = propName.First()
                               .ToString()
                               .ToLower() +
                    propName.Substring(1);
            return AddRule(propName, (f, t) => value);
        }
    }

 var data =new DataFaker<Bar>(backingFieldBinder).UsingPrivate().RuleForPrivate(x => x.SomeCollection, value)