Skip to content

Commit 5b0e85a

Browse files
committed
cache repeated accesses
1 parent 023d47c commit 5b0e85a

File tree

8 files changed

+175
-50
lines changed

8 files changed

+175
-50
lines changed

src/Cli/dotnet/Commands/Test/TestCommandParser.cs

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Diagnostics;
66
using Microsoft.DotNet.Cli.Extensions;
77
using Microsoft.Extensions.Configuration;
8+
using Microsoft.DotNet.Cli.Configuration;
9+
using Microsoft.Extensions.Configuration.DotnetCli.Services;
810

911
namespace Microsoft.DotNet.Cli.Commands.Test;
1012

@@ -166,48 +168,8 @@ public static Command GetCommand()
166168

167169
public static string GetTestRunnerName()
168170
{
169-
var builder = new ConfigurationBuilder();
170-
171-
string? dotnetConfigPath = GetDotnetConfigPath(Environment.CurrentDirectory);
172-
if (!File.Exists(dotnetConfigPath))
173-
{
174-
return CliConstants.VSTest;
175-
}
176-
177-
builder.AddIniFile(dotnetConfigPath);
178-
179-
IConfigurationRoot config = builder.Build();
180-
var testSection = config.GetSection("dotnet.test.runner");
181-
182-
if (!testSection.Exists())
183-
{
184-
return CliConstants.VSTest;
185-
}
186-
187-
string? runnerNameSection = testSection["name"];
188-
189-
if (string.IsNullOrEmpty(runnerNameSection))
190-
{
191-
return CliConstants.VSTest;
192-
}
193-
194-
return runnerNameSection;
195-
}
196-
197-
private static string? GetDotnetConfigPath(string? startDir)
198-
{
199-
string? directory = startDir;
200-
while (directory != null)
201-
{
202-
string dotnetConfigPath = Path.Combine(directory, "dotnet.config");
203-
if (File.Exists(dotnetConfigPath))
204-
{
205-
return dotnetConfigPath;
206-
}
207-
208-
directory = Path.GetDirectoryName(directory);
209-
}
210-
return null;
171+
var configurationService = DotNetConfigurationFactory.Create();
172+
return configurationService.Test.RunnerName;
211173
}
212174

213175
private static Command ConstructCommand()

src/Cli/dotnet/Configuration/DotNetConfigurationFactory.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.Extensions.Configuration.DotnetCli.Models;
66
using Microsoft.Extensions.Configuration.DotnetCli.Providers;
77
using Microsoft.Extensions.Configuration;
8+
using System.Collections.Concurrent;
89
using System.IO;
910

