Review
Last time I covered the internals of the CG.Plugin.FileSystem NUGET package, which contains a loader strategy for the CG.Plugin NUGET package. This time I’ll cover another loader strategy, this time one from the CG.Plugin.Reflection package. That package uses reflection to look for command types in the assemblies that are already loaded into the current AppDomain.
CG.Plugin.Reflection
Let’s start our review with the ReflectionSetup class, whose code is shown here:
1 2 3 4 5 6 |
internal sealed class ReflectionSetup : PluginLoaderSetupBase<ReflectionProvider>, IPluginLoaderSetup { } |
Usually, we start with a public interface and then write an abstract base class to go with it but this time, we aren’t adding any new setup properties to the IPluginLoaderSetupBase type, so we don’t need to extend anything with an additional interface.
Let’s go look at the provider class next. The trimmed down code for the FileSystemProvider class is shown here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
internal sealed class ReflectionProvider : PluginLoaderProviderBase, IPluginLoaderProvider { public override IPluginLoaderStrategy GetStrategy( IPlugin product ) { return new ReflectionStrategy( product, this ); } } |
The only thing this provider does is give out instances of the ReflectionStrategy class. It does that in the implementation for the GetStrategy method, as shown above.
The ReflectionStrategy class itself has a little more going on, as shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
internal sealed class ReflectionStrategy : PluginLoaderStrategyBase<IPluginLoaderProvider, IPluginLoaderSetup>, IPluginLoaderStrategy { public ReflectionStrategy( IPlugin product, IPluginLoaderProvider provider ) : base(product, provider) { } public override IEnumerable<ICommand> Load() { // Attempt to discover assemblies in the current app-domain. var assemblies = DiscoverAssemblies(); // Find all the command types in the assemblies. var types = DiscoverTypes(assemblies); // Create all the commands. var commands = CreateCommands(types); // Return the commands. return commands; } private IEnumerable<Assembly> DiscoverAssemblies() { // Create a place to hold assemblies. IEnumerable<Assembly> assemblies = new List<Assembly>(); try { // Attempt to discover assemblies in the current app-domain. assemblies = AppDomain.CurrentDomain.GetAssemblies() .Where(x => !x.IsDynamic) .AsEnumerable(); // Get the white list - if there is one. var assemblyWhiteList = Setup.GetAssemblyWhiteList(); if (assemblyWhiteList.Any()) { assemblies = assemblies.Where( x => assemblyWhiteList.Any( y => x.GetName().Name.Equals( y, StringComparison.InvariantCultureIgnoreCase ) ) ); } else { assemblies = assemblies.Where(x => !x.GetName().Name.StartsWith("System")) .Where(x => !x.GetName().Name.StartsWith("Microsoft")) .Where(x => !x.GetName().Name.StartsWith("mscorlib")); } // Get the black list - if there is one. var assemblyBlackList = Setup.GetAssemblyBlackList(); if (assemblyBlackList.Any()) { assemblies = assemblies.Where( x => !assemblyBlackList.Any( y => x.GetName().Name.Equals( y, StringComparison.InvariantCultureIgnoreCase ) ) ); } } catch (Exception ex) { // Wrap the exception up in a nice bow. throw new PluginException( message: Resources.ReflectionStrategy_DiscoverAssemblies, innerException: ex ); } // Return the assemblies. return assemblies; } private IEnumerable<Type> DiscoverTypes( IEnumerable<Assembly> assemblies ) { // Make a place to hold errors. var errors = new List<Exception>(); // Make a place to hold types. var types = new List<Type>(); try { // Get the black list - if there is one. var commandBlackList = Setup.GetCommandBlackList(); // Get the white list - if there is one. var commandWhiteList = Setup.GetCommandWhiteList(); // Loop through and load types. foreach (var asm in assemblies) { // Find the command types. var temp = asm.GetTypes() .Where(x => typeof(ICommand).IsAssignableFrom(x) && x.IsClass && !x.IsAbstract && !x.IsGenericType ).AsEnumerable(); if (commandWhiteList.Any()) { temp = temp.Where( x => commandWhiteList.Any( y => x.Name.Equals(y, StringComparison.InvariantCultureIgnoreCase) ) ); } if (commandBlackList.Any()) { temp = temp.Where( x => !commandBlackList.Any( y => x.Name.Equals(y, StringComparison.InvariantCultureIgnoreCase) ) ); } // Add the types to the collection. types.AddRange(temp); } } catch (Exception ex) { // Save the error to the collection. errors.Add(ex); } // Were there errors? if (errors.Any()) { // Wrap the exception up in a nice bow. throw new PluginException( message: Resources.DiskStrategy_DiscoverTypes, innerException: new AggregateException(errors) ); } // Return the list of types. return types; } private IEnumerable<ICommand> CreateCommands( IEnumerable<Type> types ) { // Make a place to hold errors. var errors = new List<Exception>(); // Create a place to hold the commands. var commands = new List<ICommand>(); try { // Loop through and create commands. foreach (var type in types) { // Create the command object. var command = Activator.CreateInstance( type ) as ICommand; // Add the command to the collection. commands.Add(command); } } catch (Exception ex) { // Save the error to the collection. errors.Add(ex); } // Were there errors? if (errors.Any()) { // Wrap the exception up in a nice bow. throw new PluginException( message: Resources.DiskStrategy_CreateCommands, innerException: new AggregateException(errors) ); } // Return the commands. return commands; } } |
The strategy class is the only non-trivial class in the package. Because there’s some complexity here, let’s cover things one method at a time.
The constructor simple passes the product and provider to the base class. Nothing else is going on there.
The Load method starts by calling a method named DiscoverAssemblies. That method is reponsible for looking in the current AppDomain and finding all the loaded assemblies, then filtering those assemblies using the optional black and while lists in the Setup.
Once a collection of assemblies is available, that information is passed to the DiscoverTypes method, which is used to search each assembly for types that implement the ICommand interface (the DiscoverTypes method is covered in detail later on).
Once the collection of types is available it is then passed to the CreateCommands method, which is used to create an instance of each command type. That command collection is then returned to the caller.
Now, let’s look at the three sub-methods, DiscoverAssemblies, DiscoverTypes and CreateCommands, in more detail.
DiscoverAssemblies looks in the current AppDomain and returns a raw list of loaded assemblies. This list constitutes everything that the current application has previously loaded into memory. From there, the list is further filtered using optional black and/or white lists from the setup. Notice that any errors in this method are collected and then thrown together at the end. This allows us to know all the reflection related errors before we stop the process and to report them all together at the end.
DiscoverTypes takes the list of assembly references and iterates through them, using reflection on each one, to produce a list of types that implement the ICommand interface. We find out which types implement ICommand with a simple LINQ query that looks for types are assignable from ICommand, are a class, are not abstract, and aren’t generic. We return the list of types for further processing. Notice again that, just like in LoadAssemblies, we collect any errors that occur during this process and throw them all together at the end.
CreateCommands is where we finally try to create actual command objects. This method uses the collection of types from DiscoverTypes and iterates through them, one at a time, using the Activator to creates each instance. Afterwards, the instances are collected into a list of ICommand references and returned to the caller. Here again, we collect any errors that occur during this process and throw them all together at the end.
That’s it for the strategy! The only other class to look at is the IBuilderExtensions class, which is used to house extensions methods for the IBuilder type. The method on this class adds our ReflectionSetup type to a builder before the Build method is called. Let’s look at that class now:
1 2 3 4 5 6 7 8 9 10 11 |
public static partial class IBuilderExtensions { public static IPluginLoaderSetup AddReflectionPluginLoader( this IBuilder builder ) { var setup = new ReflectionSetup(); builder.Setups.Add(setup); return setup; } } |
So here we create a ReflectionSetup object, add it to the builder, then return the reference so the caller can use it to configure the strategy.
Let’s pull everything together now and see how to use the strategy with a plugin…
Using this strategy
In the first article I wrote for the CG.Plugin package, I supplied a quick example of how to create a plugin object. In that example I left out the code that would have configured the builder to use a loader strategy. I’ll fix that now by adding a couple of lines of code so that the builder uses our ReflectionStrategy type. Let’s look at the code again. Here was the original code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public async Task DoExample() { var args = new Dictionary<string, object>() { // TODO : add any arguments here. }; var builder = new PluginBuilder(); // TODO : add setup(s) to the builder … using (var plugin = builder.Build()) { var commands = plugin.ByVerb("verb1", "verb2"); foreach (var command in commands) { await command.ExecuteAsync(args); } } } |
Here is the same code, modified to use our reflection loader strategy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public async Task DoExample() { var args = new Dictionary<string, object>() { // TODO : add any arguments here. }; var builder = new PluginBuilder(); builder.AddReflectionPluginLoader(); using (var plugin = builder.Build()) { var commands = plugin.ByVerb("verb1", "verb2"); foreach (var command in commands) { await command.ExecuteAsync(args); } } } |
That example now loads the proper loader strategy, which looks in the current AppDomain, at all the previously loaded assemblies, and creates instances of all the types that implement ICommand.
Final Thoughts
I’ve used plugins in one form or another for many years. I’ve created a few .NET specific plugin libraries in the past. This plugin library, along with it’s assorted loader strategies, is different in that it integrates closely with my builder library, which makes it very easy to add plugins in places where I would normally have to go invent mechanisms for configuration, creation, lifetime management, and more, just to load a simple plugin into my code. I won’t claim this plugin mechanism is remarkably better than others that are out there. I’ll only claim that this one works well and integrates nicely with my other CODEGATOR NUGET packages.
I hope you’ve enjoyed the article. Have fun with the code!
The code for this article is part of my NUGET package CG.Plugin.Reflection, which can be downloaded for free at https://github.com/CodeGator/CG.Plugin.Reflection
The source code for the CG.Plugin.Reflection project lives on Github and can be obtained for free at https://www.nuget.org/packages/CG.Plugin.Reflection