Tuesday, October 11, 2011

Fixing error “The binary operator LessThan is not defined for the types 'Type1' and 'Type2'”

During the working over next version of Camlex.NET library we encountered with the interesting problem: when we tried to construct LessThan expression for our types the following exception was thrown:

The binary operator LessThan is not defined for the types 'System.Object' and 'SomeType'.

SomeType here is the inheritor of the class where operation “<” is defined. But at the same time there is no explicitly defined operator “<” in SomeType it self. Nevertheless it allows you to write the following correct C# code:

   1: class BaseType
   2: {
   3:     public static bool operator <(object c1, BaseType c2) { return false; }
   4:     public static bool operator >(object c1, BaseType c2) { return false; }
   5: }
   6:  
   7: class SomeType : BaseType
   8: {
   9: }
  10:  
  11: ...
  12:  
  13: var o = new object();
  14: var c = new DataTypes.Text();
  15: bool b = o < c;

This code is successfully compiled and executed, i.e. operator “<” is called for operands of object and SomeType types. However if you will try to construct LessThan expression with left operand of object type and right operand of SomeType type (using Expression.LessThan method), you will get the mentioned exception. If you will use right operand of type BaseType, expression will be constructed correctly. Also you can define operators “<” and “>” directly in SomeType (and use SomeType as 2nd parameter) – expression will also be created. Seems like Expression.LessThan doesn’t search for operators defined in base classes. Instead it searches only in specified class itself (thanks to Vladimir who found the problem).

However there is more elegant solution for the problem which doesn’t require changing of the code. It was crucial for us because as I said we faced with this issue when working on Camlex where we needed to construct expressions like this:

   1: x => x["Foo"] > (DataTypes.Integer)123

x has SPListItem type which has several indexers, one of them receives string as parameter and returns object. Here is more realistic test:

   1: public class BaseFieldType
   2: {
   3:     public static explicit operator BaseFieldType(string s) { return null; }
   4: }
   5:  
   6: public class BaseFieldTypeWithOperators : BaseFieldType
   7: {
   8:     public static bool operator <(object c1, BaseFieldTypeWithOperators c2) { return false; }
   9:     public static bool operator >(object c1, BaseFieldTypeWithOperators c2) { return false; }
  10: }
  11:  
  12: public class StringBasedFieldType : BaseFieldTypeWithOperators
  13: {
  14:     public bool Contains(string text) { return true; }
  15:     public bool StartsWith(string text) { return true; }
  16: }
  17:  
  18: public static class DataTypes
  19: {
  20:     public class Text : StringBasedFieldType
  21:     {
  22:     }
  23: }
  24:  
  25: public class SPListItem
  26: {
  27:     public object this[string s] { get { return null; } }
  28: }
  29:  
  30: class Program
  31: {
  32:     static void Main(string[] args)
  33:     {
  34:         var mi = typeof(SPListItem).GetProperty("Item",
  35:             typeof(object), new[] { typeof(string) }, null).GetGetMethod();
  36:         var left =
  37:                 Expression.Call(
  38:                     Expression.Parameter(typeof(SPListItem), "x"),
  39:                     mi, new[] { Expression.Constant("foo") });
  40:  
  41:  
  42:         var right = Expression.Convert(Expression.Convert(Expression.Constant("foo"),
  43:             typeof(BaseFieldType)), typeof(DataTypes.Text));
  44:  
  45:         var expr = Expression.LessThan(left, right);
  46:     }
  47: }

This code fails because on left operand passed on the line 48 has type object, and right operand has type DataTypes.Text which is subclass of BaseFieldTypeWithOperators and which doesn't has explicitly defined operators in it.

Solution is quite simple: you should use overridden version of Expression.LessThan method which receives 4 parameters:

   1: BinaryExpression LessThan(Expression left, Expression right, bool liftToNull, MethodInfo method)

In last parameter you should specify MethodInfo variable which represents operator “<” from the base class. I.e. the previous example will look like this:

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         var mi = typeof(SPListItem).GetProperty("Item",
   6:             typeof(object), new[] { typeof(string) }, null).GetGetMethod();
   7:         var left =
   8:                 Expression.Call(
   9:                     Expression.Parameter(typeof(SPListItem), "x"),
  10:                     mi, new[] { Expression.Constant("foo") });
  11:  
  12:  
  13:         var right = Expression.Convert(Expression.Convert(Expression.Constant("foo"),
  14:             typeof(BaseFieldType)), typeof(DataTypes.Text));
  15:  
  16:  
  17:         var info = typeof(BaseFieldTypeWithOperators).GetMethod("op_LessThan");
  18:  
  19:         var expr = Expression.LessThan(left, right, false, info);
  20:     }
  21: }

Difference is in lines 17-19: at first we get reference to operator “<” (op_LessThan) and after that we pass it into Expression.LessThan() method. After this expression will be successfully created.

No comments:

Post a Comment