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

Default BuildObject implementation using reflection #24

Open
robdmoore opened this issue Apr 19, 2015 · 4 comments
Open

Default BuildObject implementation using reflection #24

robdmoore opened this issue Apr 19, 2015 · 4 comments

Comments

@robdmoore
Copy link
Member

Automatically build an object by reflecting for a constructor with the most params and getting each by looking for a property of the same name (kinda like AutoFixture, but this would use the ability to customise the object(s).

This would be the default behaviour for BuildObject - you can then override it as soon as you get a more complex example.

This just makes it even more easy to quickly create a builder.

Might be more complex than it's worth though? It doesn't take long to create the BuildObject method...

@Bringer128
Copy link
Contributor

I've implemented this as an extension method. There's a bit of ninja backflipping to get the data out of the builder when running outside of the builder but that's easily fixed when implementing this for realsies.

public static class TestDataBuilderExtensions
{
    public static TObject BuildObjectByConstructor<TObject, TBuilder>(this TestDataBuilder<TObject, TBuilder> builder)
        where TObject : class
        where TBuilder : TestDataBuilder<TObject, TBuilder>, new()
    {
        var longestConstructor = typeof (TObject)
            .GetConstructors()
            .OrderByDescending(x => x.GetParameters().Length)
            .FirstOrDefault();

        if(longestConstructor == null) throw new ObjectCreationException();

        // TODO Validation around Parameters.All(x => typeof(TObject).HasProperty(...))
        var parameterValues = longestConstructor
            .GetParameters()
            .Select(x => new { PropertyName = x.Name.ToUpperCamelCase(), PropertyType = x.ParameterType })
            .Select(x => GetValueStoredInBuilder(builder, x.PropertyType, x.PropertyName));

        return (TObject) longestConstructor.Invoke(parameterValues.ToArray());
    }

    public static string ToUpperCamelCase(this string param)
    {
        if(string.IsNullOrWhiteSpace(param)) throw new ArgumentNullException("param");

        var builder = new StringBuilder(param);
        builder[0] = char.ToUpperInvariant(builder[0]);

        return builder.ToString();
    }

    public static bool HasProperty(this Type type, string propertyName, Type propertyType)
    {
        var propertyInfo = type.GetProperty(propertyName);

        if (propertyInfo == null) return false;

        return propertyInfo.PropertyType == propertyType;
    }

    private static object GetValueStoredInBuilder<TObject, TBuilder>(TestDataBuilder<TObject, TBuilder> builder, Type propertyType,
        string propertyName) where TObject : class where TBuilder : TestDataBuilder<TObject, TBuilder>, new()
    {
        // Make a Func<TObj, TPropertyType>
        var expressionDelegateType = typeof (Func<,>).MakeGenericType(typeof (TObject), propertyType);

        // Make an expression parameter of type TObj
        var tObjParameterType = Expression.Parameter(typeof (TObject));

        var valueStoredInBuilder = typeof (TBuilder)
            .GetMethod("Get")
            .MakeGenericMethod(propertyType)
            .Invoke(builder, new object[]
            {
                Expression.Lambda(
                    expressionDelegateType,
                    Expression.Property(tObjParameterType, propertyName),
                    tObjParameterType)
            });

        return valueStoredInBuilder;
    }
}

There are obvious shortcomings to this approach:

  1. Constructor argument names must match Property Names exactly, bar the lower-upper camel casing.
  2. There must be a 1-1 relationship to constructor args - properties.

Perhaps this feature could be opt-in by making it a protected method rather than a default?

@robdmoore
Copy link
Member Author

Hi Ryan :)

Nice one!

My thinking was if we couldn't find a matching value via Get that we would then use the anonymous value stuff :) and in fact, the Get method does this for us automatically anyway.

Case sensitivity might actually be an issue because we store the values in a dictionary, we might need to make a chance to make it case insensitive by lowercasing the key when it gets popped in and when it gets checked.

@Bringer128
Copy link
Contributor

To avoid having to implement GetValueStoredInBuilder I think you'll need a private overload of Get that takes a String anyway, so the current version could proxy to the overload and the overload could handle the case sensitivity.

FYI I'm not using this personally, I just had a similar implementation to populate a DTO via setters and figured I should create a basic version while it was still in my head. :)

@Bringer128
Copy link
Contributor

I ended up using my reflective method to call Get<TValue> as I didn't want to pollute the entire generics stack with untyped stuff just for this one use case. We would need to change the whole chain of AnonymouseValueFixture.Get<TObject, T>, IAnonymousValueSupplier.CanSupplyValue and IAnonymousValueSupplier.GenerateAnonymousValue.

I'm not convinced this is a good solution, so perhaps this was a decent exercise in proving it is "too much effort" for the amount of value.

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

No branches or pull requests

2 participants