From 9e309e5e4537ba840e0c9a322b7c823ef737ec17 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Wed, 3 Jun 2026 19:51:25 -0500 Subject: [PATCH 01/17] Add agent instructions and MSBuild loader skills Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../skills/msbuild-loader-netcore/SKILL.md | 80 +++++++++++++++++++ .../msbuild-loader-netframework/SKILL.md | 67 ++++++++++++++++ AGENTS.md | 32 ++++++++ 3 files changed, 179 insertions(+) create mode 100644 .agents/skills/msbuild-loader-netcore/SKILL.md create mode 100644 .agents/skills/msbuild-loader-netframework/SKILL.md create mode 100644 AGENTS.md diff --git a/.agents/skills/msbuild-loader-netcore/SKILL.md b/.agents/skills/msbuild-loader-netcore/SKILL.md new file mode 100644 index 0000000..fd4f7d4 --- /dev/null +++ b/.agents/skills/msbuild-loader-netcore/SKILL.md @@ -0,0 +1,80 @@ +--- +name: msbuild-loader-netcore +description: >- + How MSBuildLocator loads MSBuild assemblies and discovers the .NET SDK on .NET + / .NET Core (the net8.0 target / #if NETCOREAPP branches in src/MSBuildLocator). + Covers the AssemblyLoadContext.Default.Resolving handler, search-path probing, + the SDK environment variables set on registration, hostfxr-based SDK discovery, + the dotnet location probe order, and the AllowQueryAll* widening flags. Use when + editing or reviewing net8.0 loader, registration, or SDK-discovery code, or + diagnosing assembly-resolution behavior on .NET Core hosts. +--- + +# .NET / .NET Core (`net8.0` / `NETCOREAPP`) MSBuild loader + +Scope: `src/MSBuildLocator/MSBuildLocator.cs` `#if NETCOREAPP` branches, plus +`DotNetSdkLocationHelper.cs` and `NativeMethods.cs`. For build/test and +cross-cutting conventions, see `AGENTS.md`. + +## Assembly-resolution handler +- `s_registeredHandler` is a static + `Func`; registration hooks + `AssemblyLoadContext.Default.Resolving`. +- Resolving can fire repeatedly; successful loads are cached in the local + `loadedAssemblies` dictionary keyed by `AssemblyName.FullName`. +- Resolution is not thread-safe; keep the `loadedAssemblies` lock around cache + lookup, path probing, `Assembly.LoadFrom`, and cache insert. +- The resolver receives `AssemblyName` directly — do not parse an event-args + name string (that is the net46 path). +- For each registered `msbuildSearchPaths` entry, probe + `Path.Combine(msbuildPath, assemblyName.Name + ".dll")` and load with + `Assembly.LoadFrom(targetAssembly)`. + +## SDK environment variables (NETCOREAPP only) +- Registration sets MSBuild SDK env vars via `ApplyDotNetSdkEnvironmentVariables`: + - `MSBUILD_EXE_PATH` = `\MSBuild.dll` + - `MSBuildExtensionsPath` = `` + - `MSBuildSDKsPath` = `\Sdks` +- `RegisterMSBuildPath(string)` applies these for that path before registering. +- `RegisterMSBuildPath(string[])` applies these for the first search path only, + then registers all paths. +- `RegisterInstance` applies these only when + `instance.DiscoveryType == DiscoveryType.DotNetSdk`. +- Do not move this setup into net46; Framework has its own `MSBUILD_EXE_PATH` + compatibility branch under `#if NET46` (see the `msbuild-loader-netframework` + skill). + +## SDK discovery +- Lives in `DotNetSdkLocationHelper`; instances are + `VisualStudioInstance(name: ".NET Core SDK", ..., DiscoveryType.DotNetSdk)`. +- Uses `NativeMethods` hostfxr P/Invoke under `#if NETCOREAPP` only: + - `hostfxr_resolve_sdk2` — best SDK, honoring `global.json` via + `WorkingDirectory`. + - `hostfxr_get_available_sdks` — installed SDK enumeration. +- `ResolveDotnetPathCandidates` preference order (tried in order): + `DOTNET_ROOT` (`DOTNET_ROOT(x86)` in a 32-bit process) → current process + directory when running under `dotnet` → `DOTNET_HOST_PATH` → + `DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR` → `PATH`. +- A successful `hostfxr_resolve_sdk2` sets `DOTNET_HOST_PATH` (if empty) to + `\dotnet(.exe)`. +- Default query returns the best SDK first, then unique SDK versions + newest-first from the available SDKs. + +## Widening flags +- `AllowQueryAllRuntimeVersions` / `VisualStudioInstanceQueryOptions.AllowAllRuntimeVersions` + include SDKs whose major/minor runtime exceeds `Environment.Version`. +- `AllowQueryAllDotnetLocations` / `VisualStudioInstanceQueryOptions.AllowAllDotnetLocations` + keep probing all dotnet candidate locations instead of stopping after the + first location that has SDKs. + +## What net8.0 does NOT do +- No Developer Console or Visual Studio Setup COM discovery; + `FEATURE_VISUALSTUDIOSETUP` package references/constants are net46-only in the + csproj. + +## Register-before-load contract +- `CanRegister` is false when already registered, or once any signed + `Microsoft.Build*` core assembly is loaded. +- JIT caveat: JIT-compilation of a method referencing `Microsoft.Build` types is + enough to load those assemblies and break registration. Keep locator calls + isolated before any such reference. diff --git a/.agents/skills/msbuild-loader-netframework/SKILL.md b/.agents/skills/msbuild-loader-netframework/SKILL.md new file mode 100644 index 0000000..23a5b78 --- /dev/null +++ b/.agents/skills/msbuild-loader-netframework/SKILL.md @@ -0,0 +1,67 @@ +--- +name: msbuild-loader-netframework +description: >- + How MSBuildLocator loads MSBuild assemblies and discovers installs on .NET + Framework (the net46 target / #if NET46 / non-NETCOREAPP branches in + src/MSBuildLocator). Covers the AppDomain.AssemblyResolve handler, search-path + probing, the pre-17.1 MSBuild.exe x86/amd64 fix, and Developer Console + Visual + Studio Setup (COM) discovery. Use when editing or reviewing net46 loader, + registration, or VS-discovery code, or diagnosing assembly-resolution behavior + on .NET Framework hosts. +--- + +# .NET Framework (`net46`) MSBuild loader + +Scope: `src/MSBuildLocator/MSBuildLocator.cs` `#if NET46` and Framework (`#else` +of `NETCOREAPP`) branches. `FEATURE_VISUALSTUDIOSETUP` is defined only when +`TargetFramework == net46` in `Microsoft.Build.Locator.csproj`. For build/test +and cross-cutting conventions, see `AGENTS.md`. + +## Assembly-resolution handler +- `s_registeredHandler` is a static `ResolveEventHandler`; `IsRegistered` is + `s_registeredHandler != null`. +- `RegisterMSBuildPathsInternally` stores the handler in the static field before + subscribing to `AppDomain.CurrentDomain.AssemblyResolve`; the static field + keeps the delegate alive so it persists. +- `AssemblyResolve` can fire repeatedly for the same assembly; results are cached + in `loadedAssemblies` keyed by `AssemblyName.FullName`. +- Resolution is explicitly not thread-safe; every cache lookup/load runs under + `lock (loadedAssemblies)`. +- Handler path: parse `eventArgs.Name` with `new AssemblyName(eventArgs.Name)`; + for each registered search path, if `\.dll` exists, return + `Assembly.LoadFrom(targetAssembly)`. +- Search paths come from `RegisterMSBuildPath(...)`, or from + `RegisterInstance(...)` as `instance.MSBuildPath` plus the VS NuGet path when + it exists. + +## Pre-17.1 MSBuild.exe bitness fix +- net46 reads `FileVersionInfo` from the first registered path containing + `MSBuild.exe`. +- For versions `< 17.1`, set `MSBUILD_EXE_PATH` to drive MSBuild's own lookup. +- If that pre-17.1 path ends in `\amd64`, strip the trailing folder and point + `MSBUILD_EXE_PATH` at the sibling x86 `MSBuild.exe`. + +## Discovery sources (net46 only) +- Developer command prompt: `GetDevConsoleInstance()` reads `VSINSTALLDIR`, then + parses `VSCMD_VER` (trimming any suffix after `-`), then falls back to + `VisualStudioVersion`; yields `DiscoveryType.DeveloperConsole`. +- Visual Studio Setup COM API: under `FEATURE_VISUALSTUDIOSETUP`, + `VisualStudioLocationHelper.GetInstances()` enumerates VS 2017+ setup instances + with `Microsoft.Component.MSBuild`; yields `DiscoveryType.VisualStudioSetup`. +- `DiscoveryType.DotNetSdk` exists in the enum but belongs to the Core path; net46 + `GetInstances(...)` does not call SDK discovery. + +## What net46 does NOT do +- No `AssemblyLoadContext`, `hostfxr`, or `.NET SDK` discovery — those are + `#if NETCOREAPP` paths (see the `msbuild-loader-netcore` skill). +- `ApplyDotNetSdkEnvironmentVariables(...)` exists in the file, but both + `RegisterMSBuildPath(...)` call sites that invoke it are `#if NETCOREAPP`. Do + not assume SDK env vars (`MSBUILD_EXE_PATH` to `MSBuild.dll`, + `MSBuildExtensionsPath`, `MSBuildSDKsPath`) are set on net46. + +## Register-before-load contract +- `CanRegister` is false once any strong-named `Microsoft.Build*` assembly in + `s_msBuildAssemblies` is loaded in the current `AppDomain`. +- JIT caveat: a method that references `Microsoft.Build` types can trip the + contract when JIT-compiled, even if that reference never executes. Keep locator + calls isolated before any such reference. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..02bc12e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ +# Microsoft.Build.Locator — Copilot instructions + +Small .NET library: locates an MSBuild install (Visual Studio or .NET SDK) and registers an assembly-resolution handler so the host app loads MSBuild's assemblies from that install. Ships as the `Microsoft.Build.Locator` NuGet package. + +## Build / test (root `MSBuildLocator.sln`, .NET CLI) +- `dotnet restore` / `dotnet build` (deterministic) / `dotnet test` / `dotnet pack --configuration Debug` +- Single test: `dotnet test --filter "FullyQualifiedName~QueryInstanceTests"` or `--filter "Name="` +- Tests: xUnit + Shouldly, in `src/MSBuildLocator.Tests`. +- Versioning: Nerdbank.GitVersioning (`version.json`) → build/pack needs full git history (CI `fetch-depth: 0`). +- PR validation: `.github/workflows/pull-request.yml` (`windows-latest`). Official builds: `azure-pipelines.yml` / `release-pipeline.yml`. Release steps: `Releasing_MSBuildLocator.md`. + +## Multi-targeting (central constraint) +Library: `net46` + `net8.0`. Tests: `net472` + `net8.0`. Non-trivial code forks per TFM — must compile/behave on both: +- `#if NETCOREAPP` → `AssemblyLoadContext.Resolving`; `#else` (Framework) → `AppDomain.AssemblyResolve` (`ResolveEventHandler`). See `s_registeredHandler` in `MSBuildLocator.cs`. +- `#if NET46` / `FEATURE_VISUALSTUDIOSETUP` (defined only for `net46` in csproj) gate VS Setup COM-interop discovery (`VisualStudioLocationHelper.cs`). `net8.0` does NO VS discovery — uses SDK path only. +- Always check whether a change must be mirrored/excluded under these conditionals. + +## Architecture (namespace `Microsoft.Build.Locator`) +- `MSBuildLocator.cs` — entry point: `RegisterDefaults`, `RegisterInstance`, `RegisterMSBuildPath`, `QueryVisualStudioInstances`, `CanRegister`, handler register/unregister. Both TFM forks live here. +- `DotNetSdkLocationHelper.cs` — `.NET SDK` discovery. `ResolveDotnetPathCandidates` builds an ordered preference list of dotnet locations, tried in order: `DOTNET_ROOT`(`(x86)` on 32-bit) → current process (if run from `dotnet`) → `DOTNET_HOST_PATH` → `DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR` → `PATH`. +- `NativeMethods.cs` — `NETCOREAPP`-only `hostfxr` interop (`hostfxr_resolve_sdk2`, `hostfxr_get_available_sdks`) used by SDK discovery. +- `VisualStudioLocationHelper.cs` — `net46`-only VS Setup (COM) discovery (VS 2017+). +- `VisualStudioInstance.cs`, `VisualStudioInstanceQueryOptions.cs`, `DiscoveryType.cs` — result/option types. +- `Utils/SemanticVersion*.cs`, `VersionComparer.cs` — internal SemVer parse/compare to order instances. +- Props/targets shipped from `src/MSBuildLocator/build/` (packed to `build/` + `buildTransitive/`). `EnsureMSBuildAssembliesNotCopied` emits error **MSBL001** when a consumer references `Microsoft.Build*` packages without `PrivateAssets="all"` / `ExcludeAssets="runtime"` (copying those assemblies locally breaks redirection). Keep the target's package list in sync with MSBuild's layout. + +## Conventions +- Contract-stable public API: csproj `EnablePackageValidation` + `PackageValidationBaselineVersion` (1.6.1). Intentional API changes require updating `src/MSBuildLocator/CompatibilitySuppressions.xml`. +- XML doc comments on public members; match existing style. +- Strong-name signed (`key.snk`) — don't remove signing. +- Build settings centralized in `Directory.Build.props` / `Directory.Solution.props` / `Directory.Build.rsp` — edit there, not per-project. +- Register-before-load contract: callers must register via Locator BEFORE any `Microsoft.Build.*` type loads (`CanRegister` → false once loaded). Preserve this + the lazy-loading patterns protecting it when refactoring. From 6fdd5000cf62515e81e93109d391d0b5f4c01406 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 4 Jun 2026 09:00:21 -0500 Subject: [PATCH 02/17] Fix two factual inaccuracies in AGENTS.md - Test class is QueryInstancesTests (plural); the singular form did not substring-match in dotnet test --filter. - Unregister() is a documented no-op kept only for back-compat; calling the section 'register/unregister' suggested behavior that does not exist. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 02bc12e..c2d274d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ Small .NET library: locates an MSBuild install (Visual Studio or .NET SDK) and r ## Build / test (root `MSBuildLocator.sln`, .NET CLI) - `dotnet restore` / `dotnet build` (deterministic) / `dotnet test` / `dotnet pack --configuration Debug` -- Single test: `dotnet test --filter "FullyQualifiedName~QueryInstanceTests"` or `--filter "Name="` +- Single test: `dotnet test --filter "FullyQualifiedName~QueryInstancesTests"` or `--filter "Name="` - Tests: xUnit + Shouldly, in `src/MSBuildLocator.Tests`. - Versioning: Nerdbank.GitVersioning (`version.json`) → build/pack needs full git history (CI `fetch-depth: 0`). - PR validation: `.github/workflows/pull-request.yml` (`windows-latest`). Official builds: `azure-pipelines.yml` / `release-pipeline.yml`. Release steps: `Releasing_MSBuildLocator.md`. @@ -16,7 +16,7 @@ Library: `net46` + `net8.0`. Tests: `net472` + `net8.0`. Non-trivial code forks - Always check whether a change must be mirrored/excluded under these conditionals. ## Architecture (namespace `Microsoft.Build.Locator`) -- `MSBuildLocator.cs` — entry point: `RegisterDefaults`, `RegisterInstance`, `RegisterMSBuildPath`, `QueryVisualStudioInstances`, `CanRegister`, handler register/unregister. Both TFM forks live here. +- `MSBuildLocator.cs` — entry point: `RegisterDefaults`, `RegisterInstance`, `RegisterMSBuildPath`, `QueryVisualStudioInstances`, `CanRegister`, handler registration. Both TFM forks live here. `Unregister()` is kept only for back-compat and is a no-op (the resolver is never removed). - `DotNetSdkLocationHelper.cs` — `.NET SDK` discovery. `ResolveDotnetPathCandidates` builds an ordered preference list of dotnet locations, tried in order: `DOTNET_ROOT`(`(x86)` on 32-bit) → current process (if run from `dotnet`) → `DOTNET_HOST_PATH` → `DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR` → `PATH`. - `NativeMethods.cs` — `NETCOREAPP`-only `hostfxr` interop (`hostfxr_resolve_sdk2`, `hostfxr_get_available_sdks`) used by SDK discovery. - `VisualStudioLocationHelper.cs` — `net46`-only VS Setup (COM) discovery (VS 2017+). From 7353228e052996a00dd3406254f391b5be436aa4 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 4 Jun 2026 09:46:32 -0500 Subject: [PATCH 03/17] simplify --- AGENTS.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c2d274d..db3a47a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,13 +1,16 @@ # Microsoft.Build.Locator — Copilot instructions -Small .NET library: locates an MSBuild install (Visual Studio or .NET SDK) and registers an assembly-resolution handler so the host app loads MSBuild's assemblies from that install. Ships as the `Microsoft.Build.Locator` NuGet package. +This library exists to locate an MSBuild install (Visual Studio or .NET SDK) and register an assembly-resolution handler so the host app loads MSBuild's assemblies from that install. This is required for any use of the .NET API from an application that is not part of Visual Studio or the .NET SDK. + +A .NET Framework application can only locate MSBuild from a Visual Studio installation, and a .NET 8+ application can only locate MSBuild from a .NET SDK. This library's approach to finding the library and loading the assemblies is entirely different on the different runtimes. See .agents/skills/msbuild-loader-netcore/SKILL.md and .agents/skills/msbuild-loader-netframework/SKILL.md for details. + +The core `Microsoft.Build.Locator.dll` must have minimal dependencies—nothing outside the core libraries provided by .NET for the relevant TargetFramework. ## Build / test (root `MSBuildLocator.sln`, .NET CLI) -- `dotnet restore` / `dotnet build` (deterministic) / `dotnet test` / `dotnet pack --configuration Debug` +- `dotnet restore` / `dotnet build` / `dotnet test` - Single test: `dotnet test --filter "FullyQualifiedName~QueryInstancesTests"` or `--filter "Name="` - Tests: xUnit + Shouldly, in `src/MSBuildLocator.Tests`. -- Versioning: Nerdbank.GitVersioning (`version.json`) → build/pack needs full git history (CI `fetch-depth: 0`). -- PR validation: `.github/workflows/pull-request.yml` (`windows-latest`). Official builds: `azure-pipelines.yml` / `release-pipeline.yml`. Release steps: `Releasing_MSBuildLocator.md`. +- Versioning: Nerdbank.GitVersioning. Use SemVer 2 and update `version.json` on breaking changes or feature additions. ## Multi-targeting (central constraint) Library: `net46` + `net8.0`. Tests: `net472` + `net8.0`. Non-trivial code forks per TFM — must compile/behave on both: @@ -25,7 +28,7 @@ Library: `net46` + `net8.0`. Tests: `net472` + `net8.0`. Non-trivial code forks - Props/targets shipped from `src/MSBuildLocator/build/` (packed to `build/` + `buildTransitive/`). `EnsureMSBuildAssembliesNotCopied` emits error **MSBL001** when a consumer references `Microsoft.Build*` packages without `PrivateAssets="all"` / `ExcludeAssets="runtime"` (copying those assemblies locally breaks redirection). Keep the target's package list in sync with MSBuild's layout. ## Conventions -- Contract-stable public API: csproj `EnablePackageValidation` + `PackageValidationBaselineVersion` (1.6.1). Intentional API changes require updating `src/MSBuildLocator/CompatibilitySuppressions.xml`. +- Contract-stable public API: csproj `EnablePackageValidation` + `PackageValidationBaselineVersion` (1.6.1). Intentional API changes require updating `src/MSBuildLocator/CompatibilitySuppressions.xml` and an appropriate semver update. - XML doc comments on public members; match existing style. - Strong-name signed (`key.snk`) — don't remove signing. - Build settings centralized in `Directory.Build.props` / `Directory.Solution.props` / `Directory.Build.rsp` — edit there, not per-project. From 5946509ea9b791d297a02c2bea7bfd72b075a08c Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 4 Jun 2026 09:48:32 -0500 Subject: [PATCH 04/17] Prune TFM-specific detail from AGENTS.md into skills The AssemblyLoadContext-vs-AppDomain handler split, the ResolveDotnetPathCandidates probe order, the hostfxr API names, the VS Setup COM details, and the net46-vs-netcore gating for VisualStudioLocationHelper are already covered in the two loader skill files. Keeping them in AGENTS.md as well burns the always-loaded context budget for work that only touches one TFM. Replace the per-TFM lines with pointers to the relevant skill while keeping the multi-targeting heads-up (TFM list + conditional compilation constants) so agents still know to check both forks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- AGENTS.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index db3a47a..d73da2c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,16 +13,12 @@ The core `Microsoft.Build.Locator.dll` must have minimal dependencies—nothing - Versioning: Nerdbank.GitVersioning. Use SemVer 2 and update `version.json` on breaking changes or feature additions. ## Multi-targeting (central constraint) -Library: `net46` + `net8.0`. Tests: `net472` + `net8.0`. Non-trivial code forks per TFM — must compile/behave on both: -- `#if NETCOREAPP` → `AssemblyLoadContext.Resolving`; `#else` (Framework) → `AppDomain.AssemblyResolve` (`ResolveEventHandler`). See `s_registeredHandler` in `MSBuildLocator.cs`. -- `#if NET46` / `FEATURE_VISUALSTUDIOSETUP` (defined only for `net46` in csproj) gate VS Setup COM-interop discovery (`VisualStudioLocationHelper.cs`). `net8.0` does NO VS discovery — uses SDK path only. -- Always check whether a change must be mirrored/excluded under these conditionals. +Library: `net46` + `net8.0`. Tests: `net472` + `net8.0`. Non-trivial code forks per TFM via `#if NETCOREAPP`, `#if NET46`, and `FEATURE_VISUALSTUDIOSETUP` (defined only for `net46`). Always check whether a change must be mirrored or excluded across these conditionals; for what each fork actually does, load the `msbuild-loader-netcore` or `msbuild-loader-netframework` skill. ## Architecture (namespace `Microsoft.Build.Locator`) - `MSBuildLocator.cs` — entry point: `RegisterDefaults`, `RegisterInstance`, `RegisterMSBuildPath`, `QueryVisualStudioInstances`, `CanRegister`, handler registration. Both TFM forks live here. `Unregister()` is kept only for back-compat and is a no-op (the resolver is never removed). -- `DotNetSdkLocationHelper.cs` — `.NET SDK` discovery. `ResolveDotnetPathCandidates` builds an ordered preference list of dotnet locations, tried in order: `DOTNET_ROOT`(`(x86)` on 32-bit) → current process (if run from `dotnet`) → `DOTNET_HOST_PATH` → `DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR` → `PATH`. -- `NativeMethods.cs` — `NETCOREAPP`-only `hostfxr` interop (`hostfxr_resolve_sdk2`, `hostfxr_get_available_sdks`) used by SDK discovery. -- `VisualStudioLocationHelper.cs` — `net46`-only VS Setup (COM) discovery (VS 2017+). +- `DotNetSdkLocationHelper.cs`, `NativeMethods.cs` — `.NET SDK` discovery (hostfxr); see the `msbuild-loader-netcore` skill. +- `VisualStudioLocationHelper.cs` — Visual Studio Setup (COM) discovery; see the `msbuild-loader-netframework` skill. - `VisualStudioInstance.cs`, `VisualStudioInstanceQueryOptions.cs`, `DiscoveryType.cs` — result/option types. - `Utils/SemanticVersion*.cs`, `VersionComparer.cs` — internal SemVer parse/compare to order instances. - Props/targets shipped from `src/MSBuildLocator/build/` (packed to `build/` + `buildTransitive/`). `EnsureMSBuildAssembliesNotCopied` emits error **MSBL001** when a consumer references `Microsoft.Build*` packages without `PrivateAssets="all"` / `ExcludeAssets="runtime"` (copying those assemblies locally breaks redirection). Keep the target's package list in sync with MSBuild's layout. From e372c24a575803a2ca0ef35157334756fbc0bd47 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Fri, 14 Aug 2026 14:00:40 -0500 Subject: [PATCH 05/17] no platform-specific anything in .NET Clarified platform-specific code requirements for .NET SDK discovery. --- .agents/skills/msbuild-loader-netcore/SKILL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.agents/skills/msbuild-loader-netcore/SKILL.md b/.agents/skills/msbuild-loader-netcore/SKILL.md index fd4f7d4..58bb00c 100644 --- a/.agents/skills/msbuild-loader-netcore/SKILL.md +++ b/.agents/skills/msbuild-loader-netcore/SKILL.md @@ -16,6 +16,8 @@ Scope: `src/MSBuildLocator/MSBuildLocator.cs` `#if NETCOREAPP` branches, plus `DotNetSdkLocationHelper.cs` and `NativeMethods.cs`. For build/test and cross-cutting conventions, see `AGENTS.md`. +No platform-specific code should be required for .NET SDK discovery. + ## Assembly-resolution handler - `s_registeredHandler` is a static `Func`; registration hooks From ca4e9b2422cdc15bee1fed0a93ee3f5992ce1544 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 27 Aug 2026 16:39:32 -0500 Subject: [PATCH 06/17] Refactor multi-targeting section for clarity and detail on loader skills --- AGENTS.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 02bc12e..ce84eed 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,16 +10,15 @@ Small .NET library: locates an MSBuild install (Visual Studio or .NET SDK) and r - PR validation: `.github/workflows/pull-request.yml` (`windows-latest`). Official builds: `azure-pipelines.yml` / `release-pipeline.yml`. Release steps: `Releasing_MSBuildLocator.md`. ## Multi-targeting (central constraint) -Library: `net46` + `net8.0`. Tests: `net472` + `net8.0`. Non-trivial code forks per TFM — must compile/behave on both: -- `#if NETCOREAPP` → `AssemblyLoadContext.Resolving`; `#else` (Framework) → `AppDomain.AssemblyResolve` (`ResolveEventHandler`). See `s_registeredHandler` in `MSBuildLocator.cs`. -- `#if NET46` / `FEATURE_VISUALSTUDIOSETUP` (defined only for `net46` in csproj) gate VS Setup COM-interop discovery (`VisualStudioLocationHelper.cs`). `net8.0` does NO VS discovery — uses SDK path only. -- Always check whether a change must be mirrored/excluded under these conditionals. +Library: `net46` + `net8.0`. Tests: `net472` + `net8.0`. Non-trivial code forks per TFM (`#if NETCOREAPP` / `#if NET46` / `FEATURE_VISUALSTUDIOSETUP`) and must compile/behave on both — always check whether a change belongs under these conditionals or in the common code paths. For the per-TFM loader, registration, and discovery details, use the skills: +- `msbuild-loader-netcore` — the `net8.0` / `#if NETCOREAPP` path (`AssemblyLoadContext.Resolving`, SDK discovery, hostfxr). +- `msbuild-loader-netframework` — the `net46` path (`AppDomain.AssemblyResolve`, VS Setup COM discovery). ## Architecture (namespace `Microsoft.Build.Locator`) -- `MSBuildLocator.cs` — entry point: `RegisterDefaults`, `RegisterInstance`, `RegisterMSBuildPath`, `QueryVisualStudioInstances`, `CanRegister`, handler register/unregister. Both TFM forks live here. -- `DotNetSdkLocationHelper.cs` — `.NET SDK` discovery. `ResolveDotnetPathCandidates` builds an ordered preference list of dotnet locations, tried in order: `DOTNET_ROOT`(`(x86)` on 32-bit) → current process (if run from `dotnet`) → `DOTNET_HOST_PATH` → `DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR` → `PATH`. -- `NativeMethods.cs` — `NETCOREAPP`-only `hostfxr` interop (`hostfxr_resolve_sdk2`, `hostfxr_get_available_sdks`) used by SDK discovery. -- `VisualStudioLocationHelper.cs` — `net46`-only VS Setup (COM) discovery (VS 2017+). +- `MSBuildLocator.cs` — entry point: `RegisterDefaults`, `RegisterInstance`, `RegisterMSBuildPath`, `QueryVisualStudioInstances`, `CanRegister`, handler register/unregister. Both TFM forks live here (see the loader skills). +- `DotNetSdkLocationHelper.cs` — `.NET SDK` discovery (Core path; see `msbuild-loader-netcore`). +- `NativeMethods.cs` — `NETCOREAPP`-only `hostfxr` interop used by SDK discovery (see `msbuild-loader-netcore`). +- `VisualStudioLocationHelper.cs` — `net46`-only VS Setup (COM) discovery (see `msbuild-loader-netframework`). - `VisualStudioInstance.cs`, `VisualStudioInstanceQueryOptions.cs`, `DiscoveryType.cs` — result/option types. - `Utils/SemanticVersion*.cs`, `VersionComparer.cs` — internal SemVer parse/compare to order instances. - Props/targets shipped from `src/MSBuildLocator/build/` (packed to `build/` + `buildTransitive/`). `EnsureMSBuildAssembliesNotCopied` emits error **MSBL001** when a consumer references `Microsoft.Build*` packages without `PrivateAssets="all"` / `ExcludeAssets="runtime"` (copying those assemblies locally breaks redirection). Keep the target's package list in sync with MSBuild's layout. From 765b70ab8f9874e75196d1df1a2de2438fd03257 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 27 Aug 2026 16:41:53 -0500 Subject: [PATCH 07/17] Clarify MSBL001 / do-not-copy-MSBuild guidance in AGENTS.md Split the props/targets guidance into the core no-copy rule and keeping the target's package list in sync. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2b3282da-484d-4b88-8a92-e2db9c2dac8e --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index ce84eed..54cf68a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,8 @@ Library: `net46` + `net8.0`. Tests: `net472` + `net8.0`. Non-trivial code forks - `VisualStudioLocationHelper.cs` — `net46`-only VS Setup (COM) discovery (see `msbuild-loader-netframework`). - `VisualStudioInstance.cs`, `VisualStudioInstanceQueryOptions.cs`, `DiscoveryType.cs` — result/option types. - `Utils/SemanticVersion*.cs`, `VersionComparer.cs` — internal SemVer parse/compare to order instances. -- Props/targets shipped from `src/MSBuildLocator/build/` (packed to `build/` + `buildTransitive/`). `EnsureMSBuildAssembliesNotCopied` emits error **MSBL001** when a consumer references `Microsoft.Build*` packages without `PrivateAssets="all"` / `ExcludeAssets="runtime"` (copying those assemblies locally breaks redirection). Keep the target's package list in sync with MSBuild's layout. +- Props/targets ship from `src/MSBuildLocator/build/` to `build/` and `buildTransitive/`. Never ship MSBuild DLLs with an app: local copies load before Locator's handler. `EnsureMSBuildAssembliesNotCopied` reports **MSBL001**; fix the flagged `` with `ExcludeAssets="runtime"` and `PrivateAssets="all"`. +- Keep `EnsureMSBuildAssembliesNotCopied`'s hardcoded package list synchronized with MSBuild's redistributable assemblies so it catches new packages. ## Conventions - Contract-stable public API: csproj `EnablePackageValidation` + `PackageValidationBaselineVersion` (1.6.1). Intentional API changes require updating `src/MSBuildLocator/CompatibilitySuppressions.xml`. From bc18702899928a3a59d6e09d4358ffff64eba45c Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 27 Aug 2026 16:44:02 -0500 Subject: [PATCH 08/17] Remove overdetailed "bitness fix" section --- .agents/skills/msbuild-loader-netframework/SKILL.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.agents/skills/msbuild-loader-netframework/SKILL.md b/.agents/skills/msbuild-loader-netframework/SKILL.md index 23a5b78..1470e76 100644 --- a/.agents/skills/msbuild-loader-netframework/SKILL.md +++ b/.agents/skills/msbuild-loader-netframework/SKILL.md @@ -34,13 +34,6 @@ and cross-cutting conventions, see `AGENTS.md`. `RegisterInstance(...)` as `instance.MSBuildPath` plus the VS NuGet path when it exists. -## Pre-17.1 MSBuild.exe bitness fix -- net46 reads `FileVersionInfo` from the first registered path containing - `MSBuild.exe`. -- For versions `< 17.1`, set `MSBUILD_EXE_PATH` to drive MSBuild's own lookup. -- If that pre-17.1 path ends in `\amd64`, strip the trailing folder and point - `MSBUILD_EXE_PATH` at the sibling x86 `MSBuild.exe`. - ## Discovery sources (net46 only) - Developer command prompt: `GetDevConsoleInstance()` reads `VSINSTALLDIR`, then parses `VSCMD_VER` (trimming any suffix after `-`), then falls back to From 42ed9f58b88e1a4e38063765705f6e59824e99a9 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 27 Aug 2026 16:49:55 -0500 Subject: [PATCH 09/17] drop net46 env var stuff --- .agents/skills/msbuild-loader-netframework/SKILL.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.agents/skills/msbuild-loader-netframework/SKILL.md b/.agents/skills/msbuild-loader-netframework/SKILL.md index 1470e76..9e91a4d 100644 --- a/.agents/skills/msbuild-loader-netframework/SKILL.md +++ b/.agents/skills/msbuild-loader-netframework/SKILL.md @@ -47,10 +47,6 @@ and cross-cutting conventions, see `AGENTS.md`. ## What net46 does NOT do - No `AssemblyLoadContext`, `hostfxr`, or `.NET SDK` discovery — those are `#if NETCOREAPP` paths (see the `msbuild-loader-netcore` skill). -- `ApplyDotNetSdkEnvironmentVariables(...)` exists in the file, but both - `RegisterMSBuildPath(...)` call sites that invoke it are `#if NETCOREAPP`. Do - not assume SDK env vars (`MSBUILD_EXE_PATH` to `MSBuild.dll`, - `MSBuildExtensionsPath`, `MSBuildSDKsPath`) are set on net46. ## Register-before-load contract - `CanRegister` is false once any strong-named `Microsoft.Build*` assembly in From 603e4ac2381245a1be7d686e94e2bf317eac50c7 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 27 Aug 2026 16:52:54 -0500 Subject: [PATCH 10/17] add self-update --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 54cf68a..736dcca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,8 @@ Library: `net46` + `net8.0`. Tests: `net472` + `net8.0`. Non-trivial code forks - `msbuild-loader-netcore` — the `net8.0` / `#if NETCOREAPP` path (`AssemblyLoadContext.Resolving`, SDK discovery, hostfxr). - `msbuild-loader-netframework` — the `net46` path (`AppDomain.AssemblyResolve`, VS Setup COM discovery). +When changing behavior documented by either skill, update the skill in the same change. + ## Architecture (namespace `Microsoft.Build.Locator`) - `MSBuildLocator.cs` — entry point: `RegisterDefaults`, `RegisterInstance`, `RegisterMSBuildPath`, `QueryVisualStudioInstances`, `CanRegister`, handler register/unregister. Both TFM forks live here (see the loader skills). - `DotNetSdkLocationHelper.cs` — `.NET SDK` discovery (Core path; see `msbuild-loader-netcore`). From 578c6633cac8fd300f066bec0e707a80ad49d0a1 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 27 Aug 2026 17:07:40 -0500 Subject: [PATCH 11/17] Correct resolver registration documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2b3282da-484d-4b88-8a92-e2db9c2dac8e --- .agents/skills/msbuild-loader-netframework/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.agents/skills/msbuild-loader-netframework/SKILL.md b/.agents/skills/msbuild-loader-netframework/SKILL.md index 9e91a4d..8a69a82 100644 --- a/.agents/skills/msbuild-loader-netframework/SKILL.md +++ b/.agents/skills/msbuild-loader-netframework/SKILL.md @@ -21,8 +21,8 @@ and cross-cutting conventions, see `AGENTS.md`. - `s_registeredHandler` is a static `ResolveEventHandler`; `IsRegistered` is `s_registeredHandler != null`. - `RegisterMSBuildPathsInternally` stores the handler in the static field before - subscribing to `AppDomain.CurrentDomain.AssemblyResolve`; the static field - keeps the delegate alive so it persists. + subscribing to `AppDomain.CurrentDomain.AssemblyResolve`; the event subscription + keeps the delegate alive, while the field tracks registration state. - `AssemblyResolve` can fire repeatedly for the same assembly; results are cached in `loadedAssemblies` keyed by `AssemblyName.FullName`. - Resolution is explicitly not thread-safe; every cache lookup/load runs under From 26879fbd0a011722a00b726c396d0b19793f03da Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 27 Aug 2026 17:10:01 -0500 Subject: [PATCH 12/17] Apply suggestion from @rainersigwald --- .agents/skills/msbuild-loader-netcore/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/skills/msbuild-loader-netcore/SKILL.md b/.agents/skills/msbuild-loader-netcore/SKILL.md index 58bb00c..8c34ce4 100644 --- a/.agents/skills/msbuild-loader-netcore/SKILL.md +++ b/.agents/skills/msbuild-loader-netcore/SKILL.md @@ -16,7 +16,7 @@ Scope: `src/MSBuildLocator/MSBuildLocator.cs` `#if NETCOREAPP` branches, plus `DotNetSdkLocationHelper.cs` and `NativeMethods.cs`. For build/test and cross-cutting conventions, see `AGENTS.md`. -No platform-specific code should be required for .NET SDK discovery. +SDK discovery is cross-platform, but executable names, hostfxr library loading, and symlink handling have OS-specific implementations.``` ## Assembly-resolution handler - `s_registeredHandler` is a static From d45357811bbc1b65923795be080f04d25efb7197 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 27 Aug 2026 17:11:27 -0500 Subject: [PATCH 13/17] Remove obsolete bitness fix mention Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2b3282da-484d-4b88-8a92-e2db9c2dac8e --- .agents/skills/msbuild-loader-netframework/SKILL.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.agents/skills/msbuild-loader-netframework/SKILL.md b/.agents/skills/msbuild-loader-netframework/SKILL.md index 8a69a82..e8e3bf5 100644 --- a/.agents/skills/msbuild-loader-netframework/SKILL.md +++ b/.agents/skills/msbuild-loader-netframework/SKILL.md @@ -4,10 +4,9 @@ description: >- How MSBuildLocator loads MSBuild assemblies and discovers installs on .NET Framework (the net46 target / #if NET46 / non-NETCOREAPP branches in src/MSBuildLocator). Covers the AppDomain.AssemblyResolve handler, search-path - probing, the pre-17.1 MSBuild.exe x86/amd64 fix, and Developer Console + Visual - Studio Setup (COM) discovery. Use when editing or reviewing net46 loader, - registration, or VS-discovery code, or diagnosing assembly-resolution behavior - on .NET Framework hosts. + probing, and Developer Console + Visual Studio Setup (COM) discovery. Use when + editing or reviewing net46 loader, registration, or VS-discovery code, or + diagnosing assembly-resolution behavior on .NET Framework hosts. --- # .NET Framework (`net46`) MSBuild loader From 1af100a7aef2d292b3d87bd91707148f81c1c90f Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 27 Aug 2026 17:22:58 -0500 Subject: [PATCH 14/17] Apply suggestion from @rainersigwald --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 26b3a7f..b053d3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ When changing behavior documented by either skill, update the skill in the same - `DotNetSdkLocationHelper.cs`, `NativeMethods.cs` — .NET SDK discovery (hostfxr); see `msbuild-loader-netcore`. - `VisualStudioLocationHelper.cs` — `net46`-only Visual Studio Setup discovery; see `msbuild-loader-netframework`. - `VisualStudioInstance.cs`, `VisualStudioInstanceQueryOptions.cs`, `DiscoveryType.cs` — result/option types. -- `Utils/SemanticVersion*.cs`, `VersionComparer.cs` — internal SemVer parse/compare to order instances. +- `Utils/SemanticVersion*.cs`, `VersionComparer.cs` — internal SemVer parse/compare that is an implementation detail of .NET SDK discovery. - Props/targets ship from `src/MSBuildLocator/build/` to `build/` and `buildTransitive/`. Never ship MSBuild DLLs with an app: local copies load before Locator's handler. `EnsureMSBuildAssembliesNotCopied` reports **MSBL001**; fix the flagged `` with `ExcludeAssets="runtime"` and `PrivateAssets="all"`. - Keep `EnsureMSBuildAssembliesNotCopied`'s hardcoded package list synchronized with MSBuild's redistributable assemblies so it catches new packages. From 47bb2e59e599adaa764bdfa647a5ebb55808b973 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 27 Aug 2026 17:24:01 -0500 Subject: [PATCH 15/17] Apply suggestion from @rainersigwald --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index b053d3c..ab47399 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,4 +31,4 @@ When changing behavior documented by either skill, update the skill in the same - XML doc comments on public members; match existing style. - Strong-name signed (`key.snk`) — don't remove signing. - Build settings centralized in `Directory.Build.props` / `Directory.Solution.props` / `Directory.Build.rsp` — edit there, not per-project. -- Register-before-load contract: callers must register via Locator BEFORE any `Microsoft.Build.*` type loads (`CanRegister` → false once loaded). Preserve this + the lazy-loading patterns protecting it when refactoring. + - Register-before-load contract: callers must register via Locator BEFORE any core MSBuild assembly loads (`CanRegister` → false once loaded). Preserve this + the lazy-loading patterns protecting it when refactoring.``` From 8e0f83408b4c2e183088a6f32058e11d81efbc19 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Thu, 27 Aug 2026 17:34:10 -0500 Subject: [PATCH 16/17] Add repository code review skill Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2b3282da-484d-4b88-8a92-e2db9c2dac8e --- .github/skills/code-review/SKILL.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/skills/code-review/SKILL.md diff --git a/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md new file mode 100644 index 0000000..3531c2f --- /dev/null +++ b/.github/skills/code-review/SKILL.md @@ -0,0 +1,26 @@ +--- +name: code-review +description: >- + Repository-specific checks for Microsoft.Build.Locator code reviews. Use for + loader, discovery, public API, and package changes. +license: MIT +--- + +# Microsoft.Build.Locator code review + +Read `AGENTS.md`. For loader, registration, or discovery changes, also read the +applicable `msbuild-loader-netcore` or `msbuild-loader-netframework` skill; read +both for common code. Verify that the skills accurately describe the code, and +require them to change when behavior changes. + +Check these repository-specific invariants: + +- Common code must work on both `net46` and `net8.0`; runtime-specific behavior + must stay behind the correct conditional. +- Registration must happen before any core `Microsoft.Build*` assembly loads. +- .NET Framework discovers Visual Studio; .NET 8+ discovers the .NET SDK. +- .NET Framework changes must remain compatible across supported Visual Studio + versions; do not assume only the latest MSBuild layout or behavior. +- `Microsoft.Build.Locator.dll` must retain minimal framework-only dependencies. +- Intentional public API changes require compatibility suppressions and an + appropriate version change. From 97cb0df0fa8c8c852d163189511b6acc59d00f84 Mon Sep 17 00:00:00 2001 From: Rainer Sigwald Date: Fri, 28 Aug 2026 09:28:32 -0500 Subject: [PATCH 17/17] stray backticks --- .agents/skills/msbuild-loader-netcore/SKILL.md | 2 +- AGENTS.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.agents/skills/msbuild-loader-netcore/SKILL.md b/.agents/skills/msbuild-loader-netcore/SKILL.md index 8c34ce4..c462a5b 100644 --- a/.agents/skills/msbuild-loader-netcore/SKILL.md +++ b/.agents/skills/msbuild-loader-netcore/SKILL.md @@ -16,7 +16,7 @@ Scope: `src/MSBuildLocator/MSBuildLocator.cs` `#if NETCOREAPP` branches, plus `DotNetSdkLocationHelper.cs` and `NativeMethods.cs`. For build/test and cross-cutting conventions, see `AGENTS.md`. -SDK discovery is cross-platform, but executable names, hostfxr library loading, and symlink handling have OS-specific implementations.``` +SDK discovery is cross-platform, but executable names, hostfxr library loading, and symlink handling have OS-specific implementations. ## Assembly-resolution handler - `s_registeredHandler` is a static diff --git a/AGENTS.md b/AGENTS.md index ab47399..681f0af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,4 +31,4 @@ When changing behavior documented by either skill, update the skill in the same - XML doc comments on public members; match existing style. - Strong-name signed (`key.snk`) — don't remove signing. - Build settings centralized in `Directory.Build.props` / `Directory.Solution.props` / `Directory.Build.rsp` — edit there, not per-project. - - Register-before-load contract: callers must register via Locator BEFORE any core MSBuild assembly loads (`CanRegister` → false once loaded). Preserve this + the lazy-loading patterns protecting it when refactoring.``` + - Register-before-load contract: callers must register via Locator BEFORE any core MSBuild assembly loads (`CanRegister` → false once loaded). Preserve this + the lazy-loading patterns protecting it when refactoring.