├── .gitignore ├── PluginDemo.Common ├── IPlugin.cs ├── PluginDemo.Common.csproj └── Properties │ └── AssemblyInfo.cs ├── PluginDemo.Host ├── AssemblyManager.cs ├── PluginDemo.Host.csproj ├── Program.cs ├── Properties │ └── AssemblyInfo.cs └── Scanner.cs ├── PluginDemo.sln ├── TestPluginOne ├── PluginOne.cs ├── Properties │ └── AssemblyInfo.cs └── TestPluginOne.csproj └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | obj 2 | bin 3 | *.ReSharper.user 4 | _ReSharper.* 5 | *.resharper 6 | *.DotSettings.user 7 | *.csproj.user 8 | *.vbproj.user 9 | *.suo 10 | *.cache -------------------------------------------------------------------------------- /PluginDemo.Common/IPlugin.cs: -------------------------------------------------------------------------------- 1 | namespace PluginDemo.Common 2 | { 3 | public interface IPlugin 4 | { 5 | void Setup(); 6 | void DoWork(); 7 | void Teardown(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /PluginDemo.Common/PluginDemo.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | 9.0.30729 7 | 2.0 8 | {2DC4117F-CDBF-47D4-8F43-1771E57C3476} 9 | Library 10 | Properties 11 | PluginDemo.Common 12 | PluginDemo.Common 13 | v3.5 14 | 512 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 3.5 37 | 38 | 39 | 3.5 40 | 41 | 42 | 3.5 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 59 | -------------------------------------------------------------------------------- /PluginDemo.Common/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("PluginDemo.Common")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("PluginDemo.Common")] 13 | [assembly: AssemblyCopyright("Copyright © 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("f7d7b116-1df3-45cb-8fbd-d30b8459fb17")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /PluginDemo.Host/AssemblyManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using PluginDemo.Common; 4 | 5 | namespace PluginDemo.Host 6 | { 7 | internal class AssemblyManager 8 | { 9 | private AppDomain _domain; 10 | private Scanner _scanner; 11 | 12 | public void LoadFrom(string path) 13 | { 14 | if (_domain != null) 15 | { 16 | _scanner.Teardown(); 17 | AppDomain.Unload(_domain); 18 | } 19 | 20 | var name = Path.GetFileNameWithoutExtension(path); 21 | var dirPath = Path.GetFullPath(Path.GetDirectoryName(path)); 22 | 23 | var setup = new AppDomainSetup 24 | { 25 | ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, 26 | PrivateBinPath = dirPath, 27 | ShadowCopyFiles = "true", 28 | ShadowCopyDirectories = dirPath, 29 | }; 30 | 31 | _domain = AppDomain.CreateDomain(name + "Domain", AppDomain.CurrentDomain.Evidence, setup); 32 | 33 | var scannerType = typeof (Scanner); 34 | _scanner = (Scanner)_domain.CreateInstanceAndUnwrap(scannerType.Assembly.FullName, scannerType.FullName); 35 | _scanner.Load(name); 36 | _scanner.Setup(); 37 | 38 | } 39 | 40 | public void DoWork() 41 | { 42 | _scanner.DoWork(); 43 | } 44 | 45 | public IPlugin ShowCrossDomainPollutionExceptions() 46 | { 47 | return _scanner.ShowCrossDomainPollutionExceptions(); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /PluginDemo.Host/PluginDemo.Host.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | 9.0.30729 7 | 2.0 8 | {31BB4879-9F60-4418-8320-51AA171AFA1C} 9 | Exe 10 | Properties 11 | PluginDemo.Host 12 | PluginDemo.Host 13 | v3.5 14 | 512 15 | 16 | 17 | true 18 | full 19 | false 20 | ..\bin\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | ..\bin\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 3.5 37 | 38 | 39 | 3.5 40 | 41 | 42 | 3.5 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | {2DC4117F-CDBF-47D4-8F43-1771E57C3476} 56 | PluginDemo.Common 57 | 58 | 59 | 60 | 67 | -------------------------------------------------------------------------------- /PluginDemo.Host/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace PluginDemo.Host 5 | { 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | Directory.CreateDirectory("plugins"); 11 | 12 | var manager = new AssemblyManager(); 13 | 14 | manager.LoadFrom("plugins\\TestPluginOne.dll"); 15 | manager.DoWork(); 16 | 17 | Directory.Delete("plugins", true); 18 | 19 | manager.DoWork(); 20 | manager.ShowCrossDomainPolutionExceptions(); 21 | 22 | Console.WriteLine("Press any key to continue..."); 23 | Console.ReadKey(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PluginDemo.Host/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("PluginDemo.Host")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("PluginDemo.Host")] 13 | [assembly: AssemblyCopyright("Copyright © 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("1cbd52c9-cfad-4b68-8ea0-cfe564c76d1f")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /PluginDemo.Host/Scanner.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using PluginDemo.Common; 6 | 7 | namespace PluginDemo.Host 8 | { 9 | [Serializable] 10 | internal class Scanner : MarshalByRefObject 11 | { 12 | private readonly List _plugins; 13 | 14 | public Scanner() 15 | { 16 | _plugins = new List(); 17 | 18 | 19 | } 20 | 21 | private void GetAllPlugins(AppDomain domain) 22 | { 23 | var pluginType = typeof (IPlugin); 24 | 25 | var types = domain.GetAssemblies() 26 | .SelectMany(a => a.GetTypes()) 27 | .Where(t => t.GetInterface(pluginType.Name) != null); 28 | 29 | var ctors = types.Select(t => t.GetConstructor(new Type[] {})) 30 | .Where(c => c != null); 31 | 32 | _plugins.Clear(); 33 | _plugins.AddRange(ctors.Select(c => c.Invoke(null)) 34 | .Cast()); 35 | } 36 | 37 | public void Setup() 38 | { 39 | GetAllPlugins(AppDomain.CurrentDomain); 40 | _plugins.ForEach(p => p.Setup()); 41 | } 42 | 43 | public void DoWork() 44 | { 45 | _plugins.ForEach(p => p.DoWork()); 46 | } 47 | 48 | public void Teardown() 49 | { 50 | _plugins.ForEach(p => p.Teardown()); 51 | } 52 | 53 | public void Load(string name) 54 | { 55 | Assembly.Load(name); 56 | } 57 | 58 | public IPlugin ShowCrossDomainPollutionExceptions() 59 | { 60 | return _plugins.First(); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /PluginDemo.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 10.00 3 | # Visual Studio 2008 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginDemo.Common", "PluginDemo.Common\PluginDemo.Common.csproj", "{2DC4117F-CDBF-47D4-8F43-1771E57C3476}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPluginOne", "TestPluginOne\TestPluginOne.csproj", "{343C0D11-BDE1-4FE5-9BFD-3A3DAE3DAE0A}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginDemo.Host", "PluginDemo.Host\PluginDemo.Host.csproj", "{31BB4879-9F60-4418-8320-51AA171AFA1C}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {2DC4117F-CDBF-47D4-8F43-1771E57C3476}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {2DC4117F-CDBF-47D4-8F43-1771E57C3476}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {2DC4117F-CDBF-47D4-8F43-1771E57C3476}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {2DC4117F-CDBF-47D4-8F43-1771E57C3476}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {343C0D11-BDE1-4FE5-9BFD-3A3DAE3DAE0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {343C0D11-BDE1-4FE5-9BFD-3A3DAE3DAE0A}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {343C0D11-BDE1-4FE5-9BFD-3A3DAE3DAE0A}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {343C0D11-BDE1-4FE5-9BFD-3A3DAE3DAE0A}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {31BB4879-9F60-4418-8320-51AA171AFA1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {31BB4879-9F60-4418-8320-51AA171AFA1C}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {31BB4879-9F60-4418-8320-51AA171AFA1C}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {31BB4879-9F60-4418-8320-51AA171AFA1C}.Release|Any CPU.Build.0 = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(SolutionProperties) = preSolution 30 | HideSolutionNode = FALSE 31 | EndGlobalSection 32 | EndGlobal 33 | -------------------------------------------------------------------------------- /TestPluginOne/PluginOne.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PluginDemo.Common; 3 | 4 | namespace TestPluginOne 5 | { 6 | public class PluginOne : IPlugin 7 | { 8 | public void Setup() 9 | { 10 | Console.WriteLine("PluginOne.Setup"); 11 | } 12 | 13 | public void DoWork() 14 | { 15 | Console.WriteLine("PluginOne.DoWork"); 16 | } 17 | 18 | public void Teardown() 19 | { 20 | Console.WriteLine("PluginOne.Teardown"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /TestPluginOne/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("TestPlugin")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("TestPlugin")] 13 | [assembly: AssemblyCopyright("Copyright © 2012")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("b7a839ef-f8f9-44d2-9b5f-e92b3c96cf2c")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /TestPluginOne/TestPluginOne.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | 9.0.30729 7 | 2.0 8 | {343C0D11-BDE1-4FE5-9BFD-3A3DAE3DAE0A} 9 | Library 10 | Properties 11 | TestPluginOne 12 | TestPluginOne 13 | v3.5 14 | 512 15 | 16 | 17 | true 18 | full 19 | false 20 | ..\bin\plugins\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | ..\bin\plugins\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 3.5 37 | 38 | 39 | 3.5 40 | 41 | 42 | 3.5 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {2DC4117F-CDBF-47D4-8F43-1771E57C3476} 54 | PluginDemo.Common 55 | False 56 | 57 | 58 | 59 | 66 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | This is mostly in response to this [StackOverflow Question][2], and partly because I always forget some of the details on using Shadow Copy. 3 | 4 | Obviously this would need a lot more error checking etc, but should be a good starting point. 5 | 6 | If you are writing a plugin framework for your application, I recommend looking into an existing framework for it such as MEF, rather than rolling your own. 7 | 8 | PluginDemo.Common 9 | --- 10 | 11 | This contains all your common code for plugins - pretty much what you would distribute as your app's SDK. 12 | In this case, we just have one interface called IPlugin, which we use for finding our plugin's types. 13 | 14 | TestPluginOne 15 | --- 16 | 17 | This is your plugin - note the reference to PluginDemo.Common is `CopyLocal = false` - we don't need a copy of it as we have our own version in the host. 18 | 19 | PluginDemo.Host 20 | --- 21 | 22 | This is your main application - note it only has a reference to PluginDemo.Common, not to TestPluginOne. 23 | Points of interest here are: 24 | * Scanner: 25 | - This must inherit from `MarshalByRefObject`, as it will be marshalled across AppDomains (our application's domain, and the ones we create for our plugins.) 26 | - You must not polute your main AppDomain with types from the plugins - if a method in `Scanner` returns a `Type` that is not available in your AppDomain, you will get an `FileNotFoundException`, with the name of the assembly which the type came from. This is what `Scanner.ShowCrossDomainPolutionExceptions` demonstrates. 27 | - If you are going to have plugins which live for a long time (greater than 5mins, if I remember correctly), then change the `Scanner` to inherit from the classes in this question on [StackOverflow][1], as otherwise you will run into `System.Runtime.Remoting.RemotingException: Object [...] has been disconnected or does not exist at the server.` exceptions. 28 | 29 | * AssemblyManager: 30 | - To prevent file locking on our plugins we use *Shadow Copying*. 31 | - This will currently leave debris in your temp folder, so don't forget to clear your AppDomain's `CachePath` after unloading it. You can also set your own `CachePath` to your own location if you prefer. 32 | - To allow us to communicate across AppDomains, we create an instance of `Scanner` using `AppDomain.CreateInstanceAndUnwrap`. This creates an instance of the `Scanner` class in the new AppDomain, and returns a proxy to it, which is stored in the current AppDomain (in the `_scanner` field in this case.) You can see this by setting a break point on the line and inspecting _scanner (it will be a `__TransparentProxy` type.) 33 | 34 | * Program: 35 | - Uses the `AssemblyManager` to load `TestPluginOne`. 36 | - Calls a method on a type in `TestPluginOne`. 37 | - Deletes the `TestPluginOne` loaded from. 38 | - Calls the same method in `TestPluginOne` (showing that we are not using the local copy of the dll). 39 | - Calls the Cross Domain Pollution test method. 40 | 41 | [1]: http://stackoverflow.com/questions/2410221/appdomain-and-marshalbyrefobject-life-time-how-to-avoid-remotingexception 42 | [2]: http://stackoverflow.com/questions/1031431/system-reflection-assembly-loadfile-locks-file/1031449#1031449 --------------------------------------------------------------------------------