Skip to content

Search for slnx files when setting solution-relative content root #61305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions src/Hosting/TestHost/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
#nullable enable
*REMOVED*static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! applicationBasePath, string! solutionName = "*.sln") -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
*REMOVED*static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! solutionName = "*.sln") -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! applicationBasePath, string! solutionName) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! applicationBasePath, System.ReadOnlySpan<string!> solutionNames = default(System.ReadOnlySpan<string!>)) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!
static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! solutionName) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder!

57 changes: 48 additions & 9 deletions src/Hosting/TestHost/src/WebHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.TestHost;
/// </summary>
public static class WebHostBuilderExtensions
{
private static readonly string[] _defaultSolutionNames = ["*.sln", "*.slnx"];

/// <summary>
/// Enables the <see cref="TestServer" /> service.
/// </summary>
Expand Down Expand Up @@ -115,20 +117,32 @@ public static IWebHostBuilder ConfigureTestContainer<TContainer>(this IWebHostBu
return webHostBuilder;
}

/// <summary>
/// Sets the content root of relative to the <paramref name="solutionRelativePath" />.
/// </summary>
/// <param name="builder">The <see cref="IWebHostBuilder"/>.</param>
/// <param name="solutionRelativePath">The directory of the solution file.</param>
/// <returns>The <see cref="IWebHostBuilder"/>.</returns>
public static IWebHostBuilder UseSolutionRelativeContentRoot(
this IWebHostBuilder builder,
string solutionRelativePath)
{
return builder.UseSolutionRelativeContentRoot(solutionRelativePath, AppContext.BaseDirectory, _defaultSolutionNames);
}

/// <summary>
/// Sets the content root of relative to the <paramref name="solutionRelativePath" />.
/// </summary>
/// <param name="builder">The <see cref="IWebHostBuilder"/>.</param>
/// <param name="solutionRelativePath">The directory of the solution file.</param>
/// <param name="solutionName">The name of the solution file to make the content root relative to.</param>
/// <returns>The <see cref="IWebHostBuilder"/>.</returns>
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
public static IWebHostBuilder UseSolutionRelativeContentRoot(
this IWebHostBuilder builder,
string solutionRelativePath,
string solutionName = "*.sln")
string solutionName)
{
return builder.UseSolutionRelativeContentRoot(solutionRelativePath, AppContext.BaseDirectory, solutionName);
return builder.UseSolutionRelativeContentRoot(solutionRelativePath, AppContext.BaseDirectory, [solutionName]);
}

/// <summary>
Expand All @@ -139,24 +153,49 @@ public static IWebHostBuilder UseSolutionRelativeContentRoot(
/// <param name="applicationBasePath">The root of the app's directory.</param>
/// <param name="solutionName">The name of the solution file to make the content root relative to.</param>
/// <returns>The <see cref="IWebHostBuilder"/>.</returns>
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
public static IWebHostBuilder UseSolutionRelativeContentRoot(
this IWebHostBuilder builder,
string solutionRelativePath,
string applicationBasePath,
string solutionName = "*.sln")
string solutionName)
{
return builder.UseSolutionRelativeContentRoot(solutionRelativePath, applicationBasePath, [solutionName]);
}

