A Tale of Two APIs

A strategy for dealing with multiple public interfaces for libraries.

Published on Thursday, January 22, 2015

When developing libraries it occasionally becomes necessary to expose a different public interface to different groups of users. The most common scenario is one in which your library needs to be accessed in one way by applications that use it, but another way by other libraries that extend it. You want extension developers to have access to all the behind-the-scenes details, but exposing those properties and methods to applications would be confusing or even damaging by promoting improper use. In other words, you want the internal properties and methods to be exposed to one set of developers but not another. In this post I'll examine a strategy for exposing different public APIs to different sets of users.

One way of accomplishing this is to make the extensions friend assemblies by using the InternalsVisibleTo attribute. While tempting, this is a bad idea. Friend assemblies work well for unit testing scenarios and for some enterprise-style development where there is a tight known coupling between components and all the code is well controlled. In any other case, they prevent proper extension by requiring the base library to know about all the extension libraries at compile-time. The web is strewn with disillusioned developers sharing anecdotes of how architectures that rely on InternalsVisibleTo caused them pain and agony.

So if we don't want to make our internal members public for everyone and we also don't want to expose them to specific libraries with InternalsVisibleTo, what can we do? The answer is extension methods. One interesting trait of extension methods is that they aren't available unless their namespace is in scope. This allows us to create sets of extension methods that are only visible if certain namespaces have been explicitly imported.

Consider the following class:

namespace MyLibrary
{
    public class Car
    {
        public int NumberOfTires { get; internal set; }
    }
}

Assume that the number of tires is set by some sort of car factory class internal to the library and we don't want normal library users to change it. However, let's say we did want extension developers to have access to the tire count so that they could create alternate factory classes. Using this approach, the answer would be to create an extension method that would allow changing the number of tires in a special namespace:

namespace MyLibrary.Internal
{
    public static class CarExtensions
    {
        public static void SetNumberOfTires(this Car car, int numberOfTires)
        {
            car.NumberOfTires = numberOfTires;
        }
    }
}

Because the SetNumberOfTires() extension method is still in the MyLibrary project, it has access to the internal NumberOfTires setter. In essence, the extension method is proxying the internal property and making it public. All an extension library has to do in order to use it is to add using MyLibrary.Internal; to any code that needs access.

There are a couple drawbacks to this approach. The first is that by exposing internal code through public extension methods, those bits aren't actually hidden from outside use anymore. While segregating the extensions into a special namespace makes sure they won't pollute the public API, this strategy shouldn't be used if you truly want those properties or methods to remain unavailable to outside code. Another drawback is that the API used to access the internal code doesn't directly match the internal code. For example, you'll end up with a lot of .GetXyz() and .SetXyz() extensions since you can't create extension properties. Also, you obviously can't expose entire classes this way (though I suppose you could put interfaces or proxy classes in the internal namespace for this purpose). Finally, it requires duplicating portions of your code. For every internal property or method you want to expose, you also have to write and maintain a matching extension method. However, if you can live with these limitations and feel that a clean API for different sets of consumers is more important than the maintenance burden, this might just do the trick.

comments powered by Disqus