Skip to content

Commit ad8a927

Browse files
committed
Changes per discussion. Add a test
1 parent 2f0cf8d commit ad8a927

File tree

7 files changed

+158
-48
lines changed

7 files changed

+158
-48
lines changed

src/Mvc/Mvc.Core/src/ApplicationParts/RelatedAssemblyAttribute.cs

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts
1717
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
1818
public sealed class RelatedAssemblyAttribute : Attribute
1919
{
20-
private static readonly Func<string, Assembly> LoadFromAssemblyPathDelegate =
21-
AssemblyLoadContext.GetLoadContext(typeof(RelatedAssemblyAttribute).Assembly).LoadFromAssemblyPath;
22-
2320
/// <summary>
2421
/// Initializes a new instance of <see cref="RelatedAssemblyAttribute"/>.
2522
/// </summary>
@@ -52,14 +49,15 @@ public static IReadOnlyList<Assembly> GetRelatedAssemblies(Assembly assembly, bo
5249
throw new ArgumentNullException(nameof(assembly));
5350
}
5451

55-
return GetRelatedAssemblies(assembly, throwOnError, File.Exists, LoadFromAssemblyPathDelegate);
52+
var loadContext = AssemblyLoadContext.GetLoadContext(assembly) ?? AssemblyLoadContext.Default;
53+
return GetRelatedAssemblies(assembly, throwOnError, File.Exists, new AssemblyLoadContextWrapper(loadContext));
5654
}
5755

5856
internal static IReadOnlyList<Assembly> GetRelatedAssemblies(
5957
Assembly assembly,
6058
bool throwOnError,
6159
Func<string, bool> fileExists,
62-
Func<string, Assembly> loadFile)
60+
AssemblyLoadContextWrapper assemblyLoadContext)
6361
{
6462
if (assembly == null)
6563
{
@@ -95,42 +93,46 @@ internal static IReadOnlyList<Assembly> GetRelatedAssemblies(
9593
Resources.FormatRelatedAssemblyAttribute_AssemblyCannotReferenceSelf(nameof(RelatedAssemblyAttribute), assemblyName));
9694
}
9795

98-
var relatedAssemblyName = new AssemblyName(attribute.AssemblyFileName);
9996
Assembly relatedAssembly;
100-
try
97+
var relatedAssemblyLocation = Path.Combine(assemblyDirectory, attribute.AssemblyFileName + ".dll");
98+
if (fileExists(relatedAssemblyLocation))
10199
{
102-
// Perform a cursory check to determine if the Assembly has already been loaded
103-
// before going to disk. In the ordinary case, related parts that are part of
104-
// application's reference closure should already be loaded.
105-
relatedAssembly = Assembly.Load(relatedAssemblyName);
106-
relatedAssemblies.Add(relatedAssembly);
107-
continue;
100+
relatedAssembly = assemblyLoadContext.LoadFromAssemblyPath(relatedAssemblyLocation);
108101
}
109-
catch (IOException)
102+
else
110103
{
111-
// The assembly isn't already loaded. Patience, we'll attempt to load it from disk next.
112-
}
113-
114-
var relatedAssemblyLocation = Path.Combine(assemblyDirectory, attribute.AssemblyFileName + ".dll");
115-
if (!fileExists(relatedAssemblyLocation))
116-
{
117-
if (throwOnError)
104+
try
118105
{
119-
throw new FileNotFoundException(
120-
Resources.FormatRelatedAssemblyAttribute_CouldNotBeFound(attribute.AssemblyFileName, assemblyName, assemblyDirectory),
121-
relatedAssemblyLocation);
106+
var relatedAssemblyName = new AssemblyName(attribute.AssemblyFileName);
107+
relatedAssembly = assemblyLoadContext.LoadFromAssemblyName(relatedAssemblyName);
122108
}
123-
else
109+
catch when (!throwOnError)
124110
{
111+
// Ignore assembly load failures when throwOnError = false.
125112
continue;
126113
}
127114
}
128115

129-
relatedAssembly = loadFile(relatedAssemblyLocation);
130116
relatedAssemblies.Add(relatedAssembly);
131117
}
132118

