4

While other questions about using reflection to bypass all safeties and directly call the base class's implementation of an overridden method have generally been met with derision and calls to redesign the offending code, I think I've stumbled upon a bizarre but legitimate use case: delegate serialization. Since I've seen the other questions, please don't bombard me with advice to redesign my code and stop trying to bypass the type system -- I'm writing a serialization formatter, and those already get a pass to ignore constructors.

Much to my dismay, even the v2.0 BCL's BinaryFormatter fails this simple NUnit test:

[TestFixture]
public class DelegateSerializationTestFixture
{
    [Test]
    public void DelegateSerializationTest()
    {
        var bigKitty = new Lion();
        var kittyNoise = bigKitty.GetKittyNoiseFunc();

        Assert.AreEqual("Meow", kittyNoise());

        var stream = new MemoryStream();
        var formatter = new BinaryFormatter();
        formatter.Serialize(stream, kittyNoise);
        stream.Position = 0;
        formatter = new BinaryFormatter();
        var kittyNoise2 = (Func<string>)formatter.Deserialize(stream);

        Assert.AreEqual("Meow", kittyNoise2()); // returns Grrr
    }
}

[Serializable]
public class Lion : Cat
{
    public override string GetNoise()
    {
        return "Grrr";
    }

    public Func<string> GetKittyNoiseFunc()
    {
        return base.GetNoise;
    }
}

[Serializable]
public class Cat
{
    public virtual string GetNoise()
    {
        return "Meow";
    }
}

If BinaryFormatter itself can't get this right, I guess I shouldn't be surprised that my own Compact Framework 3.5 binary serialization implementation drops the ball as well.

If it's truly impossible to reconstruct such a delegate using only the Compact Framework v3.5's limited reflection capabilities -- and it may well be -- is there a way to at least detect such a delegate so that my serializer can throw instead of leaving incorrect data in the stream?

So far, the only way I know of to detect this condition at serialization time is to use full-trust reflection to compare the private method-pointer value in the original delegate with the value I get from reconstructing the delegate using its publicly visible properties.

Community
  • 1
  • 1
Jeffrey Hantin
  • 35,734
  • 7
  • 75
  • 94
  • "but legitimate case" is where I started squinting... delegate serialization is **such** a bad idea in virtually every case, and for so many reasons... why not just serialize *the data* ? – Marc Gravell May 02 '12 at 05:37
  • @Marc It's a long story, but we crossed paths on it [once before](http://stackoverflow.com/questions/4733018). And I did ask folks to hold off on "if it hurts when you do that, don't do that" type responses :-) – Jeffrey Hantin May 02 '12 at 05:40
  • noted, but I was thinking of the "long tail", i.e. the casual observer in 10 months time. A *brief* caveat is very much worth them reading - as indeed is the context in your link in the comment above. – Marc Gravell May 02 '12 at 05:49
  • @Marc I probably need to add more context to both this question and the linked one. Almost everything you suggested in your previous answer -- rolling forward from a known state, CQRS queue, etc -- are elements that prevalence builds by using binary serialization to provide both schema and file format. – Jeffrey Hantin May 02 '12 at 05:55

1 Answers1

3

Interesting question. I've found one very hacky way of detecting it: create a copy using the public properties, and check for equality with the original. Sample code showing it working:

using System;
using System.Linq;
using System.IO;

class Base
{
    public virtual void Foo()
    {
        Console.WriteLine("Base");
    }
}

class Derived : Base
{
    public override void Foo()
    {
        Console.WriteLine("Derived");
    }

    public Action NonVirtualAction { get { return base.Foo; } }
    public Action VirtualAction { get { return Foo; } }
}

class Program
{
    static void Main()
    {
        var derived = new Derived();
        var virtualAction = derived.VirtualAction;
        var nonVirtualAction = derived.NonVirtualAction;

        var virtualCopy = CreateCopy(virtualAction);
        var nonVirtualCopy = CreateCopy(nonVirtualAction);

        Console.WriteLine(virtualCopy.Equals(virtualAction)); // True
        Console.WriteLine(nonVirtualCopy.Equals(nonVirtualAction)); // False
    }

    static Delegate CreateCopy(Delegate del)
    {
        // TODO: Validate it's not multi-cast
        return Delegate.CreateDelegate(del.GetType(), del.Target, del.Method);
    }
}

I don't know how you'd actually create it though... or whether there are other situations where this "equality of copy" would give false positives.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Well, that's an improvement over picking private fields out. I already have to decompose multicast delegates in order to write the details to the stream, so that TODO is pretty much done in my code already. – Jeffrey Hantin May 02 '12 at 05:31