Conversion operator behavior and casting

Today I hit something that I just assumed would work, but absolutely did not.  The issue was around the explicit conversion operators, which I use from time to time when I create Value Objects that wrap a specific primitive with extra behavior.  Concepts like Email, Phone Number, Money and others tend to wrap a single primitive, but with some extra behavior.  Now, in C# 3.0, extension methods can be used to add this extra behavior.  But I tend to think extension methods can be a form of primitive obsession.  So instead, I’ll create Value Object classes, but with some conversion operators to help go back and forth from primitive types to the Value Object.

However, the behavior of conversion operators isn’t exactly what I imagined.  For example, I have the fun Foo and Bar types:

public class Foo
    public Foo(int value)
        Value = value;

    public int Value { get; set; }

    public static explicit operator Bar(Foo foo)
        return new Bar(foo.Value + 10);

public class Bar
    public Bar(int otherValue)
        OtherValue = otherValue;

    public int OtherValue { get; set; }

    public static implicit operator Foo(Bar bar)
        return new Foo(bar.OtherValue + 5);

One defines an implicit operator, and the other an explicit conversion operator.  Here’s an example of the implicit and explicit operators in action:

public void This_will_work()
    Bar bar = new Bar(10);

    Foo foo = bar; // Implicit conversion operator


    Bar bar2 = (Bar) foo; // Explicit conversion operator


In the implicit operator, the Bar instance is implicitly converted to the Foo instance.  For the other way around, I need to explicitly cast to Bar for the explicit conversion operator to kick in from Foo to Bar.

Still making sense, right?  Here’s where things get a bit…wonky:

public void Does_not_work()
    object bar = new Bar(7);
    // does not compile
    // Foo foo = bar; 

    object foo = new Foo(14);
    Bar bar2 = (Bar) foo;


Now, the first conversion won’t work because the compiler only sees the object type, but at runtime it’s of type Bar.  However, the interesting thing is that the explicit conversion operator fails!  Not only that, it’s a completely bizarre and ridiculous exception:

System.InvalidCastException : Unable to cast object of type 'CastingBehavior.Foo' to type 'CastingBehavior.Bar'.

No, there is a cast available.  We created the explicit conversion operator for just that purpose.  Popping open Reflector reveals the true culprit.  Here’s the IL for the implicit operator conversion that worked:

L_000a: call class CastingBehavior.Foo CastingBehavior.Bar::op_Implicit(class CastingBehavior.Bar)

Huh.  Okay, here’s the explicit conversion that worked in the first test:

L_001f: call class CastingBehavior.Bar CastingBehavior.Foo::op_Explicit(class CastingBehavior.Foo)

Hmmm.  Not looking too promising.  Finally, here’s the IL from the cast that threw the exception:

L_0011: castclass CastingBehavior.Bar

And there’s the culprit!  Now, I’m no IL expert, but it’s easy to see that the two cast operations are doing two completely different things.  One is calling the explicit cast operator (which is a cleverly disguised method on the Foo type), and the other is calling “castclass”, whatever that is.

This means that conversion operators are evaluated at compile time, not run-time.  Which is rather unfortunate, the conversion operators are nothing more than a cheap C# compiler trick, and don’t do anything interesting in the CLR.  The same applies for all of the operator overloads, they are fancy compiler tricks to call specifically named methods.  But conversion operators aren’t embedded into the CLR, nor is the CLR aware of them.  It’s just another method call at that point.

Which is tricky, as it doesn’t look any different.  You get an InvalidCastException, even though you’ve defined an explicit conversion operator.  But this is not a “cast substitute” – it’s merely an operator.  A cast is a cast, except when it isn’t, and when it isn’t, it can trip you up with runtime exceptions as we saw here.

Worst Visual Studio message ever