133119
return relatedAssemblies;
134120
}
121+
122+
internal class AssemblyLoadContextWrapper
123+
{
124+
private readonly AssemblyLoadContext _loadContext;
125+
126+
public AssemblyLoadContextWrapper(AssemblyLoadContext loadContext)
127+
{
128+
_loadContext = loadContext;
129+
}
130+
131+
public virtual Assembly LoadFromAssemblyName(AssemblyName assemblyName)
132+
=> _loadContext.LoadFromAssemblyName(assemblyName);
133+
134+
public virtual Assembly LoadFromAssemblyPath(string assemblyPath)
135+
=> _loadContext.LoadFromAssemblyPath(assemblyPath);
136+
}
135137
}
136138
}

src/Mvc/Mvc.Core/test/ApplicationParts/RelatedAssemblyPartTest.cs

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.IO;
67
using System.Reflection;
78
using System.Reflection.Emit;
9+
using System.Runtime.Loader;
810
using Xunit;
911

1012
namespace Microsoft.AspNetCore.Mvc.ApplicationParts
@@ -43,35 +45,39 @@ public void GetRelatedAssemblies_ThrowsIfRelatedAttributeReferencesSelf()
4345
public void GetRelatedAssemblies_ThrowsIfAssemblyCannotBeFound()
4446
{
4547
// Arrange
46-
var expected = $"Related assembly 'DoesNotExist' specified by assembly 'MyAssembly' could not be found in the directory {AssemblyDirectory}. Related assemblies must be co-located with the specifying assemblies.";
4748
var assemblyPath = Path.Combine(AssemblyDirectory, "MyAssembly.dll");
4849
var assembly = new TestAssembly
4950
{
5051
AttributeAssembly = "DoesNotExist"
5152
};
5253

5354
// Act & Assert
54-
var ex = Assert.Throws<FileNotFoundException>(() => RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true));
55-
Assert.Equal(expected, ex.Message);
56-
Assert.Equal(Path.Combine(AssemblyDirectory, "DoesNotExist.dll"), ex.FileName);
55+
Assert.Throws<FileNotFoundException>(() => RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true));
5756
}
5857

5958
[Fact]
60-
public void GetRelatedAssemblies_LoadsRelatedAssembly()
59+
public void GetRelatedAssemblies_ReadsAssemblyFromLoadContext_IfItAlreadyExists()
6160
{
6261
// Arrange
63-
var destination = Path.Combine(AssemblyDirectory, "RelatedAssembly.dll");
62+
var expected = $"Related assembly 'DoesNotExist' specified by assembly 'MyAssembly' could not be found in the directory {AssemblyDirectory}. Related assemblies must be co-located with the specifying assemblies.";
63+
var assemblyPath = Path.Combine(AssemblyDirectory, "MyAssembly.dll");
64+
var relatedAssembly = typeof(RelatedAssemblyPartTest).Assembly;
6465
var assembly = new TestAssembly
6566
{
66-
AttributeAssembly = "RelatedAssembly",
67+
AttributeAssembly = "RelatedAssembly"
6768
};
68-
var relatedAssembly = typeof(RelatedAssemblyPartTest).Assembly;
69-
70-
var result = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true, file => true, file =>
69+
var loadContext = new TestableAssemblyLoadContextWrapper
7170
{
72-
Assert.Equal(file, destination);
73-
return relatedAssembly;
74-
});
71+
Assemblies =
72+
{
73+
["RelatedAssembly"] = relatedAssembly,
74+
}
75+
};
76+
77+
// Act
78+
var result = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true, file => false, loadContext);
79+
80+
// Assert
7581
Assert.Equal(new[] { relatedAssembly }, result);
7682
}
7783

