Skip to content

Commit fa22501

Browse files
committed
fix: resolve assembly identity conflict in GenerateMSBuildAssets task
Use AssemblyLoadContext on modern .NET to create isolated load context that forces both the task and user assembly to load JD.MSBuild.Fluent from the task directory. This ensures type identity matches even when the user project references the same package. On .NET Framework 4.7.2, continue using AppDomain.AssemblyResolve as AssemblyLoadContext is not available. This resolves the type identity mismatch that prevented packages from using auto-generation when they reference JD.MSBuild.Fluent directly.
1 parent 4ce6013 commit fa22501

1 file changed

Lines changed: 82 additions & 21 deletions

File tree

src/JD.MSBuild.Fluent.Tasks/GenerateMSBuildAssets.cs

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
using System.IO;
33
using System.Linq;
44
using System.Reflection;
5+
#if !NET472
6+
using System.Runtime.Loader;
7+
#endif
58
using Microsoft.Build.Framework;
69
using Microsoft.Build.Utilities;
710
using JD.MSBuild.Fluent;
@@ -45,17 +48,50 @@ public class GenerateMSBuildAssets : Task
4548
[Output]
4649
public ITaskItem[] GeneratedFiles { get; set; } = Array.Empty<ITaskItem>();
4750

51+
#if !NET472
52+
private class DefinitionFactoryContext : AssemblyLoadContext
53+
{
54+
private readonly AssemblyDependencyResolver _resolver;
55+
private readonly string _taskDirectory;
56+
57+
public DefinitionFactoryContext(string assemblyPath, string taskDirectory)
58+
: base(isCollectible: true)
59+
{
60+
_resolver = new AssemblyDependencyResolver(assemblyPath);
61+
_taskDirectory = taskDirectory;
62+
}
63+
64+
protected override Assembly? Load(AssemblyName assemblyName)
65+
{
66+
// CRITICAL: Load JD.MSBuild.Fluent from the task directory to ensure type identity
67+
if (assemblyName.Name == "JD.MSBuild.Fluent")
68+
{
69+
var fluentPath = Path.Combine(_taskDirectory, "JD.MSBuild.Fluent.dll");
70+
if (File.Exists(fluentPath))
71+
return LoadFromAssemblyPath(fluentPath);
72+
}
73+
74+
// Use the dependency resolver for other assemblies
75+
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
76+
if (assemblyPath != null)
77+
return LoadFromAssemblyPath(assemblyPath);
78+
79+
// Fall back to default context for framework assemblies
80+
return null;
81+
}
82+
}
83+
#else
4884
static GenerateMSBuildAssets()
4985
{
50-
// Register assembly resolver to find JD.MSBuild.Fluent.dll in the same directory as the task DLL
86+
// For .NET Framework, register assembly resolver
5187
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
5288
}
5389

5490
private static Assembly? OnAssemblyResolve(object? sender, ResolveEventArgs args)
5591
{
5692
var assemblyName = new AssemblyName(args.Name);
5793

58-
// Look in the same directory as the task DLL for ANY assembly
94+
// Look in the same directory as the task DLL for assemblies
5995
var taskAssemblyLocation = typeof(GenerateMSBuildAssets).Assembly.Location;
6096
var taskDirectory = Path.GetDirectoryName(taskAssemblyLocation);
6197
if (string.IsNullOrEmpty(taskDirectory))
@@ -64,6 +100,7 @@ static GenerateMSBuildAssets()
64100
var assemblyPath = Path.Combine(taskDirectory, assemblyName.Name + ".dll");
65101
return File.Exists(assemblyPath) ? Assembly.LoadFrom(assemblyPath) : null;
66102
}
103+
#endif
67104

