0

I am building an extensible framework to separate client-based business logic and data structures from our common infrastructure, based off a MVC4 WebAPI system.

To this end, I created an interface IApiServiceEntryPoint like so:

public interface IApiServiceEntryPoint : IDisposable
{
    /// <summary>
    /// Gets the name of the API Plugin
    /// </summary>
    string Name { get; }

    /// <summary>
    /// Registers the assembly in the application, 
    /// sets up the routes, and enables invocation of API requests
    /// </summary>
    void Register(RouteCollection routes);

    /// <summary>
    /// Gets the routing namespace of the plugin
    /// </summary>
    string UrlNameSpace { get; }
}

Then, in the same assembly, I created a PluginHelper class like so:

public static class PluginHelper
{
    private static readonly List<IApiServiceEntryPoint> _plugins = new List<IApiServiceEntryPoint>();
    public static List<IApiServiceEntryPoint> Plugins { get { return _plugins; } }

    private static readonly log4net.ILog Logger = log4net.LogManager.GetLogger(typeof(PluginHelper));
    /// <summary>
    /// Registers all IApiServiceEntryPoint plugin classes.
    /// </summary>
    /// <param name="pluginPath">The directory where the plugin assemblies are stored.</param>
    public static void Register(string pluginPath, RouteCollection routes)
    {
        Logger.InfoFormat("Registering plugins found at \"{0}\"...", pluginPath);
        foreach (var plugin in _plugins)
        {
            Logger.DebugFormat("Disposing plugin {0}...", plugin.Name);
            plugin.Dispose();
        }

        Logger.DebugFormat("Clearing the plugin cache...");
        _plugins.Clear();

        var libraryFiles = System.IO.Directory.GetFiles(pluginPath, "*.*")
            .Where(fn => fn.ToLowerInvariant().EndsWith(".dll") 
                || fn.ToLowerInvariant().EndsWith(".exe"))
            .ToList();
        Logger.DebugFormat("Found {0} assemblies in the plugin directory...", libraryFiles.Count);

        var assemblies = libraryFiles.Select(lf => Assembly.LoadFrom(lf))
            .ToList();
        Logger.DebugFormat("Loaded {0} assemblies into memory: {1}", assemblies.Count, string.Join(", ", assemblies.Select(a=>a.FullName).ToArray()));

        var pluginTypes = assemblies.Where(assy => assy != null)
            .SelectMany(assy => assy.GetTypes())
            .Where(t => !t.IsInterface && !t.IsAbstract && t.Namespace != null) 
            .ToList();
        Logger.DebugFormat("Located a total of {0} classes.", pluginTypes.Count);

        pluginTypes = pluginTypes.Where(t => t.IsTypeOf<IApiServiceEntryPoint>())
            .ToList();
        Logger.DebugFormat("Located a total of {0} plugin entry points.", pluginTypes.Count);

        foreach (var type in pluginTypes)
        {
            Logger.DebugFormat("Registering plugin type '{0}'...", type.Name);
            var plugin = (IApiServiceEntryPoint)Activator.CreateInstance(type);
            Logger.InfoFormat("Registering plugin \"{0}\"...", plugin.Name);
            plugin.Register(routes);
            Logger.InfoFormat("Plugin \"{0}\" Registered.", plugin.Name);
            _plugins.Add(plugin);
        }

        Logger.InfoFormat("All {0} plugin(s) have been registered.", Plugins.Count);
    }

    public static bool IsTypeOf<T>(this Type type)
    {
        return type.GetInterfaces().Any(t =>t.Name == typeof(T).Name); 
    }
}

Note the extension method IsTypeOf()... I originally tried to implement this using a form of IsAssignableFrom() but it never seemed to work... which I think may be related to my issue.

Next I created an abstract class in the same assembly:

public abstract class ApiPlugin : IApiServiceEntryPoint, IAccessControl
{
    private static readonly ILog Logger = log4net.LogManager.GetLogger(typeof(ApiPlugin));
    public abstract string Name { get; }

