Skip to content

Commit 64930d4

Browse files
authored
Move MSBuild initialization from MSBuildTestBase to MSBuildAssemblyResolver (#277)
* Move MSBuild init from MSBuildTestBase to MSBuildAssemblyResolver * Clean up README for locating MSBuild
1 parent 1a0fc0c commit 64930d4

File tree

7 files changed

+69
-30
lines changed

7 files changed

+69
-30
lines changed

README.md

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,39 @@ The above extension method would add the following item:
9191
</MyCustomType>
9292
</ItemGroup>
9393
```
94+
9495
## Resolving MSBuild
95-
This API does not redistribute MSBuild and so it must be located at run-time. There are three ways to do this:
9696

97-
1. If your project is a unit test, have your test class inherit from [MSBuildTestBase](https://github.com/jeffkl/MSBuildProjectCreator/blob/main/src/MSBuildProjectCreator/MSBuildTestBase.cs)
97+
This API does not redistribute MSBuild and so it must be located at run-time. There are two locators to be aware of:
98+
99+
1. `MSBuildAssemblyResolver.Register()` provided in this package
100+
2. `MSBuildLocator.RegisterDefaults()` in the [MSBuildLocator](https://docs.microsoft.com/en-us/visualstudio/msbuild/updating-an-existing-application?view=vs-2019#use-microsoftbuildlocator) package
101+
102+
In either case, you need to register before running any MSBuild operations. There are three main ways to ensure registration:
103+
104+
### 1. Module initializer (preferred)
105+
106+
If you're targeting C# 9.0+ / .NET 5+, use a [module initializer](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/module-initializers).
107+
MSBuild assemblies will be automatically located when your assembly is loaded:
108+
109+
```C#
110+
using System.Runtime.CompilerServices;
111+
112+
internal static class MyModuleInitializer
113+
{
114+
[ModuleInitializer]
115+
internal static void InitializeMSBuild()
116+
{
117+
MSBuildAssemblyResolver.Register();
118+
}
119+
}
120+
```
121+
122+
### 2. Inherit from MSBuildTestBase
123+
124+
If you're unable or don't want to use a module initializer and your project is a unit test, have your test class inherit
125+
from [MSBuildTestBase](https://github.com/jeffkl/MSBuildProjectCreator/blob/main/src/MSBuildProjectCreator/MSBuildTestBase.cs):
126+
98127
```C#
99128
/// <summary>
100129
/// A base class for all unit tests that inherits from MSBuildTestBase.
@@ -111,24 +140,17 @@ public class MyTest : MyTestBase
111140
{
112141
}
113142
```
114-
2. Attach the [assembly resolve event handler](https://docs.microsoft.com/en-us/dotnet/standard/assembly/resolve-loads) to [MSBuildAssemblyResolver.AssemblyResolve](https://github.com/jeffkl/MSBuildProjectCreator/blob/main/src/MSBuildProjectCreator/MSBuildAssemblyResolver.cs)
115143

116-
``` C#
117-
public static class MyApp
118-
{
119-
public static MyApp()
120-
{
121-
AppDomain.CurrentDomain.AssemblyResolve += MSBuildAssemblyResolver.AssemblyResolve;
122-
}
123-
}
124-
```
125-
3. Use [MSBuildLocator](https://docs.microsoft.com/en-us/visualstudio/msbuild/updating-an-existing-application?view=vs-2019#use-microsoftbuildlocator) to register an instance of MSBuild.
144+
### 3. Use a static constructor
145+
146+
Add a static constructor and call the registration API:
147+
126148
```C#
127149
public static class MyApp
128150
{
129151
public static MyApp()
130152
{
131-
MSBuildLocator.RegisterDefaults();
153+
MSBuildAssemblyResolver.Register();
132154
}
133155
}
134156
```

src/Microsoft.Build.Utilities.ProjectCreation/MSBuildAssemblyResolver.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
using System.IO;
88
using System.Linq;
99
using System.Reflection;
10+
#if NET6_0_OR_GREATER
11+
using System.Runtime.Loader;
12+
#endif
1013

1114
namespace Microsoft.Build.Utilities.ProjectCreation
1215
{
@@ -19,6 +22,21 @@ public static partial class MSBuildAssemblyResolver
1922

2023
private static readonly ConcurrentDictionary<string, Lazy<Assembly?>> LoadedAssemblies = new ConcurrentDictionary<string, Lazy<Assembly?>>(StringComparer.OrdinalIgnoreCase);
2124

25+
private static readonly Lazy<object> RegisterLazy = new Lazy<object>(() =>
26+
{
27+
Environment.SetEnvironmentVariable("MSBUILD_EXE_PATH", MSBuildExePath);
28+
Environment.SetEnvironmentVariable("MSBuildExtensionsPath", DotNetSdksPath);
29+
Environment.SetEnvironmentVariable("MSBuildSDKsPath", string.IsNullOrWhiteSpace(DotNetSdksPath) ? null : Path.Combine(DotNetSdksPath, "Sdks"));
30+
31+
#if NET6_0_OR_GREATER
32+
AssemblyLoadContext.Default.Resolving += AssemblyResolve;
33+
#else
34+
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
35+
#endif
36+
37+
return new object();
38+
});
39+
2240
/// <summary>
2341
/// Gets the path to the .NET SDKs.
2442
/// </summary>
@@ -34,6 +52,11 @@ public static partial class MSBuildAssemblyResolver
3452
/// </summary>
3553
public static string? MSBuildExePath => MSBuildDirectoryLazy.Value.MSBuildExePath;
3654

55+
/// <summary>
56+
/// Register to resolve MSBuild related assemblies automatically.
57+
/// </summary>
58+
public static void Register() => _ = RegisterLazy.Value;
59+
3760
private static Assembly? AssemblyResolve(AssemblyName requestedAssemblyName, Func<string, Assembly?> assemblyLoader)
3861
{
3962
if (SearchPaths == null)

src/Microsoft.Build.Utilities.ProjectCreation/MSBuildTestBase.cs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,20 @@
22
//
33
// Licensed under the MIT license.
44

5-
using System;
6-
using System.IO;
7-
#if NET6_0_OR_GREATER
8-
using System.Runtime.Loader;
9-
#endif
10-
115
namespace Microsoft.Build.Utilities.ProjectCreation
126
{
137
/// <summary>
14-
/// Provides a base class for unit test classes that use MSBuild. This class resolves MSBuild related assemblies automatically.
8+
/// Provides a base class for unit test classes that use MSBuild. This class resolves MSBuild related assemblies automatically.
159
/// </summary>
10+
/// <remarks>
11+
/// Prefer calling <see cref="MSBuildAssemblyResolver.Register"/> from a <c>ModuleInitalizerAttribute</c> if your target framework
12+
/// supports one. This base class is provided for backwards compatibility with older versions of .NET.
13+
/// </remarks>
1614
public abstract class MSBuildTestBase
1715
{
1816
static MSBuildTestBase()
1917
{
20-
Environment.SetEnvironmentVariable("MSBUILD_EXE_PATH", MSBuildAssemblyResolver.MSBuildExePath);
21-
Environment.SetEnvironmentVariable("MSBuildExtensionsPath", MSBuildAssemblyResolver.DotNetSdksPath);
22-
Environment.SetEnvironmentVariable("MSBuildSDKsPath", string.IsNullOrWhiteSpace(MSBuildAssemblyResolver.DotNetSdksPath) ? null : Path.Combine(MSBuildAssemblyResolver.DotNetSdksPath, "Sdks"));
23-
24-
#if NET6_0_OR_GREATER
25-
AssemblyLoadContext.Default.Resolving += MSBuildAssemblyResolver.AssemblyResolve;
26-
#else
27-
AppDomain.CurrentDomain.AssemblyResolve += MSBuildAssemblyResolver.AssemblyResolve;
28-
#endif
18+
MSBuildAssemblyResolver.Register();
2919
}
3020
}
3121
}

src/Microsoft.Build.Utilities.ProjectCreation/PublicAPI/net472/PublicAPI.Shipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ static Microsoft.Build.Utilities.ProjectCreation.ExtensionMethods.ToArrayWithSin
293293
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.AssemblyResolve(object? sender, System.ResolveEventArgs! args) -> System.Reflection.Assembly?
294294
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.DotNetSdksPath.get -> string?
295295
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.MSBuildExePath.get -> string?
296+
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.Register() -> void
296297
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.SearchPaths.get -> string![]?
297298
static Microsoft.Build.Utilities.ProjectCreation.PackageFeed.Create(string! rootPath) -> Microsoft.Build.Utilities.ProjectCreation.PackageFeed!
298299
static Microsoft.Build.Utilities.ProjectCreation.PackageFeed.Create(System.IO.DirectoryInfo! rootPath) -> Microsoft.Build.Utilities.ProjectCreation.PackageFeed!

src/Microsoft.Build.Utilities.ProjectCreation/PublicAPI/net6.0/PublicAPI.Shipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ static Microsoft.Build.Utilities.ProjectCreation.ExtensionMethods.ToArrayWithSin
293293
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.AssemblyResolve(System.Runtime.Loader.AssemblyLoadContext! assemblyLoadContext, System.Reflection.AssemblyName! requestedAssemblyName) -> System.Reflection.Assembly?
294294
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.DotNetSdksPath.get -> string?
295295
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.MSBuildExePath.get -> string?
296+
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.Register() -> void
296297
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.SearchPaths.get -> string![]?
297298
static Microsoft.Build.Utilities.ProjectCreation.PackageFeed.Create(string! rootPath) -> Microsoft.Build.Utilities.ProjectCreation.PackageFeed!
298299
static Microsoft.Build.Utilities.ProjectCreation.PackageFeed.Create(System.IO.DirectoryInfo! rootPath) -> Microsoft.Build.Utilities.ProjectCreation.PackageFeed!

src/Microsoft.Build.Utilities.ProjectCreation/PublicAPI/net7.0/PublicAPI.Shipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ static Microsoft.Build.Utilities.ProjectCreation.ExtensionMethods.ToArrayWithSin
293293
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.AssemblyResolve(System.Runtime.Loader.AssemblyLoadContext! assemblyLoadContext, System.Reflection.AssemblyName! requestedAssemblyName) -> System.Reflection.Assembly?
294294
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.DotNetSdksPath.get -> string?
295295
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.MSBuildExePath.get -> string?
296+
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.Register() -> void
296297
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.SearchPaths.get -> string![]?
297298
static Microsoft.Build.Utilities.ProjectCreation.PackageFeed.Create(string! rootPath) -> Microsoft.Build.Utilities.ProjectCreation.PackageFeed!
298299
static Microsoft.Build.Utilities.ProjectCreation.PackageFeed.Create(System.IO.DirectoryInfo! rootPath) -> Microsoft.Build.Utilities.ProjectCreation.PackageFeed!

src/Microsoft.Build.Utilities.ProjectCreation/PublicAPI/net8.0/PublicAPI.Shipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ static Microsoft.Build.Utilities.ProjectCreation.ExtensionMethods.ToArrayWithSin
293293
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.AssemblyResolve(System.Runtime.Loader.AssemblyLoadContext! assemblyLoadContext, System.Reflection.AssemblyName! requestedAssemblyName) -> System.Reflection.Assembly?
294294
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.DotNetSdksPath.get -> string?
295295
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.MSBuildExePath.get -> string?
296+
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.Register() -> void
296297
static Microsoft.Build.Utilities.ProjectCreation.MSBuildAssemblyResolver.SearchPaths.get -> string![]?
297298
static Microsoft.Build.Utilities.ProjectCreation.PackageFeed.Create(string! rootPath) -> Microsoft.Build.Utilities.ProjectCreation.PackageFeed!
298299
static Microsoft.Build.Utilities.ProjectCreation.PackageFeed.Create(System.IO.DirectoryInfo! rootPath) -> Microsoft.Build.Utilities.ProjectCreation.PackageFeed!

0 commit comments

Comments
 (0)