@@ -94,5 +100,21 @@ public override object[] GetCustomAttributes(Type attributeType, bool inherit)
94100
return new[] { attribute };
95101
}
96102
}
103+
104+
private class TestableAssemblyLoadContextWrapper : RelatedAssemblyAttribute.AssemblyLoadContextWrapper
105+
{
106+
public TestableAssemblyLoadContextWrapper() : base(AssemblyLoadContext.Default)
107+
{
108+
}
109+
110+
public Dictionary<string, Assembly> Assemblies { get; } = new Dictionary<string, Assembly>();
111+
112+
public override Assembly LoadFromAssemblyPath(string assemblyPath) => throw new NotSupportedException();
113+
114+
public override Assembly LoadFromAssemblyName(AssemblyName assemblyName)
115+
{
116+
return Assemblies[assemblyName.Name];
117+
}
118+
}
97119
}
98120
}

src/ProjectTemplates/BlazorTemplates.Tests/Infrastructure/GenerateTestProps.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
MicrosoftNETSdkRazorPackageVersion=$(MicrosoftNETSdkRazorPackageVersion);
3636
MicrosoftAspNetCoreAppRefPackageVersion=$(MicrosoftAspNetCoreAppRefPackageVersion);
3737
MicrosoftAspNetCoreAppRuntimePackageVersion=@(_RuntimePackageVersionInfo->'%(PackageVersion)');
38-
SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers);
38+
SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers.Trim());
3939
DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework);
4040
RepoRoot=$(RepoRoot);
4141
Configuration=$(Configuration);

src/ProjectTemplates/Shared/AspNetProcess.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7+
using System.IO;
78
using System.Linq;
89
using System.Net;
910
using System.Net.Http;
@@ -59,17 +60,29 @@ public AspNetProcess(
5960

6061
output.WriteLine("Running ASP.NET Core application...");
6162

62-
var arguments = published ? $"exec {dllPath}" : "run --no-build";
63+
string process;
64+
string arguments;
65+
if (published)
66+
{
67+
// When publishingu used the app host to run the app. This makes it easy to consistently run for regular and single-file publish
68+
process = OperatingSystem.IsWindows() ? dllPath + ".exe" : dllPath;
69+
arguments = null;
70+
}
71+
else
72+
{
73+
process = DotNetMuxer.MuxerPathOrDefault();
74+
arguments = "run --no-build";
75+
}
6376

64-
logger?.LogInformation($"AspNetProcess - process: {DotNetMuxer.MuxerPathOrDefault()} arguments: {arguments}");
77+
logger?.LogInformation($"AspNetProcess - process: {process} arguments: {arguments}");
6578

6679
var finalEnvironmentVariables = new Dictionary<string, string>(environmentVariables)
6780
{
6881
["ASPNETCORE_Kestrel__Certificates__Default__Path"] = _developmentCertificate.CertificatePath,
6982
["ASPNETCORE_Kestrel__Certificates__Default__Password"] = _developmentCertificate.CertificatePassword,
7083
};
7184

72-
Process = ProcessEx.Run(output, workingDirectory, DotNetMuxer.MuxerPathOrDefault(), arguments, envVars: finalEnvironmentVariables);
85+
Process = ProcessEx.Run(output, workingDirectory, process, arguments, envVars: finalEnvironmentVariables);
7386

7487
logger?.LogInformation("AspNetProcess - process started");
7588

src/ProjectTemplates/Shared/Project.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,16 @@ internal async Task<ProcessResult> RunDotNetNewAsync(
110110
}
111111
}
112112

113-
internal async Task<ProcessResult> RunDotNetPublishAsync(IDictionary<string, string> packageOptions = null, string additionalArgs = null)
113+
internal async Task<ProcessResult> RunDotNetPublishAsync(IDictionary<string, string> packageOptions = null, string additionalArgs = null, bool noRestore = true)
114114
{
115115
Output.WriteLine("Publishing ASP.NET Core application...");
116116

117117
// Avoid restoring as part of build or publish. These projects should have already restored as part of running dotnet new. Explicitly disabling restore
118118
// should avoid any global contention and we can execute a build or publish in a lock-free way
119119

120-
using var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), $"publish --no-restore -c Release /bl {additionalArgs}", packageOptions);
120+
var restoreArgs = noRestore ? "--no-restore" : null;
121+
122+
using var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), $"publish {restoreArgs} -c Release /bl {additionalArgs}", packageOptions);
121123
await result.Exited;
122124
CaptureBinLogOnFailure(result);
123125
return new ProcessResult(result);
@@ -188,7 +190,7 @@ internal AspNetProcess StartPublishedProjectAsync(bool hasListeningUri = true)
188190
["ASPNETCORE_Logging__Console__FormatterOptions__IncludeScopes"] = "true",
189191
};
190192

