Binary compatibility of .Net interfaces

1.6k views Asked by At

Say we have a base interface defined in C# like so:

interface IBase
{
    int Prop1 { get; set }
    string Prop2 { get; set }
}

Then we have a derived interface as follows:

interface ISub1: IBase
{
    int Prop3 { get; set }
}

These interfaces are defined in an API assembly against which custom applications compile and run. (The assembly also includes non-exposed classes which implement these interfaces and public factory methods for obtaining instances). All extant code uses ISub1, there is no existing code which directly references IBase. It was done this way in anticipation that we might eventually want to introduce a second derived interface ISub2, as a peer of ISub1, and that has now come to pass. Unfortunately though we find that ISub2 should not contain Prop2 (only Prop1 and some additional unique properties), hence we want to "demote" that property down into ISub1, resulting in the following revised interfaces:

interface IBase
{
    int Prop1 { get; set }
}

interface ISub1: IBase
{
    string Prop2 { get; set }
    int Prop3 { get; set }
}

interface ISub2: IBase
{
    string Prop4 { get; set }
}

Given that there are no consumers of IBase it seems like we should be able to do this with impunity (and I'm fairly sure we could do that in Java), but when attempting to do so we have run into a binary compatibility problem with code compiled against the old interface definitions. Specifically:

ISub1 s1 = ... // get an instance
s1.Prop2 = "help";

This code, when run against the new interface definition, fails with an exception as follows:

System.MissingMethodException : Method not found: 'Void MyNamespace.IBase.set_Prop2(System.String)'.

Note the reference to IBase. I presume this to be because the seeming call to ISub1.set_Prop2 has been compiled with a tight binding to where Prop2 is actually introduced, in IBase.

Can anyone help me with a way out of this conundrum? I.e. is there a way to re-factor the interfaces so that the definition of ISub2 is "clean" (does not include the extraneous Prop2)? Asking all existing applications to recompile is out of the question.

5

There are 5 answers

0
xanatos On

By writing it in TryRoslyn it becomes quite evident that there is a difference based on where you put the property in the interface:

Given:

interface ISub1A: IBaseA
{
    int Prop3 { get; set; }
}

interface IBaseA
{
    int Prop1 { get; set; }
    string Prop2 { get; set; }
}

interface ISub1B: IBaseB
{
    int Prop3 { get; set; }
    string Prop2 { get; set; }
}

interface IBaseB
{
    int Prop1 { get; set; }
}

and

ISub1A a = null;
a.Prop2 = "Hello";

ISub1B b = null;
b.Prop2 = "Hello";

(note that in both cases I'm using the ISub1* interface in C# code)

The generated IL code is:

IL_0001: ldstr "Hello"
IL_0006: callvirt instance void IBaseA::set_Prop2(string)
IL_000b: ldnull
IL_000c: ldstr "Hello"
IL_0011: callvirt instance void ISub1B::set_Prop2(string)

so the IL code "correctly" resolves to the interface where the property is really defined.

0
Hadi Brais On

First, you should hide ISub2.Prop2 by implementing it explicitly. Then, depending on why ISub2 should not contain Prop2, you should either deprecate that implementation using the ObsoleteAttribute attribute or throw an InvalidOperationException from both accessors.

1
paparazzo On

Kind of hacky and not sure it will work but maybe worth a try

interface IBase0
{
    int Prop1 { get; set; }
}
interface IBase : IBase0
{
    int Prop1 { get; set; }
    string Prop2 { get; set; }
}
interface ISub1: IBase
{
    int Prop3 { get; set; }
}
interface ISub2 : IBase0
{
    int Prop4 { get; set; }
}
0
Damien_The_Unbeliever On

Basically, this:

interface ISub1: IBase

Just says "any class that implements ISub1 will promise to also implement IBase". There's no mixing of the methods defined within each interface, such that it also means "ISub1 contains 3 properties, Prop1 - Prop3".

So that's why it's not working. ISub1 is currently defined to require exactly one property called Prop3.

0
Erik Hart On

While this question is quite old, I want to mention a similar problem I had, when splitting up an interface into a base and inheriting type. Because it was part of a Nuget package with the same major release version, it had to be downward compatible with previous versions. I solved it by duplicating the members in the original interface with the "new" keyword.

MissingMethodException after extracting base interface