How to get compile-time checking for value types in LINQ/lambdas/expression trees?

.net c# expression-trees lambda linq

Question

I'm using the following code to set Control properties in a thread-safe manner:

private delegate void SetPropertyThreadSafeDelegate<TPropertyType>(Control @this, Expression<Func<TPropertyType>> property, TPropertyType value);

public static void SetPropertyThreadSafe<TPropertyType>(this Control @this, Expression<Func<TPropertyType>> property, TPropertyType value)
{
  var propertyInfo = (property.Body as MemberExpression ?? (property.Body as UnaryExpression).Operand as MemberExpression).Member as PropertyInfo;

  if (propertyInfo == null ||
      !propertyInfo.ReflectedType.IsAssignableFrom(@this.GetType()) ||
      @this.GetType().GetProperty(propertyInfo.Name, propertyInfo.PropertyType) == null)
  {
    throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
  }

  if (propertyInfo.PropertyType.IsValueType &&
      !propertyInfo.PropertyType.IsAssignableFrom(typeof(TPropertyType)))
  {
    throw new ArgumentException(string.Format("Attempted to assign incompatible value type: expecting {0}, got {1}.", propertyInfo.PropertyType, typeof(TPropertyType)));
  }

  if (@this.InvokeRequired)
  {
    @this.Invoke(new SetPropertyThreadSafeDelegate<TPropertyType>(SetPropertyThreadSafe), new object[] { @this, property, value });
  }
  else
  {
    @this.GetType().InvokeMember(propertyInfo.Name, BindingFlags.SetProperty, null, @this, new object[] { value });
  }
}

It's called like so:

downloadProgressBar.SetPropertyThreadSafe(() => downloadProgressBar.Step, 32);

The reason for doing this is to get compile-time checking of property names and type assignments. It works perfectly for standard objects, but everything goes a bit pear-shaped with value types because the compiler is happy to accept the following, which of course bombs at runtime:

downloadProgressBar.SetPropertyThreadSafe(() => downloadProgressBar.Step, 'c');
downloadProgressBar.SetPropertyThreadSafe(() => downloadProgressBar.Step, long.MaxValue);

I've already modified the SetPropertyThreadSafe method to handle the case when value types are used, and throw an exception if the incorrect type is used as an argument, but what I'm really loooking for is the ability to get this method to perform compile-time type checking for 100% of cases, i.e. objects and value types. Is this even possible and if so how would I need to modify my code to do this?

Accepted Answer

Change the contract to:

public static void SetPropertyThreadSafe<TPropertyType, TValue>(
        this Control self,
        Expression<Func<TPropertyType>> property,
        TValue value)
        where TValue : TPropertyType

Note that with this you no longer need to do the IsAssignableFrom check since the compiler will enforce it.

The reasons your example compiled is because the compiler made a guess as to what the type parameter was. Here is what the compiler turns those calls into:

progBar.SetPropertyThreadSafe<int>(() => progBar.Step, 'c');
progBar.SetPropertyThreadSafe<long>(() => progBar.Step, long.MaxValue);

Notice how the first one is int, that's because ProgressBar.Step is an int and 'c' is a char which has an implicit conversion to int. Same with the next example, int has an implicit conversion to long, and the second one is long, so the compiler guesses that it is long.

If you want inheritance and conversions like those to work, don't make the compiler guess. Your two solutions are:

  1. Always specify the type perameter. In this case, you would have noticed that the second one is long, and fixed the problem.

Of course this is less than ideal, because then you are basically hard coding in the type of the Func. What you really want to do is let the compiler determine both types and tell you if they are compatible.

  1. Provide a different type for both so that the compiler can figure it out for you.

NOTE: Below is the code that I would use, which is entirely different from yours:

    public static void SetPropertyThreadSafe<TControl>(this TControl self, Action<TControl> setter)
        where TControl : Control
    {
        if (self.InvokeRequired)
        {
            var invoker = (Action)(() => setter(self));
            self.Invoke(invoker);
        }
        else
        {
            setter(self);
        }
    }

    public static void Example()
    {
        var progBar = new ProgressBar();
        progBar.SetPropertyThreadSafe(p => p.Step = 3);
    }

Popular Answer

You just need to make some minor changes to your generic and expression:

public static void SetPropertyThreadSafe<TSource, TPropertyType>(this TSource source, Expression<Func<TSource, TPropertyType>> property, TPropertyType value)

Then you supply a lambda like so:

var someObject = new /*Your Object*/

someObject.SetPropertyThreadSafe(x => x.SomeProperty, /* Your Value */);

The value you specify must be covariant to the type of SomeProperty, and this is checked at compile time. Let me know if I'm misunderstanding something. If you need to constrain it to Control, you simply change the signature to

this Control source

or

where TSource : Control



Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why