Skip to content

Commit 182e933

Browse files
Support loading plugin dependencies from .deps.json on .NET Framework and Visual Studio MSBuild (#411)
* Support loading plugin dependencies from .deps.json on .NET Framework and Visual Studio MSBuild * Update NUnitRetry * Fix loading plugin assemblies from deps.json on linux (case sensitive) * refactor resolvers and introduce base class * Move external plugin tests to ExternalPluginsTest * Add external test for SpecSync.AzureDevOps.TestSuiteBasedExecution.Reqnroll plugin --------- Co-authored-by: Gáspár Nagy <gaspar.nagy@gmail.com>
1 parent f59ced6 commit 182e933

28 files changed

+379
-106
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* MsTest: Only use TestContext for output and not Console.WriteLine (#368)
1313

1414
* Fix: Replace deprecated dependency `Specflow.Internal.Json` with `System.Text.Json`. The dependency was used for laoding `reqnroll.json`, for Visual Studio integration and for telemetry. (#373)
15+
* Fix: Support loading plugin dependencies from .deps.json on .NET Framework and Visual Studio MSBuild (#408)
1516

1617
*Contributors of this release (in alphabetical order):* @clrudolphi, @obligaron, @olegKoshmeliuk
1718

Reqnroll/PlatformCompatibility/PlatformHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ public static void RegisterPluginAssemblyLoader(IObjectContainer container)
99
if (PlatformInformation.IsDotNetFramework)
1010
container.RegisterTypeAs<DotNetFrameworkPluginAssemblyLoader, IPluginAssemblyLoader>();
1111
else
12-
container.RegisterTypeAs<PluginAssemblyLoader, IPluginAssemblyLoader>();
12+
container.RegisterTypeAs<DotNetCorePluginAssemblyLoader, IPluginAssemblyLoader>();
1313
}
1414
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Reflection;
6+
using Microsoft.Extensions.DependencyModel;
7+
using Microsoft.Extensions.DependencyModel.Resolution;
8+
9+
namespace Reqnroll.Plugins;
10+
11+
public abstract class AssemblyResolverBase
12+
{
13+
private readonly Lazy<Assembly> _assembly;
14+
15+
public Assembly GetAssembly() => _assembly.Value;
16+
17+
private ICompilationAssemblyResolver _assemblyResolver;
18+
private DependencyContext _dependencyContext;
19+
20+
protected AssemblyResolverBase(string relativePath)
21+
{
22+
var path = Path.GetFullPath(relativePath);
23+
_assembly = new Lazy<Assembly>(() => Initialize(path));
24+
}
25+
26+
protected abstract Assembly Initialize(string path);
27+
28+
protected void SetupDependencyContext(string path, Assembly assembly, bool throwOnError)
29+
{
30+
try
31+
{
32+
_dependencyContext = DependencyContext.Load(assembly);
33+
34+
if (_dependencyContext is null) return;
35+
36+
_assemblyResolver = new CompositeCompilationAssemblyResolver(
37+
[
38+
new AppBaseCompilationAssemblyResolver(Path.GetDirectoryName(path)!),
39+
new ReferenceAssemblyPathResolver(),
40+
new PackageCompilationAssemblyResolver()
41+
]);
42+
}
43+
catch (Exception)
44+
{
45+
if (throwOnError)
46+
throw;
47+
48+
// We ignore if there was a problem with initializing context from .deps.json
49+
}
50+
}
51+
52+
protected abstract Assembly LoadAssemblyFromPath(string assemblyPath);
53+
54+
protected Assembly TryResolveAssembly(AssemblyName name)
55+
{
56+
var library = _dependencyContext?.RuntimeLibraries.FirstOrDefault(
57+
runtimeLibrary => string.Equals(runtimeLibrary.Name, name.Name, StringComparison.OrdinalIgnoreCase));
58+
59+
if (library == null)
60+
return null;
61+
62+
var wrapper = new CompilationLibrary(
63+
library.Type,
64+
library.Name,
65+
library.Version,
66+
library.Hash,
67+
library.RuntimeAssemblyGroups.SelectMany(g => g.AssetPaths),
68+
library.Dependencies,
69+
library.Serviceable,
70+
library.Path,
71+
library.HashPath);
72+
73+
var assemblies = new List<string>();
74+
_assemblyResolver.TryResolveAssemblyPaths(wrapper, assemblies);
75+
76+
if (_assemblyResolver.TryResolveAssemblyPaths(wrapper, assemblies) && assemblies.Count > 0)
77+
{
78+
foreach (var assemblyPath in assemblies)
79+
{
80+
try
81+
{
82+
return LoadAssemblyFromPath(assemblyPath);
83+
}
84+
catch
85+
{
86+
// Don't throw if we can't load the specified assembly (perhaps something is missing or misconfigured)
87+
}
88+
}
89+
}
90+
91+
return null;
92+
}
93+
}

Reqnroll/Plugins/PluginAssemblyLoader.cs renamed to Reqnroll/Plugins/DotNetCorePluginAssemblyLoader.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Reqnroll.Plugins;
55
/// <summary>
66
/// This class is used for .NET Core based frameworks (.NET 6+) only. For .NET Framework <see cref="DotNetFrameworkPluginAssemblyLoader"/> is used instead. See <see cref="PlatformCompatibility.PlatformHelper"/>.
77
/// </summary>
8-
public class PluginAssemblyLoader : IPluginAssemblyLoader
8+
public class DotNetCorePluginAssemblyLoader : IPluginAssemblyLoader
99
{
10-
public Assembly LoadAssembly(string assemblyName) => PluginAssemblyResolver.Load(assemblyName);
10+
public Assembly LoadAssembly(string assemblyPath) => DotNetCorePluginAssemblyResolver.Load(assemblyPath);
1111
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System.Reflection;
2+
using System.Runtime.Loader;
3+
4+
namespace Reqnroll.Plugins;
5+
6+
/// <summary>
7+
/// This class is used for .NET Core based frameworks (.NET 6+) only. See <see cref="PlatformCompatibility.PlatformHelper"/>.
8+
/// </summary>
9+
public sealed class DotNetCorePluginAssemblyResolver(string path) : AssemblyResolverBase(path)
10+
{
11+
private AssemblyLoadContext _loadContext;
12+
13+
protected override Assembly Initialize(string path)
14+
{
15+
_loadContext = AssemblyLoadContext.GetLoadContext(typeof(DotNetCorePluginAssemblyResolver).Assembly);
16+
var assembly = LoadAssemblyFromPath(path);
17+
18+
SetupDependencyContext(path, assembly, true);
19+
20+
_loadContext.Resolving += OnResolving;
21+
_loadContext.Unloading += OnUnloading;
22+
23+
return assembly;
24+
}
25+
26+
protected override Assembly LoadAssemblyFromPath(string assemblyPath)
27+
=> _loadContext.LoadFromAssemblyPath(assemblyPath);
28+
29+
private void OnUnloading(AssemblyLoadContext context)
30+
{
31+
_loadContext.Resolving -= OnResolving;
32+
_loadContext.Unloading -= OnUnloading;
33+
}
34+
35+
private Assembly OnResolving(AssemblyLoadContext context, AssemblyName name)
36+
{
37+
return TryResolveAssembly(name);
38+
}
39+
40+
public static Assembly Load(string path)
41+
{
42+
return new DotNetCorePluginAssemblyResolver(path).GetAssembly();
43+
}
44+
}

Reqnroll/Plugins/DotNetFrameworkPluginAssemblyLoader.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
namespace Reqnroll.Plugins;
44

5+
/// <summary>
6+
/// This class is used for .NET Framework v4.* only. For .NET +6 <see cref="DotNetCorePluginAssemblyLoader"/> is used instead. See <see cref="PlatformCompatibility.PlatformHelper"/>.
7+
/// </summary>
58
public class DotNetFrameworkPluginAssemblyLoader : IPluginAssemblyLoader
69
{
7-
public Assembly LoadAssembly(string assemblyName) => Assembly.LoadFrom(assemblyName);
10+
public Assembly LoadAssembly(string assemblyPath) => DotNetFrameworkPluginAssemblyResolver.Load(assemblyPath);
811
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using System.Reflection;
3+
4+
namespace Reqnroll.Plugins;
5+
6+
/// <summary>
7+
/// This class is used for .NET Framework 4.* only. See <see cref="PlatformCompatibility.PlatformHelper"/>.
8+
/// </summary>
9+
public sealed class DotNetFrameworkPluginAssemblyResolver(string path) : AssemblyResolverBase(path)
10+
{
11+
protected override Assembly Initialize(string path)
12+
{
13+
var assembly = LoadAssemblyFromPath(path);
14+
15+
SetupDependencyContext(path, assembly, false);
16+
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
17+
18+
return assembly;
19+
}
20+
21+
protected override Assembly LoadAssemblyFromPath(string assemblyPath)
22+
=> Assembly.LoadFrom(assemblyPath);
23+
24+
private Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
25+
{
26+
var assemblyName = new AssemblyName(args.Name);
27+
return TryResolveAssembly(assemblyName);
28+
}
29+
30+
public static Assembly Load(string path)
31+
{
32+
return new DotNetFrameworkPluginAssemblyResolver(path).GetAssembly();
33+
}
34+
}

Reqnroll/Plugins/IPluginAssemblyLoader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
namespace Reqnroll.Plugins;
44
public interface IPluginAssemblyLoader
55
{
6-
Assembly LoadAssembly(string assemblyName);
6+
Assembly LoadAssembly(string assemblyPath);
77
}

Reqnroll/Plugins/PluginAssemblyResolver.cs

Lines changed: 0 additions & 80 deletions
This file was deleted.

Reqnroll/Plugins/RuntimePluginLoader.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ namespace Reqnroll.Plugins
77
{
88
public class RuntimePluginLoader(IPluginAssemblyLoader _pluginAssemblyLoader) : IRuntimePluginLoader
99
{
10-
public IRuntimePlugin LoadPlugin(string pluginAssemblyName, ITraceListener traceListener, bool traceMissingPluginAttribute)
10+
public IRuntimePlugin LoadPlugin(string pluginAssemblyPath, ITraceListener traceListener, bool traceMissingPluginAttribute)
1111
{
1212
Assembly assembly;
1313
try
1414
{
15-
assembly = _pluginAssemblyLoader.LoadAssembly(pluginAssemblyName);
15+
assembly = _pluginAssemblyLoader.LoadAssembly(pluginAssemblyPath);
1616
}
1717
catch (Exception ex)
1818
{
19-
throw new ReqnrollException($"Unable to load plugin: {pluginAssemblyName}. Please check https://go.reqnroll.net/doc-plugins for details. (Framework: {PlatformInformation.DotNetFrameworkDescription})", ex);
19+
throw new ReqnrollException($"Unable to load plugin: {pluginAssemblyPath}. Please check https://go.reqnroll.net/doc-plugins for details. (Framework: {PlatformInformation.DotNetFrameworkDescription})", ex);
2020
}
2121

2222
var pluginAttribute = (RuntimePluginAttribute)Attribute.GetCustomAttribute(assembly, typeof(RuntimePluginAttribute));

0 commit comments

Comments
 (0)