diff --git a/src/Plugins/Loader/AssemblyLoadContextBuilder.cs b/src/Plugins/Loader/AssemblyLoadContextBuilder.cs
index 03329bd..c7dd8e0 100644
--- a/src/Plugins/Loader/AssemblyLoadContextBuilder.cs
+++ b/src/Plugins/Loader/AssemblyLoadContextBuilder.cs
@@ -26,6 +26,7 @@ public class AssemblyLoadContextBuilder
private AssemblyLoadContext _defaultLoadContext = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
private string? _mainAssemblyPath;
private bool _preferDefaultLoadContext;
+ private bool _lazyLoadReferences;
#if FEATURE_UNLOAD
private bool _isCollectible;
@@ -63,6 +64,7 @@ public AssemblyLoadContext Build()
resourceProbingPaths,
_defaultLoadContext,
_preferDefaultLoadContext,
+ _lazyLoadReferences,
#if FEATURE_UNLOAD
_isCollectible,
_loadInMemory,
@@ -148,21 +150,46 @@ public AssemblyLoadContextBuilder PreferLoadContextAssembly(AssemblyName assembl
/// The builder.
public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName)
{
- if (assemblyName.Name == null || _defaultAssemblies.Contains(assemblyName.Name))
+ // Lazy loaded references have dependencies resolved as they are loaded inside the actual Load Context.
+ if (_lazyLoadReferences)
{
- // base cases
+ if (assemblyName.Name != null && !_defaultAssemblies.Contains(assemblyName.Name))
+ {
+ _defaultAssemblies.Add(assemblyName.Name);
+ var assembly = _defaultLoadContext.LoadFromAssemblyName(assemblyName);
+ foreach (var reference in assembly.GetReferencedAssemblies())
+ {
+ if (reference.Name != null)
+ {
+ _defaultAssemblies.Add(reference.Name);
+ }
+ }
+ }
+
return this;
}
- _defaultAssemblies.Add(assemblyName.Name);
-
- // Recursively load and find all dependencies of default assemblies.
- // This sacrifices some performance for determinism in how transitive
- // dependencies will be shared between host and plugin.
- var assembly = _defaultLoadContext.LoadFromAssemblyName(assemblyName);
- foreach (var reference in assembly.GetReferencedAssemblies())
+ var names = new Queue();
+ names.Enqueue(assemblyName);
+ while (names.TryDequeue(out var name))
{
- PreferDefaultLoadContextAssembly(reference);
+ if (name.Name == null || _defaultAssemblies.Contains(name.Name))
+ {
+ // base cases
+ continue;
+ }
+
+ _defaultAssemblies.Add(name.Name);
+
+ // Load and find all dependencies of default assemblies.
+ // This sacrifices some performance for determinism in how transitive
+ // dependencies will be shared between host and plugin.
+ var assembly = _defaultLoadContext.LoadFromAssemblyName(name);
+
+ foreach (var reference in assembly.GetReferencedAssemblies())
+ {
+ names.Enqueue(reference);
+ }
}
return this;
@@ -187,6 +214,24 @@ public AssemblyLoadContextBuilder PreferDefaultLoadContext(bool preferDefaultLoa
return this;
}
+ ///
+ /// Instructs the load context to lazy load dependencies of all shared assemblies.
+ /// Reduces plugin load time at the expense of non-determinism in how transitive dependencies are loaded
+ /// between the plugin and the host.
+ ///
+ /// Please be aware of the danger of using this option:
+ ///
+ /// https://github.com/natemcmaster/DotNetCorePlugins/pull/164#issuecomment-751557873
+ ///
+ ///
+ /// True to lazy load, else false.
+ /// The builder.
+ public AssemblyLoadContextBuilder IsLazyLoaded(bool isLazyLoaded)
+ {
+ _lazyLoadReferences = isLazyLoaded;
+ return this;
+ }
+
///
/// Add a managed library to the load context.
///
diff --git a/src/Plugins/Loader/ManagedLoadContext.cs b/src/Plugins/Loader/ManagedLoadContext.cs
index 1c5185f..d4acd46 100644
--- a/src/Plugins/Loader/ManagedLoadContext.cs
+++ b/src/Plugins/Loader/ManagedLoadContext.cs
@@ -24,11 +24,12 @@ internal class ManagedLoadContext : AssemblyLoadContext
private readonly IReadOnlyDictionary _managedAssemblies;
private readonly IReadOnlyDictionary _nativeLibraries;
private readonly IReadOnlyCollection _privateAssemblies;
- private readonly IReadOnlyCollection _defaultAssemblies;
+ private readonly ICollection _defaultAssemblies;
private readonly IReadOnlyCollection _additionalProbingPaths;
private readonly bool _preferDefaultLoadContext;
private readonly string[] _resourceRoots;
private readonly bool _loadInMemory;
+ private readonly bool _lazyLoadReferences;
private readonly AssemblyLoadContext _defaultLoadContext;
#if FEATURE_NATIVE_RESOLVER
private readonly AssemblyDependencyResolver _dependencyResolver;
@@ -45,6 +46,7 @@ internal class ManagedLoadContext : AssemblyLoadContext
IReadOnlyCollection resourceProbingPaths,
AssemblyLoadContext defaultLoadContext,
bool preferDefaultLoadContext,
+ bool lazyLoadReferences,
bool isCollectible,
bool loadInMemory,
bool shadowCopyNativeLibraries)
@@ -64,12 +66,13 @@ internal class ManagedLoadContext : AssemblyLoadContext
_basePath = Path.GetDirectoryName(mainAssemblyPath) ?? throw new ArgumentException(nameof(mainAssemblyPath));
_managedAssemblies = managedAssemblies ?? throw new ArgumentNullException(nameof(managedAssemblies));
_privateAssemblies = privateAssemblies ?? throw new ArgumentNullException(nameof(privateAssemblies));
- _defaultAssemblies = defaultAssemblies ?? throw new ArgumentNullException(nameof(defaultAssemblies));
+ _defaultAssemblies = defaultAssemblies != null ? defaultAssemblies.ToList() : throw new ArgumentNullException(nameof(defaultAssemblies));
_nativeLibraries = nativeLibraries ?? throw new ArgumentNullException(nameof(nativeLibraries));
_additionalProbingPaths = additionalProbingPaths ?? throw new ArgumentNullException(nameof(additionalProbingPaths));
_defaultLoadContext = defaultLoadContext;
_preferDefaultLoadContext = preferDefaultLoadContext;
_loadInMemory = loadInMemory;
+ _lazyLoadReferences = lazyLoadReferences;
_resourceRoots = new[] { _basePath }
.Concat(resourceProbingPaths)
@@ -105,6 +108,19 @@ internal class ManagedLoadContext : AssemblyLoadContext
var defaultAssembly = _defaultLoadContext.LoadFromAssemblyName(assemblyName);
if (defaultAssembly != null)
{
+ // Add referenced assemblies to the list of default assemblies.
+ // This is basically lazy loading
+ if (_lazyLoadReferences)
+ {
+ foreach (var reference in defaultAssembly.GetReferencedAssemblies())
+ {
+ if (reference.Name != null && !_defaultAssemblies.Contains(reference.Name))
+ {
+ _defaultAssemblies.Add(reference.Name);
+ }
+ }
+ }
+
// Older versions used to return null here such that returned assembly would be resolved from the default ALC.
// However, with the addition of custom default ALCs, the Default ALC may not be the user's chosen ALC when
// this context was built. As such, we simply return the Assembly from the user's chosen default load context.
diff --git a/src/Plugins/PluginConfig.cs b/src/Plugins/PluginConfig.cs
index 8fc0737..a7cf617 100644
--- a/src/Plugins/PluginConfig.cs
+++ b/src/Plugins/PluginConfig.cs
@@ -62,10 +62,22 @@ public PluginConfig(string mainAssemblyPath)
///
public bool PreferSharedTypes { get; set; }
- ///
+ ///
+ /// If enabled, will lazy load dependencies of all shared assemblies.
+ /// Reduces plugin load time at the expense of non-determinism in how transitive dependencies are loaded
+ /// between the plugin and the host.
+ ///
+ /// Please be aware of the danger of using this option:
+ ///
+ /// https://github.com/natemcmaster/DotNetCorePlugins/pull/164#issuecomment-751557873
+ ///
+ ///
+ public bool IsLazyLoaded { get; set; } = false;
+
+ ///
/// If set, replaces the default used by the .
/// Use this feature if the of the is not the Runtime's default load context.
- /// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) !=
+ /// i.e. (AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly) !=
///
public AssemblyLoadContext DefaultContext { get; set; } = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()) ?? AssemblyLoadContext.Default;
@@ -104,9 +116,9 @@ public bool LoadInMemory
///
public bool EnableHotReload { get; set; }
- ///
+ ///
/// Specifies the delay to reload a plugin, after file changes have been detected.
- /// Default value is 200 milliseconds.
+ /// Default value is 200 milliseconds.
///
public TimeSpan ReloadDelay { get; set; } = TimeSpan.FromMilliseconds(200);
#endif
diff --git a/src/Plugins/PluginLoader.cs b/src/Plugins/PluginLoader.cs
index 7b67508..ee2d8a4 100644
--- a/src/Plugins/PluginLoader.cs
+++ b/src/Plugins/PluginLoader.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
@@ -112,9 +113,15 @@ public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Type[] sh
{
if (sharedTypes != null)
{
+ var uniqueAssemblies = new HashSet();
foreach (var type in sharedTypes)
{
- config.SharedAssemblies.Add(type.Assembly.GetName());
+ uniqueAssemblies.Add(type.Assembly);
+ }
+
+ foreach (var assembly in uniqueAssemblies)
+ {
+ config.SharedAssemblies.Add(assembly.GetName());
}
}
configure(config);
@@ -374,6 +381,7 @@ private static AssemblyLoadContextBuilder CreateLoadContextBuilder(PluginConfig
}
#endif
+ builder.IsLazyLoaded(config.IsLazyLoaded);
foreach (var assemblyName in config.SharedAssemblies)
{
builder.PreferDefaultLoadContextAssembly(assemblyName);
diff --git a/src/Plugins/PublicAPI.Unshipped.txt b/src/Plugins/PublicAPI.Unshipped.txt
index e69de29..0707ad6 100644
--- a/src/Plugins/PublicAPI.Unshipped.txt
+++ b/src/Plugins/PublicAPI.Unshipped.txt
@@ -0,0 +1,3 @@
+McMaster.NETCore.Plugins.PluginConfig.IsLazyLoaded.get -> bool
+McMaster.NETCore.Plugins.PluginConfig.IsLazyLoaded.set -> void
+McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder.IsLazyLoaded(bool isLazyLoaded) -> McMaster.NETCore.Plugins.Loader.AssemblyLoadContextBuilder!
diff --git a/src/Plugins/releasenotes.props b/src/Plugins/releasenotes.props
index c3b7385..e772487 100644
--- a/src/Plugins/releasenotes.props
+++ b/src/Plugins/releasenotes.props
@@ -2,7 +2,8 @@
Changes:
-* @Sewer56 - search in additional probing paths (PR #172)
+* @Sewer56 - feature: add option to support lazy loading of transitive dependencies to increase performance (PR #164)
+* @Sewer56 - bugfix: search in additional probing paths (PR #172)
Changes:
diff --git a/test/Plugins.Tests/SharedTypesTests.cs b/test/Plugins.Tests/SharedTypesTests.cs
index 0e717ae..f03f42c 100644
--- a/test/Plugins.Tests/SharedTypesTests.cs
+++ b/test/Plugins.Tests/SharedTypesTests.cs
@@ -42,13 +42,12 @@ public void PluginsCanForceSharedTypes()
/// accounted for. Without this, the order in which code loads
/// could cause different assembly versions to be loaded.
///
- [Fact]
- public void TransitiveAssembliesOfSharedTypesAreResolved()
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void TransitiveAssembliesOfSharedTypesAreResolved(bool isLazyLoaded)
{
- using var loader = PluginLoader.CreateFromAssemblyFile(
- TestResources.GetTestProjectAssembly("TransitivePlugin"),
- sharedTypes: new[] { typeof(SharedType) });
-
+ using var loader = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("TransitivePlugin"), sharedTypes: new[] { typeof(SharedType) }, config => config.IsLazyLoaded = isLazyLoaded);
var assembly = loader.LoadDefaultAssembly();
var configType = assembly.GetType("TransitivePlugin.PluginConfig", throwOnError: true)!;
var config = Activator.CreateInstance(configType);