Git Product home page Git Product logo

nuplug's Introduction

Build status Nuget Nuget Nuget Issue Stats Issue Stats Coverage Status

NuPlug

Plugin management powered by NuGet and MEF.

What is it?

The idea of using NuGet for plugin management is not new, e.g. Visual Studio manages its extensions with NuGet as does JetBrains ReSharper. As we can find, most ideas date back to about 2011 when Matt Hamilton announced that Comicster uses NuGet for plugins.

Quick start

You should have a plugin and an application consuming the plugin.

Application

First, install NuPlug to your application

Install-Package NuPlug

Then in the startup part, create a package manager. Here is an example snippet from the ConsoleSample application:

var packageSource = "https://mynugetfeed/"; // UNC share, folder, ... 
var feed = PackageRepositoryFactory.Default.CreateRepository(packageSource);
var packageManager = new NuPlugPackageManager(feed, "plugins") 
  { 
     Logger = new TraceLogger(),
     TargetFramework = VersionHelper.GetTargetFramework()
  };

This will download NuGet packages from the specified package source to the output directory plugins.

Next, you need to specify which plugin packages to load. The most common way is to use a packages.config Xml file, e.g.

var packagesConfig = new XDocument(
	new XElement("packages",
		new XElement("package", new XAttribute("id", "NuPlug.SamplePlugin"), new XAttribute("version", version))
	));

As an alternative, you can use xml files or string for the configuration.

const string xml = @"<?xml version=""1.0"" encoding=""utf-8""?>
<packages>
    <package version=""0.1.1-beta0001"" id=""Caliburn.Micro.TestingHelpers"" targetFramework=""net452"" />
</packages>";
var xdoc = XDocument.Parse(xml);

Then install your plugin packages by

packageManager.InstallPackages(packagesConfig);

// When plugins update, be sure to remove previous versions 
packageManager.RemoveDuplicates();

Finally, load the installed packages using NuGetPackageContainer<T> typed to your plugin interface. The console sample uses AutoFac modules:

var modulePlugins = new NuGetPackageContainer<IModule>(packageManager);
modulePlugins.Update();

var builder = new ContainerBuilder();

foreach (var module in modulePlugins.Items)
	builder.RegisterModule(module);

var container = builder.Build();

Plugin

For the plugin part, you need to export your implementation as the specified plugin contract interface. The sample application specified IModule as contract, so for the SamplePlugin

[Export(typeof(IModule))]
public class MyPluginModule : Module
{
	protected override void Load(ContainerBuilder builder)
	{
		Trace.TraceInformation("Load: " + GetType().Name);
		base.Load(builder);
	}
}

The build a NuGet package of your plugin project, push it to your feed and you are set.

Controlling type discovery from plugins

You can filter the types for MEF to discover by using the TypeFilter property of PackageContainer<TItem>. By default, the package container only discovers public implementations of TItem, i.e.

TypeFilter = type =>
	type.IsPublic && type.IsClass && !type.IsAbstract && typeof(TItem).IsAssignableFrom(type);

This assumes that MEF does not need to resolve or compose any dependencies to instantiate the requested plugins. Note that in the provided examples we use AutoFac for dependency injection, not MEF.

Controlling assembly discovery from NuGet packages

As noted in Issue #7 the MEF part may load an awfully large number of assemblies, especially when considering the full dependency tree. As this not only may cause large startup times (preventing just-in-time code loading) but may also bypass binding redirects of the main application.

Since v0.4 we added optional support for filtering the assemblies to be scanned by MEF

var regex = new Regex("Plugin.dll$");
var packageContainer = new PackageContainer<string> { FileFilter = regex.IsMatch });

Using the example above, the package container will only scan files with names matching the specified regular expression, in this case files ending with Plugin.dll.

Register plugin dependencies (MEF)

In case your plugins need dependencies, you can add these to the package container's CompositionBatch. Here is an example

