├── .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
--------------------------------------------------------------------------------