-
Notifications
You must be signed in to change notification settings - Fork 10.4k
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -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> | ||||
|
@@ -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> | ||||
|
@@ -144,19 +158,45 @@ public static IWebHostBuilder UseSolutionRelativeContentRoot( | |||
this IWebHostBuilder builder, | ||||
string solutionRelativePath, | ||||
string applicationBasePath, | ||||
string solutionName = "*.sln") | ||||
string solutionName) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I think we can remove the It's really unfortunate that we have all these overloads of just |
||||
{ | ||||
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", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Is this still necessary considering it's now the only overload of UseSolutionRelativeContentRoot that has a default parameter? |
||||
public static IWebHostBuilder UseSolutionRelativeContentRoot( | ||||
this IWebHostBuilder builder, | ||||
string solutionRelativePath, | ||||
string applicationBasePath, | ||||
ReadOnlySpan<string> solutionNames = default) | ||||
kimsey0 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think |
||||
{ | ||||
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) | ||||
kimsey0 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be a LINQ query instead if you prefer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the foreach is fine. |
||||
{ | ||||
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; | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
// 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; | ||
|
||
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); | ||
} | ||
} | ||
} |
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); | ||
} | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These changes should go in the
PublicAPI.Unshipped.txt
file. For the cases where you removed the default value for the optionalsolutionName
parameter, you can prefix the old API with*REMOVED*
and then re-add it without the default value. You can take a look at the OpenApi PublicAPI.Unshipped.txt for reference.