[Export(typeof(IPlugin)
public class MyPlugin : IPlugin
{
    public MyClass(IPluginDependency dependency) { ... }
}

Then setup the package container like this

var packageContainer = new PackageContainer<IPlugin>();
// add service provider to satisfy plugin constructors
packageContainer.AddExportedValue(_serviceProvider);
...
if (!packageContainer.Items.Any())
	packageContainer.Update();

Using MEF conventions

You can even use MEF conventions by setting the Conventions property like this

var conventions = new RegistrationBuilder();
conventions.ForTypesDerivedFrom<IDisposable>()
    .ExportInterfaces();

packageContainer.Conventions = conventions;
...
if (!packageContainer.Items.Any())
	packageContainer.Update();

Note that you use conventions only to select exports but not to hide types like with PartNonDiscoverableAttribute. This is why we added the TypeFilter property.

Selecting the target framework

There is good chance that your plugins themselves have runtime dependencies. This is called transitive dependencies and resolving these dependencies is what package managers like NuGet are really made for.

However, with more and more packages becoming cross-platform, you should only need to install the dependencies needed for the runtime target framework of your application.

This is especially true, when you are hosting a NuGet feed for your plugins yourself. As of the current NuGet.Core.2.10.1, the PackageManager does not consider the target framework specified in the packages.config.

We think that this will be addressed soon by the Nuget team. Meanwhile you should use NuPlugPackageManager like in the example specified above, i.e.

var packageManager = new NuPlugPackageManager(feed, "plugins") 
  { 
     Logger = new TraceLogger(),
     TargetFramework = VersionHelper.GetTargetFramework()
  }; 

Where to go from here?

Some hints getting up to speed in production with NuPlug

1. Automate packaging

To get up to speed you should automate as much of the manual tasks as possible and make it part of your build process. For instance, we do NuGet packaging using OneClickBuild).

2. Speeding up development cycles for DEBUG

During hot development, short feedback cycles are king. Note that, decoupling your code using plugins is cool but is likely to increase your development cycles unless you automate building and publishing the plugins within the standard Visual Studio build. To move fast, we totally skip NuGet packaging during DEBUG and just load the plugin assemblies from a directory. For this to work, you need two things

  1. Have an AfterBuild target to copy your modules output to this directory. For instance, we include a Sample.targets containing a step to auto-copy the build plugin package to the local feed directory

     <PropertyGroup>
         <UseLocalPackages Condition="'$(Configuration)' == 'Debug' And $(RootNamespace.EndsWith('Plugin')) And '$(NCrunch)' != '1'">True</UseLocalPackages>
       </PropertyGroup>
     
       <Target Name="CopyLocalPackage" DependsOnTargets="Package" AfterTargets="Build" Condition="'$(UseLocalPackages)' == 'True' " >
         <ItemGroup>
           <Packages Include="$(ProjectDir)\*.nupkg"/>
         </ItemGroup>
         <Message Text="Copying Package '$(ProjectName)' to output ..." Importance="High" Condition="'@(Packages->Count())' &gt; 0"/>
         <Copy SourceFiles="@(Packages)"
               DestinationFolder="$(SolutionDir)Samples\feed\"
               SkipUnchangedFiles="True"
               Condition="'@(Packages->Count())' &gt; 0"/>
       </Target>
    
  2. Have a factory for the IPackageContainer<T> deciding which implementation to use at runtime. For DEBUG just use the PackageContainer<T> base class like this:

     private static IPackageContainer<TItem> CreateDirectoryContainer(string localPath)
     {
     	var packageContainer = new PackageContainer<TItem>();
     	if (Directory.Exists(localPath))
     	{
     		foreach (var directory in Directory.GetDirectories(localPath))
     			packageContainer.AddDirectory(directory);
     	}
     	else
     	{
     		Trace.TraceWarning("Packages directory \"{0}\" does not exist", localPath);
     	}
     	return packageContainer;
     }
    

with localPath ~= ..\..\..\feed\.

nuplug's People

Contributors

mkoertgen avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.