2

We have a custom LocalizedString type used in our domain model. We want to decorate the properties with validation attributes like MaxLength. For this, we added implicit operators to enable casts required by this attribute.

Strangely, the operator seems to never get called and an InvalidCastException get's thrown in the attributes IsValid method. Performing this cast in our own project works.

Is there a special cast behavior compiler magix going on in this system clr ngen'ed attribute or something?

// Custom type
public class LocalizedString
{
    public string Value
    {
        get { return string.Empty; }
    }

    public static implicit operator Array(LocalizedString localizedString)
    {
        if (localizedString.Value == null)
        {
            return new char[0];
        }

        return localizedString.Value.ToCharArray();
    }
}

// Type: System.ComponentModel.DataAnnotations.MaxLengthAttribute
// Assembly: System.ComponentModel.DataAnnotations, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
// Assembly location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.ComponentModel.DataAnnotations.dll
public override bool IsValid(object value)
{
  this.EnsureLegalLengths();
  if (value == null)
  {
    return true;
  }
  else
  {
    string str = value as string;
    int num = str == null ? ((Array) value).Length : str.Length;
    if (-1 != this.Length)
      return num <= this.Length;
    else
      return true;
  }
}


[TestMethod]
public void CanCallIsValidWithLocalizedString()
{
    // Arrange
    var attribute = new MaxLengthAttribute(20);
    var localized = new LocalizedString { Value = "123456789012345678901" };

    // Act
    var valid = attribute.IsValid(localized);

    // Assert
    Assert.IsFalse(valid);
}

Thanks for your help.

Edit

Das Objekt des Typs "Nexplore.ReSearch.Base.Core.Domain.Model.LocalizedString" kann nicht in Typ "System.Array" umgewandelt werden.
bei System.ComponentModel.DataAnnotations.MaxLengthAttribute.IsValid(Object value)
bei Nexplore.ReSearch.Base.Tests.Unit.Infrastructure.CodeFirst.MaxLengthLocalizedAttributeTests.CanCallIsValidWithLocalizedString() in d:\Nexplore\SourceForge\Nexplore.ReSearch.Base\Source\Nexplore.ReSearch.Base.Tests.Unit\Infrastructure.CodeFirst\MaxLengthLocalizedAttributeTests.cs:Zeile 40.
benwasd
  • 1,342
  • 10
  • 18

2 Answers2

9

Operators of any kind only apply if the type of object is known at compile time. They are not applied "on the fly" to object.

You could try using dynamic which does do that.

Examples:

using System;

class Foo
{
    public static implicit operator Array(Foo foo)
    {
        return new int[0]; // doesn't matter
    }
    static void Main()
    {
        Foo foo = new Foo();
        Array x = (Array)foo; // implicit operator called via compiler
        dynamic dyn = foo;
        Array y = (Array)dyn; // implicit operator called via dynmic
        object obj = foo;
        Array z = (Array)obj; // implicit operator NOT called
                              // - this is a type-check (BOOM!)
    }
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 1
    +1 I had to read twice until I saw "this is a type check"! So the only way to convert from an object of unknown type to something else is to use IConvertible? – Adriano Repetti Jun 14 '12 at 07:24
  • 1
    @Adriano I'm not sure I would lean too much towards `IConvertible`, but indeed *something* like that, yes – Marc Gravell Jun 14 '12 at 07:29
  • So in this case we'd probably have to create our own custom LocalizedMaxLengthAttribute for this to work on LocalizedString? Are there other ways? – sharp johnny Jun 18 '12 at 16:26
  • Ran into this exact problem. My deserialization routines return an Int64Proxy object, which when read from a dynamic ExpandObject field has their implicit Int64 operator called. However, because ExpandoObject will throw an error if I try to access the value when it doesn't exist, I have to first cast ExpandoObject to an IDictionary and call TryGetValue, which returns an "object". Such a plain "object" type cannot be casted implicitly, despite the fact that the object's underlying type is Int64Proxy which contains the implicit cast operator. So yes, dynamic works. – Triynko Jan 27 '14 at 20:59
  • Why on earth does ExpandoObject throw an error if a field does not exist. It's a dynamic object, it should have an explicit value like Flash/AS3 of "undefined" to differentiate it from an existing null value. Instead of just being able to check whether the field is present, since the client may or may not include the value I'm looking for (it's optional), I cannot just write `if (data.optionalField != undefined)`, I have to write `object fieldValue; if ((data as IDictionary).TryGetValue( "optionalField", out fieldValue ))` etc. – Triynko Jan 27 '14 at 21:04
  • Then I have to stick the object returned by TryGetValue back into the dynamic object before I can implicitly cast it to its final value, since "object" cannot be implicitly casted. What a terrible implementation. – Triynko Jan 27 '14 at 21:05
1

You write

((Array) value)

But the static type of value is object. So this is compiled as a cast from object to Array. Your conversion operator is never even considered.

Change that to

((Array)(value as LocalizedString))

and you'll be fine.

Kris Vandermotten
  • 10,111
  • 38
  • 49
  • Unless LocalizedString is a private class which defines the implicit conversion operators. I use a private Int64Proxy class that results from a deserialization process (JSON.NET cannot deserialize to a value-type struct like Int64 from a two-part high/low value, so I had to create a reference-type proxy class that can be initialized via two separate assignments of the high and low parts). When read from a dynamic object, the implicit operator works, but if I have to test for its existence via IDictionary, the resulting object cannot be casted to Int64Proxy, since its a private class. – Triynko Jan 27 '14 at 21:17
  • Only a "dynamic" object will properly look up and call the implicit operator on my Int64Proxy class, so because ExpandoObject throws errors instead of returning undefined or null when a non-existing field value is requested, I am forced to use the `IDictionary` interface to try to get a plain old object value, whose implicit operator cannot be called unless I first cast to Int64Proxy, which I cannot do as it's a private class. – Triynko Jan 27 '14 at 21:19