Different Approaches to Add-In Discovery [Jesse Kaplan]

One of the first problems you try to solve when adding extensibility to your application is that of discovery: how do you find the add-ins to your application? Depending on the application, and what happens to pop into your head at the time, you may go in different directions. I’ve seen solutions where developers needed to discover add-ins from databases, across machine boundaries, and even via RSS feeds but for this post I’d like to concentrate on the simpler scenarios where the application wants to find all the add-ins in a directory, set of directories, or an individual file.

There are three basic patterns for discovery that I’ve seen again and again (and written a couple times myself):

1. Type hierarchy

2. CustomAttribute

3. Manifest/Xml

All of these approaches can be easily generalized to a search in a directory, a set of directories, or an individual file so for the purposes of conversation we’ll take a good middle ground and discuss the case where we’re searching for add-ins in an individual directory.

Type Hierarch Based Discovery

This is a fairly straight forward approach where the host defines an abstract base class or an interface and is trying to find all the types that implement that abstract base class or interface.

The pseudo code for this is pretty straight forward:

       public IList<Type> TypeBased(Type addInBase, string path)

        {

            IList<Type> addinTypes = new List<Type>();

            foreach (Assembly asm in assemblies)

            {

                foreach (Type t in asm.GetTypes())

                {

                    if (addInBase.IsAssignableFrom(t))

                    {

                        addinTypes.Add(t);

                    }

                }

            }

            return addinTypes;

        }

Pros:

· Very straight-forward and relatively easy to code

· Extremely low burden on add-in developers as all they have to do is inherit from a class/interface and place in the right directory

· All of the information is contained in the assembly itself so there is no external registration or description that needs to remain in-sync

Cons:

· The performance of this system can be very poor as it requires loading a lot of assemblies off of disk and reflecting over each type in each assembly to determine whether it meets your criteria

· While the simple implementation of this is simple it starts to get more complicated when you implement this in a real application

o You need to use a different AppDomain because otherwise you’ll end up loading lots of assemblies in your main domain and won’t be able to unload them

o If you care about security you need to use reflection only loading to examine the assemblies and have to deal with the subtleties of using that feature

· You end up finding types that may match your signature but were never intended to be activatable add-ins

· There is no way to get any metadata about the add-in (other than its type and assembly name) without activating it

Custom Attribute Based Discovery

This solution takes the type hierarchy based discovery one step further and uses custom attributes to negate/mitigate some of the cons from the base solution. Depending on which cons you happen to be concerned about there are two main variations on this theme:

1. If you are concerned about knowing exactly which types were intended to be add-ins and getting information about them without executing the add-in you can use a system of requiring that add-ins have a custom attribute applied to them that both tell you more information about the add-in and clearly identify them in the assembly.

2. If you are more concerned about the performance costs of iterating through all types in assembly you can use assembly level custom attributes that specify which types in the assembly are add-ins

The code for #1 requires only slight modifications from the hierarchy based discovery:

       public IList<Type> TypeAttributeBased(Type addInBase, Type customAttr, string path)

        {

            IList<Type> addinTypes = new List<Type>();

            foreach (Assembly asm in assemblies)

            {

                foreach (Type t in asm.GetTypes())

                {

                    if (t.GetCustomAttributes(customAttr,false).Length > 0 &&

                        addInBase.IsAssignableFrom(t))

                    {

                        addinTypes.Add(t);

                    }

                }

            }

            return addinTypes;

        }

The only changes required were to take in the type of custom attribute you expect the add-ins to apply (and that describes any extra information you want about the add-in) and then to check each type to see if that attribute has been applied to it before you verify that it implements the type you expect.

The code for #2 is a little different in that in that you get to replace a foreach over all the types in an assembly to one that only iterates over all the “MyAddInAttribute”’s in the assembly.

       public IList<Type> AsmAttributeBased(Type addInBase, string path)

        {

            IList<Type> addinTypes = new List<Type>();

            foreach (Assembly asm in assemblies)

            {

                MyAddInAttribute[] addins = (MyAddInAttribute [])

asm.GetCustomAttributes(typeof(MyAddInAttribute), false);

                foreach (MyAddInAttribute addInAttribute in addins)

                {

                    addinTypes.Add(addInAttribute.AddInType);

                }

            }

            return addinTypes;

        }

