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.