191-
var projectDll = $"{ProjectName}.dll";
193+
var projectDll = Path.Combine(TemplatePublishDir, ProjectName);
192194
return new AspNetProcess(Output, TemplatePublishDir, projectDll, environment, published: true, hasListeningUri: hasListeningUri);
193195
}
194196

src/ProjectTemplates/test/Infrastructure/GenerateTestProps.targets

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
MicrosoftNETSdkRazorPackageVersion=$(MicrosoftNETSdkRazorPackageVersion);
3636
MicrosoftAspNetCoreAppRefPackageVersion=$(MicrosoftAspNetCoreAppRefPackageVersion);
3737
MicrosoftAspNetCoreAppRuntimePackageVersion=@(_RuntimePackageVersionInfo->'%(PackageVersion)');
38-
SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers);
38+
SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers.Trim());
3939
DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework);
4040
RepoRoot=$(RepoRoot);
4141
Configuration=$(Configuration);

src/ProjectTemplates/test/MvcTemplateTest.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,77 @@ public async Task MvcTemplate_IndividualAuth(bool useLocalDB)
223223
}
224224
}
225225

226+
[ConditionalFact]
227+
[SkipOnHelix("cert failure", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")]
228+
public async Task MvcTemplate_SingleFileExe()
229+
{
230+
// This test verifies publishing an MVC app as a single file exe works. We'll limit testing
231+
// this to a few operating systems to make our lives easier.
232+
string runtimeIdentifer;
233+
if (OperatingSystem.IsWindows())
234+
{
235+
runtimeIdentifer = "win-x64";
236+
}
237+
else if (OperatingSystem.IsLinux())
238+
{
239+
runtimeIdentifer = "linux-x64";
240+
}
241+
else
242+
{
243+
return;
244+
}
245+
246+
Project = await ProjectFactory.GetOrCreateProject("mvcindividualuld", Output);
247+
Project.RuntimeIdentifier = runtimeIdentifer;
248+
249+
var createResult = await Project.RunDotNetNewAsync("mvc", auth: "Individual", useLocalDB: true);
250+
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult));
251+
252+
var publishResult = await Project.RunDotNetPublishAsync(additionalArgs: $"/p:PublishSingleFile=true -r {runtimeIdentifer}", noRestore: false);
253+
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult));
254+
255+
var pages = new[]
256+
{
257+
new Page
258+
{
259+
// Verify a view from the app works
260+
Url = PageUrls.HomeUrl,
261+
Links = new []
262+
{
263+
PageUrls.HomeUrl,
264+
PageUrls.RegisterUrl,
265+
PageUrls.LoginUrl,
266+
PageUrls.HomeUrl,
267+
PageUrls.PrivacyUrl,
268+
PageUrls.DocsUrl,
269+
PageUrls.PrivacyUrl
270+
}
271+
},
272+
new Page
273+
{
274+
// Verify a view from a RCL (in this case IdentityUI) works
275+
Url = PageUrls.RegisterUrl,
276+
Links = new []
277+
{
278+
PageUrls.HomeUrl,
279+
PageUrls.RegisterUrl,
280+
PageUrls.LoginUrl,
281+
PageUrls.HomeUrl,
282+
PageUrls.PrivacyUrl,
283+
PageUrls.ExternalArticle,
284+
PageUrls.PrivacyUrl
285+
}
286+
},
287+
};
288+
289+
using var aspNetProcess = Project.StartPublishedProjectAsync();
290+
Assert.False(
291+
aspNetProcess.Process.HasExited,
292+
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", Project, aspNetProcess.Process));
293+
294+
await aspNetProcess.AssertPagesOk(pages);
295+
}
296+
226297
[Fact]
227298
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23993")]
228299
public async Task MvcTemplate_RazorRuntimeCompilation_BuildsAndPublishes()

0 commit comments

Comments
 (0)