If you wanted you could make #2 even more powerful by requiring that the assembly level custom attributes not just identify the add-in types but also provide you extra information about each one in order to help the host/user decide which ones to load.

 

Pros:

At first glance only slightly harder to code up than a type hierarchy based system

· Allows you to get additional metadata about add-ins without executing them and calling methods

· Can help mitigate the performance problems of examining every type in an assembly

· No external registration steps are required

· Still a very low burden on add-in developers as the only extra work they have to do is to apply a custom attribute on their classes and/or assemblies and inherit from the specified base class/interface

Cons:

· The performance of this can still be bad is it requires you to load many assemblies at discovery time (though not every type in each assembly)

· Again, the devil of the implementation is in the details.

o You still need to create another AppDomain in order to avoid loading all the assemblies you examine into your host’s AppDomain (and thus being prevented from ever unloading them)

o If you care about security you need to use reflection only loading to avoid executing add-in code before you choose an add-in

o Getting information about custom attributes from reflection only loaded assemblies can be incredibly difficult as you need to actually instantiate a custom attribute (and thus execute code) in order to get the information without directly reading from the metadata

· There is a small chance of assemblies getting out of date if you add/remove add-ins from an assembly without making the corresponding change to the custom attributes

 

Manifest/Registration

This solution avoids the requirement of loading and reflecting over assemblies to find the add-in by instead relying on some sort of registration of the add-in that tells the host exactly which assembly to use and which class in it to load. In the past the Windows Registry was a popular location for this type of registration but that has fallen out of vogue and now we see hosts relying on a “manifest” based system instead. Generally the manifest based system relies on the add-ins placing an XML file with a known extension in a particular location that describes the add-in. This description will always include a way to find the assembly and identify the type to use inside it and will often include extra information that helps the host decide which add-in to load.

Here is some pseudo code demonstrating a manifest based discovery system that looks in a directory for all manifests and returns those types back to the caller.

 

public IList<Type> ManifestBasedDiscovery(String path)

{

  IList<Type> addinTypes = new List<Type>();

        foreach (string directory in Directory.GetDirectories(path))

        {

            foreach (string manifestPath in Directory.GetFiles(path, ".manifest"))

            {

                XmlSerializer serializer = new XmlSerializer(typeof(AddInManifest));

                FileStream manifestFile = File.OpenRead(manifestPath);

                AddInManifest manifest = (AddInManifest)serializer.Deserialize(manifestFile);

                Assembly addInAssembly = Assembly.LoadFrom(manifest.AssemblyPath);

                Type addinType = addInAssembly.GetType(manifest.AddInType);

                addinTypes.Add(addinType);

            }

        }

        return addinTypes;

      

}

        public class AddInManifest

        {

            [XmlElement]

            public string AssemblyPath;

            [XmlElement]

            public string AddInType;

        }

Pros:

· This is easily the most performant of these solutions as it avoids loading any assemblies that don’t contain add-ins and doesn’t even have to inspect the assemblies that do

· The code required is easy to write and doesn’t have many of the same pitfalls as the other solutions because it never needs to inspect actual assemblies

o You should do the actual assembly and type loading in a remote AppDomain if you care about sandboxing, but this needs to be done only when you choose to activate an add-in as you know everything you need to about an add-in before you load any of its assemblies (this will be the case for any discovery/activation solution)

Cons:

· The manifest files can be tricky to author and it can be very easy to make a typo in them and be surprised when your add-in simply isn’t found

· It’s easy for an add-in and it’s manifest to get out of sync

o If this happens on an end user machine it can be very hard to fix the problem without a re-install/uninstall of the add-in and even that isn’t guaranteed to solve the problem

o On development machines this happens more often and can be a frustrating experience as it can prevent the app from seeing the add-in or even cause it to load the wrong interim version of the add-in

· Installation of add-ins suddenly becomes more than x-copying the add-in dll to a folder

o A minimum of two files are now needed

o Often the manifest and the add-in will be installed into different locations and will make setup authoring more difficult

· The meta-con here is that the burden placed on the add-in developer and deployer is much higher than with the other options and makes it a stretch for hosts to support entry-level and non-professional developers on their platform

 

 

Next Time

On my next post I’ll do a quick review of the lessons we learned from these solutions and I’ll go into details about the discovery solution we built in the new framework add-in APIs.