A Quirk With Implicit vs Explicit Interfaces

Aaron Powell - Sep 11 '19 - - Dev Community

The other day I got to work and the first thing I did was open an IL disassembler and got to town reading the IL of some code I was having a problem with.

That’s a pretty normal start to most .NET developers day right? Right…?

Nope

It all came about as I’m doing some exploration of Durable Entities which is part of the Durable Functions v2 preview that was announced at Build. I was using the new strongly-typed support that appeared in the beta-2 release, and allows you to write entities like this:

public interface ICounter
{
    void Add(int count);
    void Clear();
}

[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class Counter : ICounter
{
    [JsonProperty]
    public int Count { get; set; }

    public void Add(int count) => Count += count;
    public void Clear() => Entity.Current.DestructOnExit();

    [FunctionName(nameof(Counter))]
    public Task Run([EntityTrigger] IDurableEntityContext ctx) => ctx.DispatchAsync<Counter>();
}
Enter fullscreen mode Exit fullscreen mode

And then we can invoke it in a strongly-typed manner, rather than using magic strings:

// ctx is IDurableEntityContext
await ctx.SignalEntityAsync<ICounter>(id, proxy => proxy.Add(1));
Enter fullscreen mode Exit fullscreen mode

This gives some nice type-safety to the way that you work with your entities.

Naturally though, I wasn’t writing this in C#, I was using F#, which means the code looks more like this:

type ICounter =
    abstract member Add: int -> unit
    abstract member Clear: unit -> unit

[<JsonObject(MemberSerialization = MemberSerialization.OptIn)>]
type Counter() =
    [<JsonProperty>]
    member val Count = 0 with get, set

    interface ICounter with
        member this.Add count =
            this.Count <- this.Count + count

        member this.Clear() =
            this.Count <- 0

    [<FunctionName("Counter")>]
    member __.Run([<EntityTrigger>] ctx : IDurableEntityContext) =
        ctx.DispatchAsync<Counter>()
Enter fullscreen mode Exit fullscreen mode

And the invocation is:

do! ctx.SignalEntityAsync<ICounter>(id, (proxy : ICounter) => proxy.Add(1)) |> Async.AwaitTask
Enter fullscreen mode Exit fullscreen mode

But it kept throwing a highly cryptic error within the Durable Functions framework that the call to the method Add failed, but it wouldn’t tell me why. After a bunch of debugging into the source of Durable Functions I found the cause of the failure, the Add method wasn’t being found on the Counter instance. But the C# code worked just fine, so what gives?

Well it turns out that in F# when you implement an interface it’s implemented explicitly, whereas in C# you can implement an interface implicitly or explicitly.

Interface Implementations, Implicit vs Explicit

Before really understanding the problem we need to understand a bit about how interface implementations work. Let’s do it in C# since it supports both types and we’ll start with an implicit implementation. This is what you’re most likely using when you’re working with interfaces, and it looks like this:

interface IFoo {
    void Bar();
}

class Foo : IFoo {
    public void Bar() { }
}
Enter fullscreen mode Exit fullscreen mode

When you do an implicit interface implementation you are adding public non-static methods to the class, and they have to be public non-static (see here). What this means is that the class can be thought of as itself (Foo) or its interface(s) (IFoo) and the members provided by that interface are part of the class, they are implicitly there.

Ok, so what’s an explicit interface implementation look like?

interface IFoo {
    void Bar();
}

class Foo : IFoo {
    void IFoo.Bar() { }
}
Enter fullscreen mode Exit fullscreen mode

The difference this time is that in our class we have a non-public Bar function that is prefixed with the interface name (IFoo). Since the member is non-public we have to be explicit that the type is the interface if we want to access the members that are provided by the interface. In the docs there are a few scenarios of why you’d want to use explicit interface implementations over implicit, and it mainly comes down to how to handle multiple interfaces on a single type.

Since F# only supports explicit interface implementations you only think of types as their interface, which is how I tend to think of types-implementing-interfaces in C# anyway, so what’s the big deal?

Not All IL is Generated the Same

Back to the problem that I’d discovered, it was telling me that the type I was providing, Counter, didn’t have a member Add that could be accessed, and now that we understand how the members of an explicit interface are defined, that actually makes sense, there is no member Add on Counter, its name is actually ICounter.Add, because it’s only part of the interface implementation.

Here’s the IL generated:

.method private final hidebysig newslot virtual
    instance void UserQuery.ICounter.Add (
        int32 count
    ) cil managed
{
    .override method instance void UserQuery/ICounter::Add(int32)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldarg.0
    IL_0002: call instance int32 UserQuery/Counter::get_Count()
    IL_0007: ldarg.1
    IL_0008: add
    IL_0009: call instance void UserQuery/Counter::set_Count(int32)
    IL_000e: nop
    IL_000f: ret
}
Enter fullscreen mode Exit fullscreen mode

Compare that to the implicit interface implementation:

.method public final hidebysig newslot virtual
    instance void Add (
        int32 count
    ) cil managed
{
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldarg.0
    IL_0002: call instance int32 UserQuery/Counter2::get_Count()
    IL_0007: ldarg.1
    IL_0008: add
    IL_0009: call instance void UserQuery/Counter2::set_Count(int32)
    IL_000e: nop
    IL_000f: ret
}
Enter fullscreen mode Exit fullscreen mode

Notice the difference between .method definitions, our explicit is private while implicit is public and the name of the explicit is UserQuery.ICounter.Add (Namespace.InterfaceName.MemberName) compared to Add for implicit.

Why Does It Matter?

Ok, it’s all very interesting learning about the differences in the IL generated by the compiler, but why is this important to know?

Well, it turns out that you need to understand this difference if you’re doing Reflection. Let’s say you have a type and you want to get a method of that type by its name. To do that you’d write code like this:

var method = typeof(T).GetMethod(
    methodName,
    BindingFlags.IgnoreCase |
    BindingFlags.Public |
    BindingFlags.NonPublic |
    BindingFlags.Instance
);
Enter fullscreen mode Exit fullscreen mode

But this will fail if the method name you’re looking for is provided by an explicitly implemented interface!

Try out this code on try.dot.net:

using System;
using System.Linq;
using System.Reflection;

public class Program
{
  public static void Main()
  {
    Console.WriteLine(string.Join(", ", typeof(FooE).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Select(m => m.Name)));
    Console.WriteLine(string.Join(", ", typeof(FooI).GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Select(m => m.Name)));
  }
}

interface IFoo
{
    void Bar();
}

class FooE : IFoo
{
    void IFoo.Bar() => Console.WriteLine("Explicit Bar");
}

class FooI : IFoo
{
    public void Bar() => Console.WriteLine("Implicit Bar");
}
Enter fullscreen mode Exit fullscreen mode

The output will be:

IFoo.Bar, Equals, Finalize, GetHashCode, GetType, MemberwiseClone, ToString
Bar, Equals, Finalize, GetHashCode, GetType, MemberwiseClone, ToString
Enter fullscreen mode Exit fullscreen mode

Meaning that our call to GetMethod and just providing Bar will return null since this is no method on that type with that name!

Conclusion

This was a really fun problem to try and solve, it’s been a long time since I dived deep into .NET internals and it’s quite interesting to learn the difference in the way interface implementations are handled.

There’s an open bug on Durable Functions to work out a way to resolve this and it turned out that I caught a few people with this one!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player