1011
namespace Microsoft.DotNet.Cli.Configuration
@@ -15,14 +16,44 @@ namespace Microsoft.DotNet.Cli.Configuration
1516
/// </summary>
1617
public static class DotNetConfigurationFactory
1718
{
19+
// Cache for the default configuration service instance
20+
private static readonly Lazy<IDotNetConfigurationService> _defaultInstance = new(() => CreateInternal(null), LazyThreadSafetyMode.ExecutionAndPublication);
21+
22+
// Cache for configuration services by working directory
23+
private static readonly ConcurrentDictionary<string, Lazy<IDotNetConfigurationService>> _instancesByDirectory = new();
24+
1825
/// <summary>
1926
/// Creates and configures the .NET CLI configuration service with default providers.
2027
/// This method follows the layered configuration approach: environment variables override global.json,
2128
/// and configuration is loaded lazily for performance.
29+
/// Results are cached to avoid repeated expensive configuration building.
2230
/// </summary>
2331
/// <param name="workingDirectory">The working directory to search for global.json files. Defaults to current directory.</param>
2432
/// <returns>A configured IDotNetConfigurationService instance</returns>
2533
public static IDotNetConfigurationService Create(string? workingDirectory = null)
34+
{
35+
if (workingDirectory == null)
36+
{
37+
// Use the default cached instance for null working directory
38+
return _defaultInstance.Value;
39+
}
40+
41+
// Normalize the working directory path for consistent caching
42+
var normalizedPath = Path.GetFullPath(workingDirectory);
43+
44+
// Get or create a cached instance for this specific working directory
45+
var lazyInstance = _instancesByDirectory.GetOrAdd(normalizedPath,
46+
path => new Lazy<IDotNetConfigurationService>(() => CreateInternal(path), LazyThreadSafetyMode.ExecutionAndPublication));
47+
48+
return lazyInstance.Value;
49+
}
50+
51+
/// <summary>
52+
/// Internal method that performs the actual configuration creation without caching.
53+
/// </summary>
54+
/// <param name="workingDirectory">The working directory to search for configuration files.</param>
55+
/// <returns>A configured IDotNetConfigurationService instance</returns>
56+
private static IDotNetConfigurationService CreateInternal(string? workingDirectory)
2657
{
2758
workingDirectory ??= Directory.GetCurrentDirectory();
2859

@@ -31,10 +62,13 @@ public static IDotNetConfigurationService Create(string? workingDirectory = null
3162
// Configuration sources are added in reverse precedence order
3263
// Last added has highest precedence
3364

34-
// 1. global.json (custom provider with key mapping) - lowest precedence
65+
// 1. dotnet.config (custom provider with key mapping) - lowest precedence
66+
configurationBuilder.Add(new DotNetConfigurationSource(workingDirectory));
67+
68+
// 2. global.json (custom provider with key mapping) - medium precedence
3569
configurationBuilder.Add(new GlobalJsonConfigurationSource(workingDirectory));
3670

37-
// 2. Environment variables (custom provider with key mapping) - highest precedence
71+
// 3. Environment variables (custom provider with key mapping) - highest precedence
3872
configurationBuilder.Add(new DotNetEnvironmentConfigurationSource());
3973

4074
var configuration = configurationBuilder.Build();
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.Extensions.Configuration.DotnetCli.Models;
5+
6+
/// <summary>
7+
/// Configuration settings that control test runner behavior.
8+
/// </summary>
9+
public sealed class TestConfiguration
10+
{
11+
/// <summary>
12+
/// Gets or sets the test runner name to use.
13+
/// Mapped from dotnet.config file: [dotnet.test.runner] name=VALUE
14+
/// Defaults to "VSTest" if not specified.
15+
/// </summary>
16+
public string RunnerName { get; set; } = "VSTest";
17+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Extensions.Configuration.Ini;
5+
6+
namespace Microsoft.Extensions.Configuration.DotnetCli.Providers;
7+
8+
/// <summary>
9+
/// Configuration provider for dotnet.config INI files with key mapping.
10+
/// Maps dotnet.config keys to the expected configuration structure.
11+
/// </summary>
12+
public class DotNetConfigurationProvider : IniConfigurationProvider
13+
{
14+
private static readonly Dictionary<string, string> KeyMappings = new(StringComparer.OrdinalIgnoreCase)
15+
{
16+
// Map INI section:key to expected configuration path
17+
["dotnet.test.runner:name"] = "Test:RunnerName",
18+
19+
// Future mappings can be added here for other dotnet.config settings
20+
// ["dotnet.example.section:key"] = "ConfigSection:Property",
21+
};
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="DotNetConfigurationProvider"/> class.
25+
/// </summary>
26+
/// <param name="source">The configuration source.</param>
27+
public DotNetConfigurationProvider(DotNetConfigurationSource source) : base(source)
28+
{
29+
}
30+
31+
/// <summary>
32+
/// Loads configuration data with key transformation.
33+
/// </summary>
34+
public override void Load()
35+
{
36+
// Load the INI file normally first
37+
base.Load();
38+
39+
// Transform keys according to our mapping
40+
var transformedData = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
41+
42+
foreach (var kvp in Data)
43+
{
44+
string key = kvp.Key;
45+
string? value = kvp.Value;
46+
47+
// Check if we have a mapping for this key
48+
if (KeyMappings.TryGetValue(key, out string? mappedKey))
49+
{
50+
transformedData[mappedKey] = value;
51+
}
52+
else
53+
{
54+
// Keep unmapped keys as-is
55+
transformedData[key] = value;
56+
}
57+
}
58+
59+
// Replace the data with transformed keys
60+
Data = transformedData;
61+
}
62+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Extensions.Configuration.Ini;
5+
6+
namespace Microsoft.Extensions.Configuration.DotnetCli.Providers;
7+
8+
/// <summary>
9+
/// Configuration source for dotnet.config INI files with key mapping.
10+
/// </summary>
11+
public class DotNetConfigurationSource : IniConfigurationSource
12+
{
13+
public DotNetConfigurationSource(string workingDirectory)
14+
{
15+
Path = FindDotNetConfigPath(workingDirectory);
16+
Optional = true; // Make it optional since dotnet.config may not exist
17+
}
18+
/// <summary>
19+
/// Builds the configuration provider for dotnet.config files.
20+
/// </summary>
21+
/// <param name="builder">The configuration builder.</param>
22+
/// <returns>The configuration provider.</returns>
23+
public override IConfigurationProvider Build(IConfigurationBuilder builder)
24+
{
25+
EnsureDefaults(builder);
26+
return new DotNetConfigurationProvider(this);
27+
}
28+
29+
private static string FindDotNetConfigPath(string? workingDirectory = null)
30+
{
31+
string? directory = workingDirectory ?? Directory.GetCurrentDirectory();
32+
// Search for dotnet.config in the current directory and upwards
33+
while (directory != null)
34+
{
35+
string dotnetConfigPath = System.IO.Path.Combine(directory, "dotnet.config");
36+
if (File.Exists(dotnetConfigPath))
37+
{
38+
return dotnetConfigPath;
39+
}
40+
41+
directory = System.IO.Path.GetDirectoryName(directory);
42+
}
43+
return "dotnet.config"; // Return default path even if not found
44+
}
45+
}

src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,8 @@ public static IConfiguration Create(string? workingDirectory = null)
2727

2828
workingDirectory ??= Directory.GetCurrentDirectory();
2929

30-
// Add dotnet.config if it exists (future enhancement)
31-
var dotnetConfigPath = Path.Combine(workingDirectory, "dotnet.config");
32-
if (File.Exists(dotnetConfigPath))
33-
{
34-
builder.AddIniFile(dotnetConfigPath, optional: true, reloadOnChange: false);
35-
}
30+
// Add dotnet.config with custom key mapping
31+
builder.Add(new DotNetConfigurationSource(workingDirectory));
3632

3733
// Add global.json with a custom configuration provider that maps keys
3834
builder.Add(new GlobalJsonConfigurationSource(workingDirectory));

src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class DotNetConfigurationService : IDotNetConfigurationService
2222
private readonly Lazy<DevelopmentConfiguration> _development;
2323
private readonly Lazy<ToolConfiguration> _tool;
2424
private readonly Lazy<NuGetConfiguration> _nuget;
25+
private readonly Lazy<TestConfiguration> _test;
2526

2627
public IConfiguration RawConfiguration => _configuration;
2728

@@ -35,6 +36,7 @@ public class DotNetConfigurationService : IDotNetConfigurationService
3536
public DevelopmentConfiguration Development => _development.Value;
3637
public ToolConfiguration Tool => _tool.Value;
3738
public NuGetConfiguration NuGet => _nuget.Value;
39+
public TestConfiguration Test => _test.Value;
3840

3941
public DotNetConfigurationService(IConfiguration configuration)
4042
{
@@ -59,5 +61,7 @@ public DotNetConfigurationService(IConfiguration configuration)
5961
_configuration.GetSection("Tool").Get<ToolConfiguration>() ?? new());
6062
_nuget = new Lazy<NuGetConfiguration>(() =>
6163
_configuration.GetSection("NuGet").Get<NuGetConfiguration>() ?? new());
64+
_test = new Lazy<TestConfiguration>(() =>
65+
_configuration.GetSection("Test").Get<TestConfiguration>() ?? new());
6266
}
6367
}

src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,9 @@ public interface IDotNetConfigurationService
5959
/// Gets NuGet package management configuration settings.
6060
/// </summary>
6161
NuGetConfiguration NuGet { get; }
62+
63+
/// <summary>
64+
/// Gets test runner configuration settings.
65+
/// </summary>
66+
TestConfiguration Test { get; }
6267
}

0 commit comments

Comments
 (0)