Skip to content

Use a shared EvaluationContext for ReleasePropertyProjectLocator pre-evaluation (pack/publish) #55193

Description

@ViktorHofer

Summary

ReleasePropertyProjectLocator pre-evaluates project(s) in-process (via the MSBuild Object Model) before dotnet pack/dotnet publish runs, solely to read PackRelease/PublishRelease and decide whether to inject Configuration=Release. Each evaluation is created with an isolated EvaluationContext (the default), so no state is shared between them. For a solution, it evaluates every project (Parallel.ForEach over sln.SolutionProjects), each paying the full cost of SDK resolution + importing the entire .props/.targets graph from scratch.

Passing a shared EvaluationContext to these evaluations would let them reuse SDK resolver results, file-existence checks, and glob/import I/O across all projects — the dominant cost of the property (pass 1) evaluation — with no behavioral change.

Where

src/Cli/dotnet/ReleasePropertyProjectLocator.cs

private static ProjectInstance? TryGetProjectInstance(string projectPath, ReadOnlyDictionary<string, string>? globalProperties)
{
    return new ProjectInstance(projectPath, globalProperties, "Current"); // isolated EvaluationContext (default)
}

and the solution fan-out in GetArbitraryProjectFromSolution:

Parallel.ForEach(sln.SolutionProjects.AsEnumerable(), (project, state) => {
    var projectData = TryGetProjectInstance(projectFullPath, globalProps);
    ...
});

Why an EvaluationContext helps

MSBuild has no partial ("properties-only") evaluation API — reading a single property still runs the full evaluation, and the expensive part is pass 1 (properties + the SDK import graph). MSBuild does, however, expose Microsoft.Build.Evaluation.EvaluationContext which caches across evaluations:

  • EvaluationContext.Create(EvaluationContext.SharingPolicy.Shared) — reuses SDK resolution, file existence, and glob/import I/O across all evaluations that use it. It is documented as thread-safe for parallel evaluations.
  • SharingPolicy.SharedSDKCache — reuses only the SDK resolver cache (a safer subset if full file-system caching is a concern for correctness).

ProjectInstance has a constructor overload that accepts an EvaluationContext. Creating one context per GetCustomDefaultConfigurationValueIfSpecified() call and threading it through GetTargetedProject -> GetArbitraryProjectFromSolution -> TryGetProjectInstance (and the single-project path) would collapse the repeated per-project SDK-resolver + import cost into a one-time cost.

For reference, MSBuild's own ProjectGraph construction already does exactly this — it creates one EvaluationContext.Create(SharingPolicy.Shared) and reuses it across all nodes.

Suggested change

  1. Create a single EvaluationContext (Shared, or SharedSDKCache if we want to be conservative about file-system caching correctness) inside GetCustomDefaultConfigurationValueIfSpecified.
  2. Pass it down to every new ProjectInstance(...) in TryGetProjectInstance (and the VirtualProjectBuildingCommand path) via the ProjectInstance overload that accepts an EvaluationContext.
  3. Keep the existing DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS opt-in (which evaluates only one project) as the fast path; the shared context reduces the cost of the default full-solution path.

Impact

  • Single project: minor (one evaluation either way).
  • Solution: potentially large. The pre-evaluation currently evaluates the entire solution just to read one property, with each project independently re-resolving the SDK and re-importing the full targets graph. Sharing the context removes that redundant per-project cost.
  • No behavioral change: the same property values are read; only caching of resolver/file-system state is shared.

Notes

This cost is invisible to -bl and MSBUILDDEBUGENGINE because these are pure Object-Model evaluations (no BuildManager, no logger), so it does not show up in binlogs and is easy to overlook when profiling pack/publish.

Related env vars for context: DOTNET_CLI_DISABLE_PUBLISH_AND_PACK_RELEASE (disables the whole pre-eval) and DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS (evaluate a single project instead of the whole solution).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions