Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6807c16
Discover built-in scaffolders in process
danroth27 Sep 16, 2026
fa2c661
Preserve scaffolder provider extensibility
danroth27 Sep 16, 2026
de8a0d2
Narrow in-process discovery change
danroth27 Sep 16, 2026
5875c98
Streamline scaffolder discovery tests
danroth27 Sep 16, 2026
3d0fa4c
Preserve category picker logger dependency
danroth27 Sep 16, 2026
b4f27e8
Align discovery code formatting
danroth27 Sep 16, 2026
4d6e56a
Clarify scaffolder metadata ownership
danroth27 Sep 17, 2026
6aecdb6
Resolve built-in metadata from runner
danroth27 Sep 17, 2026
b0d0d38
Restrict scaffolder tool discovery
danroth27 Sep 17, 2026
388a8cf
Remove scaffolder package predicate test
danroth27 Sep 17, 2026
77f01d3
Skip tool restore during built-in discovery
danroth27 Sep 17, 2026
e567427
Reuse explicit component check
danroth27 Sep 17, 2026
b867bd1
Keep selected components non-null
danroth27 Sep 17, 2026
799f337
Avoid nullable suppression in tool discovery
danroth27 Sep 17, 2026
add1e2d
Distinguish null and empty component lists
danroth27 Sep 17, 2026
9c8b012
Clarify local tool restore behavior
danroth27 Sep 17, 2026
b0f17d9
Preserve local tool restore rationale
danroth27 Sep 17, 2026
33fa0e6
Simplify local tool restore comment
danroth27 Sep 17, 2026
e35d91f
Preserve empty component discovery behavior
danroth27 Sep 17, 2026
09335d4
Simplify default component selection
danroth27 Sep 17, 2026
28a3412
Preserve explicit component discovery
danroth27 Sep 18, 2026
e63a7cd
Query built-in scaffolder metadata from the running assembly
danroth27 Sep 20, 2026
cffdeda
Cover default scaffolder discovery and restore behavior
danroth27 Sep 21, 2026
aa3b1af
Capture CLI diagnostics in .NET 9 integration tests
danroth27 Sep 22, 2026
5c598ca
Use compatible Extensions dependencies for net8 and net9
danroth27 Sep 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
<Project>
<Import Project="Sdk.props" Sdk="Microsoft.DotNet.Arcade.Sdk" />

<!-- Extensions 11 pulls in a DiagnosticSource fallback that is incompatible with .NET 9 HTTP metrics. -->
<PropertyGroup Condition="'$(TargetFramework)' == 'net8.0' or '$(TargetFramework)' == 'net9.0'">
<MicrosoftExtensionsDependencyInjectionPackageVersion>$(MicrosoftExtensionsPackageVersion10)</MicrosoftExtensionsDependencyInjectionPackageVersion>
<MicrosoftExtensionsDependencyModelPackageVersion>$(MicrosoftExtensionsPackageVersion10)</MicrosoftExtensionsDependencyModelPackageVersion>
<MicrosoftExtensionsHostingPackageVersion>$(MicrosoftExtensionsPackageVersion10)</MicrosoftExtensionsHostingPackageVersion>
<MicrosoftExtensionsLoggingConsolePackageVersion>$(MicrosoftExtensionsPackageVersion10)</MicrosoftExtensionsLoggingConsolePackageVersion>
</PropertyGroup>

<PropertyGroup>
<Copyright>$(CopyrightMicrosoft)</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<MicrosoftExtensionsFileProvidersEmbeddedPackageVersion>11.0.0-rc.2.26455.110</MicrosoftExtensionsFileProvidersEmbeddedPackageVersion>
<!-- Microsoft.Extensions.Hosting -->
<MicrosoftExtensionsHostingPackageVersion>11.0.0-preview.1.26104.118</MicrosoftExtensionsHostingPackageVersion>
<MicrosoftExtensionsPackageVersion10>10.0.12</MicrosoftExtensionsPackageVersion10>
<!-- Microsoft.Extensions.Logging.Console -->
<MicrosoftExtensionsLoggingConsolePackageVersion>11.0.0-rc.2.26455.110</MicrosoftExtensionsLoggingConsolePackageVersion>
<!-- Microsoft.Extensions.Logging.Debug -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public CategoryDiscovery(IDotNetToolService dotnetToolService, DotNetToolInfo? c
.WithSpinner()
.Start("Discovering scaffolders", statusContext =>
{
return _dotnetToolService.GetAllCommandsParallel(envVars: envVars);
IList<DotNetToolInfo>? components = _componentPicked is null ? null : [_componentPicked];
return _dotnetToolService.GetAllCommandsParallel(components, envVars);
});

if (allCommands is not null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public CommandDiscovery(IDotNetToolService dotnetToolService, DotNetToolInfo? co
.WithSpinner()
.Start("Discovering scaffolders", statusContext =>
{
return _dotnetToolService.GetAllCommandsParallel(envVars: envVars);
IList<DotNetToolInfo>? components = _componentPicked is null ? null : [_componentPicked];
return _dotnetToolService.GetAllCommandsParallel(components, envVars);
});

if (allCommands is not null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ namespace Microsoft.DotNet.Tools.Scaffold.Services;
/// </summary>
internal class DotNetToolService : IDotNetToolService
{
private const string DotNetScaffoldPackageName = "Microsoft.dotnet-scaffold";

private readonly ILogger _logger;
private readonly IEnvironmentService _environmentService;
private readonly IFileSystem _fileSystem;
Expand All @@ -39,18 +41,28 @@ public DotNetToolService(ILogger<DotNetToolService> logger, IEnvironmentService

/// <summary>
/// Gets the list of commands provided by a specific .NET tool.
/// Built-in commands are queried from the running assembly.
/// </summary>
/// <param name="dotnetTool">The .NET tool information.</param>
/// <param name="envVars">Optional environment variables.</param>
/// <returns>List of <see cref="CommandInfo"/> objects, or an empty list if none found.</returns>
public List<CommandInfo> GetCommands(DotNetToolInfo dotnetTool, IDictionary<string, string>? envVars = null)
{
List<CommandInfo>? commands = null;
var runner = dotnetTool.IsGlobalTool ?
DotnetCliRunner.Create(dotnetTool.Command, ["get-commands"], envVars) :
DotnetCliRunner.CreateDotNet(dotnetTool.Command, ["get-commands"], envVars);
DotnetCliRunner runner;
if (IsDotNetScaffoldTool(dotnetTool))
{
// Preserve support for tools installed with --allow-roll-forward.
runner = DotnetCliRunner.CreateDotNet("exec", ["--roll-forward", "Major", typeof(DotNetToolService).Assembly.Location, "get-commands"], envVars);
}
else
{
runner = dotnetTool.IsGlobalTool ?
DotnetCliRunner.Create(dotnetTool.Command, ["get-commands"], envVars) :
DotnetCliRunner.CreateDotNet(dotnetTool.Command, ["get-commands"], envVars);
}

var exitCode = runner.ExecuteAndCaptureOutput(out var stdOut, out _);
var exitCode = ExecuteAndCaptureOutput(runner, out var stdOut, out _);
if (exitCode == 0 && !string.IsNullOrEmpty(stdOut))
{
try
Expand Down Expand Up @@ -95,25 +107,27 @@ public List<CommandInfo> GetCommands(DotNetToolInfo dotnetTool, IDictionary<stri
}

/// <summary>
/// Gets all commands from all .NET tools in parallel.
/// Gets all commands from the specified .NET tools in parallel.
/// </summary>
/// <param name="components">Optional list of components to query. If null, all tools are queried.</param>
/// <param name="components">Optional list of components to query. If null or empty, the dotnet-scaffold tool is queried.</param>
/// <param name="envVars">Optional environment variables.</param>
/// <returns>List of key-value pairs of tool command and <see cref="CommandInfo"/>.</returns>
public IList<KeyValuePair<string, CommandInfo>> GetAllCommandsParallel(IList<DotNetToolInfo>? components = null, IDictionary<string, string>? envVars = null)
{
var restoreLocalTools = components is { Count: > 0 };
if (components is null || components.Count == 0)
{
components = GetDotNetTools(refresh: true, envVars);
components = GetDotNetTools(refresh: true, envVars)
.Where(IsDotNetScaffoldTool)
.ToList();
Comment thread
danroth27 marked this conversation as resolved.
Comment thread
danroth27 marked this conversation as resolved.
Comment thread
danroth27 marked this conversation as resolved.
}

//if any local tools are present, we need to restore them first
//when sdks/runtimes are switched/rolled forward, local tools need to be restored before they are called
var anyLocalTools = components.FirstOrDefault(x => !x.IsGlobalTool) is not null;
if (anyLocalTools)
// Explicitly supplied local tools may need to be restored when SDKs or runtimes change.
// Default discovery queries the running assembly, so the discovered installation need not be restored.
if (restoreLocalTools && components.Any(x => !x.IsGlobalTool))
{
var runner = DotnetCliRunner.CreateDotNet("tool", ["restore"], envVars);
runner.ExecuteAndCaptureOutput(out _, out _);
ExecuteAndCaptureOutput(runner, out _, out _);
}

var options = new ParallelOptions
Expand All @@ -137,6 +151,15 @@ public IList<KeyValuePair<string, CommandInfo>> GetAllCommandsParallel(IList<Dot
return commands.ToList();
}

/// <summary>
/// Executes a tool discovery command and captures its output.
/// </summary>
protected virtual int ExecuteAndCaptureOutput(DotnetCliRunner runner, out string? stdOut, out string? stdErr)
=> runner.ExecuteAndCaptureOutput(out stdOut, out stdErr);

private static bool IsDotNetScaffoldTool(DotNetToolInfo tool)
=> tool.PackageName.Equals(DotNetScaffoldPackageName, StringComparison.OrdinalIgnoreCase);

/// <summary>
/// Installs a .NET tool using the dotnet CLI.
/// </summary>
Expand Down Expand Up @@ -233,8 +256,8 @@ public IList<DotNetToolInfo> GetDotNetTools(bool refresh = false, IDictionary<st
var dotnetToolList = new List<DotNetToolInfo>();
var runner = DotnetCliRunner.CreateDotNet("tool", ["list", "-g"], envVars);
var localRunner = DotnetCliRunner.CreateDotNet("tool", ["list"], envVars);
var exitCode = runner.ExecuteAndCaptureOutput(out var stdOut, out _);
var localExitCode = localRunner.ExecuteAndCaptureOutput(out var localStdOut, out var localStdErr);
var exitCode = ExecuteAndCaptureOutput(runner, out var stdOut, out _);
var localExitCode = ExecuteAndCaptureOutput(localRunner, out var localStdOut, out var localStdErr);
// Parse through local dotnet tools first.
if (localExitCode == 0 && !string.IsNullOrEmpty(localStdOut))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ internal interface IDotNetToolService
/// <summary>
/// Gets all commands for the specified tools in parallel.
/// </summary>
/// <param name="components">The list of tool components to query. If null, all tools are used.</param>
/// <param name="components">The list of tool components to query. If null or empty, the dotnet-scaffold tool is used.</param>
/// <param name="envVars">Optional environment variables for the command execution.</param>
/// <returns>A list of key-value pairs mapping tool command names to their command info.</returns>
IList<KeyValuePair<string, CommandInfo>> GetAllCommandsParallel(IList<DotNetToolInfo>? components = null, IDictionary<string, string>? envVars = null);
Expand Down Expand Up @@ -56,6 +56,7 @@ internal interface IDotNetToolService

/// <summary>
/// Gets the list of commands provided by a specific .NET tool.
/// Built-in commands are queried from the running assembly.
/// </summary>
/// <param name="dotnetTool">The tool to query for commands.</param>
/// <param name="envVars">Optional environment variables for the command execution.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
using System.Threading.Tasks;
using Microsoft.DotNet.Tools.Scaffold.Tests.Helpers;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.DotNet.Tools.Scaffold.Tests.AspNet.Integration.API;

public class ApiControllerNet9IntegrationTests : ApiControllerIntegrationTestsBase
public class ApiControllerNet9IntegrationTests(ITestOutputHelper output) : ApiControllerIntegrationTestsBase
{
protected override string TargetFramework => "net9.0";
protected override string TestClassName => nameof(ApiControllerNet9IntegrationTests);
Expand Down Expand Up @@ -36,6 +37,7 @@ public async Task Scaffold_ApiControllerCrud_Net9_CliInvocation()
"--controller", "TestApiController",
"--dataContext", "TestDbContext",
"--dbProvider", "sqlite-efcore");
output.WriteLine($"CLI exit code: {cliExitCode}\nStandard output:\n{cliOutput}\nStandard error:\n{cliError}");
Assert.True(cliExitCode == 0, $"CLI scaffold should succeed.\nOutput: {cliOutput}\nError: {cliError}");

// Assert — expected files were created
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
using System.Threading.Tasks;
using Microsoft.DotNet.Tools.Scaffold.Tests.Helpers;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.DotNet.Tools.Scaffold.Tests.AspNet.Integration.API;

public class MinimalApiNet9IntegrationTests : MinimalApiIntegrationTestsBase
public class MinimalApiNet9IntegrationTests(ITestOutputHelper output) : MinimalApiIntegrationTestsBase
{
protected override string TargetFramework => "net9.0";
protected override string TestClassName => nameof(MinimalApiNet9IntegrationTests);
Expand Down Expand Up @@ -36,6 +37,7 @@ public async Task Scaffold_MinimalApi_Net9_CliInvocation()
"--endpoints", "TestModelEndpoints",
"--dataContext", "TestDbContext",
"--dbProvider", "sqlite-efcore");
output.WriteLine($"CLI exit code: {cliExitCode}\nStandard output:\n{cliOutput}\nStandard error:\n{cliError}");
Assert.True(cliExitCode == 0, $"CLI scaffold should succeed.\nOutput: {cliOutput}\nError: {cliError}");

// Assert — expected files were created
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
using System.Threading.Tasks;
using Microsoft.DotNet.Tools.Scaffold.Tests.Helpers;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.DotNet.Tools.Scaffold.Tests.AspNet.Integration.Identity;

public class IdentityNet9IntegrationTests : IdentityIntegrationTestsBase
public class IdentityNet9IntegrationTests(ITestOutputHelper output) : IdentityIntegrationTestsBase
{
protected override string TargetFramework => "net9.0";
protected override string TestClassName => nameof(IdentityNet9IntegrationTests);
Expand Down Expand Up @@ -78,6 +79,7 @@ public async Task Scaffold_Identity_Net9_CliInvocation()
"--project", _testProjectPath,
"--dataContext", "TestDbContext",
"--dbProvider", "sqlite-efcore");
output.WriteLine($"CLI exit code: {cliExitCode}\nStandard output:\n{cliOutput}\nStandard error:\n{cliError}");
Assert.True(cliExitCode == 0, $"CLI scaffold should succeed.\nOutput: {cliOutput}\nError: {cliError}");

// Assert — expected files/directories were created
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
using System.Threading.Tasks;
using Microsoft.DotNet.Tools.Scaffold.Tests.Helpers;
using Xunit;
using Xunit.Abstractions;

namespace Microsoft.DotNet.Tools.Scaffold.Tests.AspNet.Integration.RazorPages;

public class RazorPagesCrudNet9IntegrationTests : RazorPagesCrudIntegrationTestsBase
public class RazorPagesCrudNet9IntegrationTests(ITestOutputHelper output) : RazorPagesCrudIntegrationTestsBase
{
protected override string TargetFramework => "net9.0";
protected override string TestClassName => nameof(RazorPagesCrudNet9IntegrationTests);
Expand Down Expand Up @@ -67,6 +68,7 @@ public async Task Scaffold_RazorPagesCrud_Net9_CliInvocation()
"--dataContext", "TestDbContext",
"--dbProvider", "sqlite-efcore",
"--page", "CRUD");
output.WriteLine($"CLI exit code: {cliExitCode}\nStandard output:\n{cliOutput}\nStandard error:\n{cliError}");
Assert.True(cliExitCode == 0, $"CLI scaffold should succeed.\nOutput: {cliOutput}\nError: {cliError}");

// Assert — expected files were created (skip if scaffolding encountered errors)
Expand Down
Loading
Loading