Skip to content

Latest commit

 

History

History
112 lines (79 loc) · 4.22 KB

what-are-shared-types.md

File metadata and controls

112 lines (79 loc) · 4.22 KB

An Explanation of Shared Types

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);

Concepts

First, a quick overview of essential concepts.

Type identity

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.

Diamond dependencies

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

  1. Choosing D.1
  2. Choosing D.2
  3. Choosing both

Type unification (option 2)

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 version this 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); 

But what if...

There are two common problems with type unification.

  1. 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?
  2. Static vs dynamic: what if my app has a plugin system with dynamic dependencies?

This library's answer...

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);