    public virtual void Register(RouteCollection routes)
    {
        var rt = string.Format("{0}/{{controller}}/{{id}}", UrlNameSpace);
        var nameSpace = this.GetType().Namespace;
        Logger.DebugFormat("Route Template: {0} in namespace {1}...", rt, nameSpace);
        var r = routes.MapHttpRoute(
            name: Name,
            routeTemplate: rt,
            defaults: new { id = RouteParameter.Optional, controller = "Default" }
            );
        r.DataTokens["Namespaces"] = new[] { nameSpace };

        Logger.InfoFormat("Plugin '{0}' registered namespace '{1}'.", Name, nameSpace);
    }

    public abstract string UrlNameSpace { get; }

    public bool IsAuthorized<T>(Func<T> method)
    {
        var methodName = method.Method.Name;
        var userName = User.Identity.Name;
        return
            ValidateAccess(userName, methodName) &&
            ValidateLicense(userName, methodName);
    }

    protected virtual bool ValidateAccess(string userName, string methodName)
    {
        // the default behavior to allow access to the method.
        return true;
    }

    protected virtual bool ValidateLicense(string userName, string methodName)
    {
        // the default behavior is to assume the user is licensed.
        return true;
    }

    public abstract IPrincipal User { get; }

    public abstract void Dispose();

    public virtual bool ClientAuthorized(object clientId)
    {
        return true;
    }
}

So far, all is working swimmingly. Now to write my first plugin in its own assembly. I made it very simple:

public class DefaultPlugin : ApiPlugin
{
    private static readonly ILog Logger = log4net.LogManager.GetLogger(typeof(DefaultPlugin));

    [HttpGet]
    public DateTime GetSystemTimeStamp()
    {
        if (IsAuthorized(GetSystemTimeStamp))
        {
            return DateTime.UtcNow;
        }
        throw new AuthorizationException();
    }

    public override string Name
    {
        get { return "Default API Controller"; }
    }


    public override string UrlNameSpace
    {
        get { return "Default"; }
    }

    public override System.Security.Principal.IPrincipal User
    {
        get { return new GenericPrincipal(new GenericIdentity("Unauthenticated User"), new[] { "None" }); }
    }

    public override void  Dispose()
    {
        //TODO: Unregister the plugin.   
    }
}

I built this, and I referenced the binary directory for this plugin in my MVC project in my invocation of the plugin registration.

When the PluginHelper.Register() method is called, I find the plugin class, but on the following line:

var plugin = (IApiServiceEntryPoint)Activator.CreateInstance(type);

I end up with the following InvalidCastException being thrown:

Unable to cast object of type 'myPluginNameSpace.DefaultPlugin' to type 'myInterfaceNamespace.IApiServiceEntryPoint'

Here's the thing: it absolutely is an implementation of that interface.

Now I've done this sort of plugin thing before, so I know it can work, but for the life of me I cannot figure out what I am doing wrong. I expect it has something to do with specific builds/ versions, or perhaps strong naming? Please advise.

Jeremy Holovacs
  • 22,480
  • 33
  • 117
  • 254

2 Answers2

0

Try

public static bool IsTypeOf<T>(this Type type)
{
    return typeof(T).IsAssignableFrom(type); 
}

or

public static bool IsTypeOf<T>(this Type type)
{
    return type.GetInterfaces().Any(t => t.FullName == typeof(T).FullName); 
}

Also, try throwing a Debug.Assert(typeof(T).IsInterface) just to be sure.

scottt732
  • 3,877
  • 2
  • 21
  • 23
  • That's pretty much what I started with, and it didn't have any noticeable effect. – Jeremy Holovacs Feb 14 '14 at 14:23
  • Do you have PostSharp or anything rewriting IL? I realize it's redundant, but what happens if you explicitly add the interface., ex public class DefaultPlugin : ApiPlugin, IApiServiceEntryPoint ? You don't by any chance have 2 ApiPlugin or IApiServiceEntryPoint's defined in your project on accident (bad using statement). Your PluginHelper code looks fine otherwise. It shows "Located a total of 1 plugin entry points."? Try using reflection to execute the constructor rather than Activator.CreateNewInstance() and/or log the interfaces of the plugin type. Consider trying MEF if all else fails – scottt732 Feb 14 '14 at 16:27
0

Per @HOKBONG, the answer here was exactly the same thing I was doing, and the proposed solution worked like a charm.

Community
  • 1
  • 1
Jeremy Holovacs
  • 22,480
  • 33
  • 117
  • 254