/// <summary>
/// Sets the content root of relative to the <paramref name="solutionRelativePath" />.
/// </summary>
/// <param name="builder">The <see cref="IWebHostBuilder"/>.</param>
/// <param name="solutionRelativePath">The directory of the solution file.</param>
/// <param name="applicationBasePath">The root of the app's directory.</param>
/// <param name="solutionNames">The names of the solution files to make the content root relative to. If empty, defaults to *.sln and *.slnx.</param>
/// <returns>The <see cref="IWebHostBuilder"/>.</returns>
[SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads", Justification = "Required to maintain compatibility")]
public static IWebHostBuilder UseSolutionRelativeContentRoot(
this IWebHostBuilder builder,
string solutionRelativePath,
string applicationBasePath,
ReadOnlySpan<string> solutionNames = default)
{
ArgumentNullException.ThrowIfNull(solutionRelativePath);
ArgumentNullException.ThrowIfNull(applicationBasePath);

if (solutionNames.IsEmpty)
{
solutionNames = _defaultSolutionNames;
}

var directoryInfo = new DirectoryInfo(applicationBasePath);
do
{
var solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, solutionName).FirstOrDefault();
if (solutionPath != null)
foreach (var solutionName in solutionNames)
{
builder.UseContentRoot(Path.GetFullPath(Path.Combine(directoryInfo.FullName, solutionRelativePath)));
return builder;
var solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, solutionName).FirstOrDefault();
if (solutionPath != null)
{
builder.UseContentRoot(Path.GetFullPath(Path.Combine(directoryInfo.FullName, solutionRelativePath)));
return builder;
}
}

directoryInfo = directoryInfo.Parent;
Expand Down
200 changes: 200 additions & 0 deletions src/Hosting/TestHost/test/UseSolutionRelativeContentRootTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.TestHost;

#pragma warning disable ASPDEPR004 // WebHostBuilder is obsolete
#pragma warning disable ASPDEPR008 // WebHost is obsolete
public class UseSolutionRelativeContentRootTests : IDisposable
{
private readonly string _tempDirectory;
private readonly string _contentDirectory;

public UseSolutionRelativeContentRootTests()
{
_tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")[..8]);
_contentDirectory = Path.Combine(_tempDirectory, "src");
Directory.CreateDirectory(_contentDirectory);
}

[Fact]
public void UseSolutionRelativeContentRoot_FindsSlnFile()
{
var solutionFile = Path.Combine(_tempDirectory, "TestApp.sln");
File.WriteAllText(solutionFile, "Microsoft Visual Studio Solution File, Format Version 12.00");

var builder = new WebHostBuilder()
.UseTestServer()
.Configure(app => { });

builder.UseSolutionRelativeContentRoot("src", applicationBasePath: _tempDirectory);

using var host = builder.Build();
var environment = host.Services.GetRequiredService<IWebHostEnvironment>();

Assert.Equal(_contentDirectory, environment.ContentRootPath);
}

[Fact]
public void UseSolutionRelativeContentRoot_FindsSlnxFile()
{
var solutionFile = Path.Combine(_tempDirectory, "TestApp.slnx");
File.WriteAllText(solutionFile, """
<Solution>
<Configurations>
<Configuration Name="Debug|Any CPU" />
<Configuration Name="Release|Any CPU" />
</Configurations>
</Solution>
""");

var builder = new WebHostBuilder()
.UseTestServer()
.Configure(app => { });

builder.UseSolutionRelativeContentRoot("src", applicationBasePath: _tempDirectory);

using var host = builder.Build();
var environment = host.Services.GetRequiredService<IWebHostEnvironment>();

Assert.Equal(_contentDirectory, environment.ContentRootPath);
}

[Fact]
public void UseSolutionRelativeContentRoot_WithSolutionName_FindsSpecifiedFile()
{
var subDirectory = Path.Combine(_tempDirectory, "sub");
Directory.CreateDirectory(subDirectory);

var slnFile = Path.Combine(subDirectory, "TestApp.sln");
var slnxFile = Path.Combine(_tempDirectory, "TestApp.slnx");
File.WriteAllText(slnFile, "Microsoft Visual Studio Solution File, Format Version 12.00");
File.WriteAllText(slnxFile, """
<Solution>
<Configurations>
<Configuration Name="Debug|Any CPU" />
</Configurations>
</Solution>
""");

var builder = new WebHostBuilder()
.UseTestServer()
.Configure(app => { });

builder.UseSolutionRelativeContentRoot("src", _tempDirectory, "*.slnx");

using var host = builder.Build();
var environment = host.Services.GetRequiredService<IWebHostEnvironment>();

Assert.Equal(_contentDirectory, environment.ContentRootPath);
}

[Fact]
public void UseSolutionRelativeContentRoot_WithMultipleSolutionNames_FindsInCurrentDirectoryFirst()
{
var expectedPath = Path.Combine(_contentDirectory, "sub");
Directory.CreateDirectory(expectedPath);

var slnFile = Path.Combine(_tempDirectory, "TestApp.sln");
var slnxFile = Path.Combine(_contentDirectory, "TestApp.slnx");
File.WriteAllText(slnFile, "Microsoft Visual Studio Solution File, Format Version 12.00");
File.WriteAllText(slnxFile, """
<Solution>
<Configurations>
<Configuration Name="Debug|Any CPU" />
</Configurations>
</Solution>
""");

var builder = new WebHostBuilder()
.UseTestServer()
.Configure(app => { });

builder.UseSolutionRelativeContentRoot("sub", _contentDirectory, ["*.sln", "*.slnx"]);

using var host = builder.Build();
var environment = host.Services.GetRequiredService<IWebHostEnvironment>();

Assert.Equal(expectedPath, environment.ContentRootPath);
}

[Fact]
public void UseSolutionRelativeContentRoot_WithMultipleSolutionNames_WorksWithMultipleFiles()
{
var slnFile = Path.Combine(_tempDirectory, "TestApp.sln");
var slnxFile = Path.Combine(_tempDirectory, "TestApp.slnx");
File.WriteAllText(slnFile, "Microsoft Visual Studio Solution File, Format Version 12.00");
File.WriteAllText(slnxFile, """
<Solution>
<Configurations>
<Configuration Name="Debug|Any CPU" />
</Configurations>
</Solution>
""");

var builder = new WebHostBuilder()
.UseTestServer()
.Configure(app => { });

builder.UseSolutionRelativeContentRoot("src", applicationBasePath: _tempDirectory, solutionNames: ["*.sln", "*.slnx"]);

using var host = builder.Build();
var environment = host.Services.GetRequiredService<IWebHostEnvironment>();

Assert.Equal(_contentDirectory, environment.ContentRootPath);
}

[Fact]
public void UseSolutionRelativeContentRoot_ThrowsWhenSolutionNotFound()
{
var builder = new WebHostBuilder()
.UseTestServer()
.Configure(app => { });

var exception = Assert.Throws<InvalidOperationException>(() =>
builder.UseSolutionRelativeContentRoot("src", applicationBasePath: _tempDirectory));

Assert.Contains("Solution root could not be located", exception.Message);
Assert.Contains(_tempDirectory, exception.Message);
}

[Fact]
public void UseSolutionRelativeContentRoot_WithSolutionName_SearchesParentDirectories()
{
var subDirectory = Path.Combine(_tempDirectory, "sub", "folder");
Directory.CreateDirectory(subDirectory);

var solutionFile = Path.Combine(_tempDirectory, "TestApp.slnx");
File.WriteAllText(solutionFile, """
<Solution>
<Configurations>
<Configuration Name="Debug|Any CPU" />
</Configurations>
</Solution>
""");

var builder = new WebHostBuilder()
.UseTestServer()
.Configure(app => { });

builder.UseSolutionRelativeContentRoot("src", subDirectory, "*.slnx");

using var host = builder.Build();
var environment = host.Services.GetRequiredService<IWebHostEnvironment>();

Assert.Equal(_contentDirectory, environment.ContentRootPath);
}

public void Dispose()
{
if (Directory.Exists(_tempDirectory))
{
Directory.Delete(_tempDirectory, recursive: true);
}
}
}
#pragma warning restore ASPDEPR008 // WebHost is obsolete
#pragma warning disable ASPDEPR004 // WebHostBuilder is obsolete
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using BasicWebSite;

namespace Microsoft.AspNetCore.Mvc.FunctionalTests;

public class WebApplicationFactorySlnxTests : IClassFixture<WebApplicationFactory<BasicWebSite.Startup>>, IDisposable
{
private readonly string _tempDirectory;
private readonly string _contentDirectory;

public WebApplicationFactorySlnxTests(WebApplicationFactory<BasicWebSite.Startup> factory)
{
Factory = factory;
_tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")[..8]);
_contentDirectory = Path.Combine(_tempDirectory, "BasicWebSite");

Directory.CreateDirectory(_tempDirectory);
Directory.CreateDirectory(_contentDirectory);

// Create a minimal wwwroot directory to satisfy content root expectations
var wwwrootDir = Path.Combine(_contentDirectory, "wwwroot");
Directory.CreateDirectory(wwwrootDir);
}

public WebApplicationFactory<BasicWebSite.Startup> Factory { get; }

[Fact]
public async Task WebApplicationFactory_UsesSlnxForSolutionRelativeContentRoot()
{
// Create .slnx file in temp directory
var slnxFile = Path.Combine(_tempDirectory, "TestSolution.slnx");
File.WriteAllText(slnxFile, """
<Solution>
<Configurations>
<Configuration Name="Debug|Any CPU" />
<Configuration Name="Release|Any CPU" />
</Configurations>
<Folder Name="/BasicWebSite/">
<Project Path="BasicWebSite/BasicWebSite.csproj" />
</Folder>
</Solution>
""");

var factory = Factory.WithWebHostBuilder(builder =>
{
builder.UseSolutionRelativeContentRoot("BasicWebSite", _tempDirectory, "TestSolution.slnx");
});

using var client = factory.CreateClient();

// Verify that the content root was set correctly by accessing the environment
var environment = factory.Services.GetRequiredService<IWebHostEnvironment>();
Assert.Equal(_contentDirectory, environment.ContentRootPath);
Assert.True(Directory.Exists(environment.ContentRootPath));

// Verify the factory is functional with the .slnx-resolved content root
var response = await client.GetAsync("/");
Assert.True(response.IsSuccessStatusCode);
}

public void Dispose()
{
if (Directory.Exists(_tempDirectory))
{
Directory.Delete(_tempDirectory, recursive: true);
}
}
}
Loading