Assembly Neutral Interfaces

If you look around the ASP.NET vNext code base, you'll see the [AssemblyNeutral] attribute on certain types. People have asked me what they were on Twitter and JabbR so I thought I'd write a blog post about it.

Loose coupling is something we strive for when developing software. We develop systems that are properly layered and well thought out until one day, we need to take a dependency on something that wasn't factored into the big picture.

Today, .NET framework has a messy dependency graph because everytime something new comes along (for e.g. Task) that needs to be "core" it has to be pushed to into a lower assembly. This is why mscorlib has everything and the kitchen sink.

There's been tons of discussions on how techniques like duck typing can help solve these types of layering problems:

Other languages solve these problems in different ways. Go for example, favors composition over inheritance which I think is a good thing. Their interfaces match based on structural equivalence and this allows a library to re-declare a dependency as long as the required members match. In fact, some places in the golang standard library duplicate tiny interfaces rather than take a dependency on another package.

C# even has some duck typing today:

  • The compiler will support foreach on your type if you implement a GetEnumerator method
  • The compiler let you use collection initializer syntax on your type if you have a method called Add.
  • You can make anything awaitable

So what is an assembly neutral interface? Let's talk about the problem we have today in .NET with an example. Let's say the EntityFramework has a dependency on a logger interface:

public namespace EntityFramework  
{
    public interface ILogger
    {
       void Log(string value);
    }
}

Now, a customer that wants to use EF with log4Net:

public namespace EntityFramework.Log4Net  
{
    public class Log4NetLogger : ILogger
    {
        public void Log(string value)
        {
            // Do log4net specific stuff
        }
    }
}

They define this class and put it in a package called EntityFramework.Log4Net.

Another customer wants to use nLog, and the same thing happens.

Wouldn't it be great if everyone agreed on the same logging abstraction? Let's say we lived in a world where they did, what assembly/package would that interface live in? Where would the source live? What would it be called? Who owns it? How does it version? How do you convince each of those library authors to take a dependency on this logger abstraction.

There are examples of this in .NET, the most popular one being the Common Service Locator.

Now as the author of log4net/nLog/name your logger abstraction, you need to maintain packages for every framework you want to integrate with.

What if everyone agreed on an interface definition (the full definition!) and what if it was just a contract. Once agreed upon, this contract can live anywhere as long as the namespace and name match. That's an assembly neutral interface; the full type name of the type is the namespace name plus the type name, there is no assembly name (hence neutral). What this means is the type can be re-declared in source or it could be brought in via a nuget package (just for intellisense, as it's not a deployment artifact).

  • Reflection works so you can inspect types and see if they implement an interface without bizzare logic.
  • Dependency injection works because the type is the exact same type, not a generated proxy.
  • Types can be used as dictionary keys.

Next time, I'll dig into how we implemented it using Roslyn and assembly load magic and how we're using it throughout the stack.

comments powered by Disqus