68105
public override bool Execute()
69106
{
@@ -111,25 +148,42 @@ public override bool Execute()
111148

112149
private PackageDefinition? LoadDefinitionFromFactory()
113150
{
151+
#if !NET472
152+
DefinitionFactoryContext? context = null;
153+
#endif
154+
114155
try
115156
{
116-
// CRITICAL: Pre-load JD.MSBuild.Fluent.dll from the task directory FIRST
117-
// This ensures the user assembly uses OUR copy, not a potentially different one from their bin folder
157+
// Get task directory for loading JD.MSBuild.Fluent
118158
var taskAssemblyLocation = typeof(GenerateMSBuildAssets).Assembly.Location;
119159
var taskDirectory = Path.GetDirectoryName(taskAssemblyLocation);
120-
if (!string.IsNullOrEmpty(taskDirectory))
160+
if (string.IsNullOrEmpty(taskDirectory))
121161
{
122-
var fluentAssemblyPath = Path.Combine(taskDirectory, "JD.MSBuild.Fluent.dll");
123-
if (File.Exists(fluentAssemblyPath))
124-
{
125-
// Force load this assembly into the default context BEFORE loading user assembly
126-
var fluentAsm = Assembly.LoadFrom(fluentAssemblyPath);
127-
Log.LogMessage(MessageImportance.Low, $"Pre-loaded JD.MSBuild.Fluent from: {fluentAsm.Location}");
128-
}
162+
Log.LogError("Could not determine task assembly directory");
163+
return null;
129164
}
165+
166+
#if !NET472
167+
// Create isolated load context that forces JD.MSBuild.Fluent to load from task directory
168+
context = new DefinitionFactoryContext(AssemblyFile, taskDirectory);
130169

131-
// Now load user assembly - it should resolve JD.MSBuild.Fluent to the one we just loaded
170+
// Load user assembly in the isolated context
171+
var assembly = context.LoadFromAssemblyPath(AssemblyFile);
172+
#else
173+
// For .NET Framework, pre-load JD.MSBuild.Fluent from task directory
174+
var fluentAssemblyPath = Path.Combine(taskDirectory, "JD.MSBuild.Fluent.dll");
175+
if (File.Exists(fluentAssemblyPath))
176+
{
177+
var fluentAsm = Assembly.LoadFrom(fluentAssemblyPath);
178+
Log.LogMessage(MessageImportance.Low, $"Pre-loaded JD.MSBuild.Fluent from: {fluentAsm.Location}");
179+
}
180+
181+
// Load user assembly
132182
var assembly = Assembly.LoadFrom(AssemblyFile);
183+
#endif
184+
185+
Log.LogMessage(MessageImportance.Low,
186+
$"Loaded user assembly: {assembly.FullName} from {assembly.Location}");
133187

134188
// Find type
135189
var type = assembly.GetType(FactoryType);
@@ -170,18 +224,18 @@ public override bool Execute()
170224
return null;
171225
}
172226

173-
// The result is a PackageDefinition from the user's assembly context
174-
// We need to work with it via reflection since it's a different type identity
227+
// Try to cast to PackageDefinition
175228
var packageDef = result as PackageDefinition;
176229
if (packageDef == null)
177230
{
178-
// Log detailed type information for debugging
231+
// Type identity mismatch - keep diagnostics for debugging
179232
var resultType = result.GetType();
180233
var expectedType = typeof(PackageDefinition);
181-
Log.LogError($"Factory method returned type {resultType.FullName} from assembly {resultType.Assembly.FullName}");
182-
Log.LogError($"Expected type {expectedType.FullName} from assembly {expectedType.Assembly.FullName}");
183-
Log.LogError($"Types equal: {resultType == expectedType}");
184-
Log.LogError($"Assemblies equal: {resultType.Assembly == expectedType.Assembly}");
234+
Log.LogError($"Type identity mismatch!");
235+
Log.LogError($" Returned: {resultType.FullName} from {resultType.Assembly.Location}");
236+
Log.LogError($" Expected: {expectedType.FullName} from {expectedType.Assembly.Location}");
237+
Log.LogError($" Types equal: {resultType == expectedType}");
238+
Log.LogError($" Assemblies equal: {resultType.Assembly.FullName == expectedType.Assembly.FullName}");
185239
return null;
186240
}
187241

@@ -202,8 +256,15 @@ public override bool Execute()
202256
}
203257
catch (Exception ex)
204258
{
205-
Log.LogErrorFromException(ex);
259+
Log.LogErrorFromException(ex, showStackTrace: true);
206260
return null;
207261
}
262+
finally
263+
{
264+
#if !NET472
265+
// Don't unload the context yet - we still need the PackageDefinition object
266+
// MSBuild will clean up after the task completes
267+
#endif
268+
}
208269
}
209270
}

0 commit comments

Comments
 (0)