0

This question is related to this other question.

I have the following method:

public static T GetNewData<T>(params Action<dynamic>[] actions) where T : class, new()
{
    dynamic dynamicData = new DeepObject();

    foreach (var action in actions)
    {
        action(dynamicData);
    }

    return Converter.Convert<T>(dynamicData);
}

The users of this method will include less technical people, even non-developers and as such the easier writing calls to this method is the better. My sticking point right now is that by using Action<dynamic> as the parameter type there is no intellisense provided to the user. In the context I know that the intellisense should be acting as if the dynamic was in fact T.

So is their a way I could either: Tell Visual Studio to use type T for the intellisense or change the parameter to be Action<T> and somehow programmatically change it to be Action<dynamic> or Action<DeepObject> so that the call to it will succeed?

EDIT: To clarify, the types that I am using for T are not of type DeepObject and they do not inherit any standard interface, the use of DeepObject is to allow setting up nested types without the user needing to explicitly instantiate at each level. This was the original usage before adding the dynamic and DeepObject code:

ExampleDataFactory.GetNewData<ServicesAndFeaturesInfo>(
    x => x.Property1 = ExampleDataFactory.GetNewData<Property1Type>(),
    x => x.Property1.Property2 = ExampleDataFactory.GetNewData<Property2Type>(),
    x => x.Property1.Property2.Property3 = ExampleDataFactory.GetNewData<Property3Type>(),
    x => x.Property1.Property2.Property3.Property4 = true);

Here is what it looks like now:

ExampleDataFactory.GetNewData<ServicesAndFeaturesInfo>(
    x => x.Property1.Property2.Property3.Property4 = true);

EDIT: Here is the fully implemented solution based on nmclean's answer

public static DataBuilder<T> GetNewData<T>() where T : class, new()
{
    return new DataBuilder<T>();
}

The DataBuilder Class:

public class DataBuilder<T>
{
    public readonly T data;

    public DataBuilder()
    {
        data = Activator.CreateInstance<T>();
    }

    public DataBuilder(T data)
    {
        this.data = data;
    }

    public DataBuilder<T> SetValue<T2>(Expression<Func<T, T2>> expression, T2 value)
    {
        var mExpr = GetMemberExpression(expression);

        var obj = Recurse(mExpr);
        var p = (PropertyInfo)mExpr.Member;
        p.SetValue(obj, value); 
        return this;
    }

    public T Build()
    {
        return data;
    }

    public object Recurse(MemberExpression expr)
    {
        if (expr.Expression.Type != typeof(T))
        {
            var pExpr = GetMemberExpression(expr.Expression);
            var parent = Recurse(pExpr);

            var pInfo = (PropertyInfo) pExpr.Member;
            var obj = pInfo.GetValue(parent);
            if (obj == null)
            {
                obj = Activator.CreateInstance(pInfo.PropertyType);
                pInfo.SetValue(parent, obj);
            }

            return obj;
        }
        return data;
    }

    private static MemberExpression GetMemberExpression(Expression expr)
    {
        var member = expr as MemberExpression;
        var unary = expr as UnaryExpression;
        return member ?? (unary != null ? unary.Operand as MemberExpression : null);
    }

    private static MemberExpression GetMemberExpression<T2>(Expression<Func<T, T2>> expr)
    {
        return GetMemberExpression(expr.Body);
    }
}

The Usage:

ExampleDataFactory.GetNewData<ServicesAndFeaturesInfo>()
            .SetValue(x=> x.Property1.EnumProperty, EnumType.Own)
            .SetValue(x=> x.Property2.Property3.Property4.BoolProperty, true)
            .Build();
Community
  • 1
  • 1

2 Answers2

1

Do not use Action<dynamic>, use Action<T>with method's constraint where T:DeepObject. Users will get intellisence and ability to use strongly typed objects:

public static DeepObject GetNewData<T>(params Action<T>[] actions) 
 where T : DeepObject, //restrict user only inheritors of DeepObject 
           new()       //and require constructor
{
    var data = new T(); 

    foreach (var action in actions)
    {
        action(data); 
    }

    return data; 
}
xtmq
  • 3,380
  • 22
  • 26
  • Sorry I wasn't very clear about what T was, I have added an edit that I hope explains the situation a bit better. –  Jan 17 '14 at 20:07
0

Does the user need to access unknown properties or add new ones? If not, using dynamic objects seems like a step backwards. If your desired syntax does compile as an Action<T>, I think you should just declare it that way and then go with your first instinct of using the LINQ Expression API to decide how to interpret the code.

Unfortunately, although statements, such as an assignment, are part of the API, C# doesn't support converting them to expression trees. This is not allowed:

public static T GetNewData<T>(params Expression<Action<T>>[] actions)
                where T : class, new() {
    ...
}

...

ExampleDataFactory.GetNewData<ServicesAndFeaturesInfo>(
    x => x.Property1.Property2.Property3.Property4 = true);

Only single-line expressions that would have a return a value are supported. So I think the best you could do is something like this:

public class Assignment<T> {
    public readonly Expression Expression;
    public readonly object Value;

    public Assignment(Expression<Func<T, object>> expression, object value) {
        Expression = expression;
        Value = value;
    }
}

...

public static T GetNewData<T>(params Assignment<T>[] assignments)
                where T : class, new() {
    var data = Activator.CreateInstance<T>();

    foreach (var assignment in assignments) {
        // todo:
        // - pull property names from assignment.Expression
        // - initialize nested properties / assign to assignment.Value
    }

    return data;
}


...

ExampleDataFactory.GetNewData<ServicesAndFeaturesInfo>(
    new Assignment<ServicesAndFeaturesInfo>(
        x => x.Property1.Property2.Property3.Property4, true));

Getting the property names from an expression tree of chained property access is not too complicated. Here is one implementation.

Of course, the new Assignment<ServicesAndFeaturesInfo>(...) is ugly and repetitive, so maybe it could be restructured to something like this:

var newData = ExampleDataFactory.NewData<ServicesAndFeaturesInfo>();
newData.Add(x => x.Property1.Property2.Property3.Property4, true);
newData.Add(...);
...
newData.Get();
Community
  • 1
  • 1
nmclean
  • 7,564
  • 2
  • 28
  • 37
  • This solution, looks to be the closest I'm going to get to what I want. The builder pattern will be slightly more difficult to implement as I am not able to modify the classes that I will be returning. –  Jan 20 '14 at 17:25
  • @JustinM You wouldn't need to modify the classes -- I was thinking along the lines of `NewData` returns some `DataBuilder`, and `DataBuilder.Get` returns the internal `T` object. – nmclean Jan 20 '14 at 18:23
  • Ahh ok, that is exactly how I ended up implementing it. I will most the code at the bottom of my question for those that are interested. –  Jan 20 '14 at 18:38