The PluginLoader API uses the term "shared types". This document explains what it means.
PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll",
sharedTypes: new [] { typeof(ILogger) });
// versus
PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll",
config => config.PreferSharedTypes = true);
First, a quick overview of essential concepts.
Type identity what makes a class/struct/enum unique. It is defined by the combination of type name (which includes its namespace), assembly name, assembly public key token, and assembly version. You can inspect a type's identity in .NET by looking at System.Type.AssemblyQualifiedName.
For example,
typeof(ILogger).AssemblyQualifiedName
=> "Microsoft.Extensions.Logging.ILogger, Microsoft.Extensions.Logging.Abstractions, Version=2.2.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60"
Element | Value |
---|---|
Type name | Microsoft.Extensions.Logging.ILogger |
Assembly name | Microsoft.Extensions.Logging.Abstractions |
Assembly version | 2.2.0.0 |
Assembly public key token | adb9793829ddae60 |
Simplification: we're going to ignore the "Culture" part of the fully qualified name for now.
The diamond dependency problem is when a library A depends on libraries B and C, both B and C depend on library D, but B requires version D.1 and C requires version D.2.
You could solve this problem by
- Choosing D.1
- Choosing D.2
- Choosing both
Type unification is .NET's solution for the diamond dependency problem. In the simple example above, .NET's build system picks the higher version (D.2) and writes this into the application manifest (the .deps.json or .config file in build output.) Then, when the application is running and encounters usages of D.1, .NET binds the usage to D.2 instead.
In other words, .NET will ignore assembly version when evaluating type identity.
- Type name
- Assembly name
Assembly versionthis part gets ignored- Assembly public key token
Why is this done? It allows type exchange so code can share instances of types even if the code was originally compiled with different dependency versions.
var instanceOfD = new D(); // D.2
new B().DoSomethingWith(instanceOfD); // Library B was compiled to expect D.1, but type unification makes it work with D.2
new C().DoSomethingWith(instanceOfD);
There are two common problems with type unification.
- Breaking changes: what if library B depends on a behavior of D.1 that changed in D.2 and breaks B? Conversely, what library C uses a new API added in D.2, but we force the app to use D.1 instead?
- Static vs dynamic: what if my app has a plugin system with dynamic dependencies?
By default, this PluginLoader
does not unify any types. This means you can have multiple versions of the same assembly
loaded in separate plugins.
PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll")
This can make working with a plugin difficult because it breaks type exchange, so the sharedTypes
list API
is provided to allow you to select which types you want to make sure are unified between the plugin and the
application loading the plugin (aka the host).
PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll",
sharedTypes: new [] { typeof(ILogger) });
Finally, you can invert the default completely to always attempt to unify by setting PreferSharedTypes
. In this mode,
the assembly version provided by the host uses is always used.
PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll",
config => config.PreferSharedTypes = true);
// In older versions of the library, this API was found on PluginLoaderOptions
PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll",
PluginLoaderOptions.PreferSharedTypes);