diff --git a/src/Mvc/Mvc.Core/src/ApplicationParts/RelatedAssemblyAttribute.cs b/src/Mvc/Mvc.Core/src/ApplicationParts/RelatedAssemblyAttribute.cs index 5bdfb076c0..bda87a8b3e 100644 --- a/src/Mvc/Mvc.Core/src/ApplicationParts/RelatedAssemblyAttribute.cs +++ b/src/Mvc/Mvc.Core/src/ApplicationParts/RelatedAssemblyAttribute.cs @@ -17,9 +17,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] public sealed class RelatedAssemblyAttribute : Attribute { - private static readonly Func LoadFromAssemblyPathDelegate = - AssemblyLoadContext.GetLoadContext(typeof(RelatedAssemblyAttribute).Assembly).LoadFromAssemblyPath; - /// /// Initializes a new instance of . /// @@ -52,14 +49,15 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts throw new ArgumentNullException(nameof(assembly)); } - return GetRelatedAssemblies(assembly, throwOnError, File.Exists, LoadFromAssemblyPathDelegate); + var loadContext = AssemblyLoadContext.GetLoadContext(assembly) ?? AssemblyLoadContext.Default; + return GetRelatedAssemblies(assembly, throwOnError, File.Exists, new AssemblyLoadContextWrapper(loadContext)); } internal static IReadOnlyList GetRelatedAssemblies( Assembly assembly, bool throwOnError, Func fileExists, - Func loadFile) + AssemblyLoadContextWrapper assemblyLoadContext) { if (assembly == null) { @@ -95,42 +93,46 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts Resources.FormatRelatedAssemblyAttribute_AssemblyCannotReferenceSelf(nameof(RelatedAssemblyAttribute), assemblyName)); } - var relatedAssemblyName = new AssemblyName(attribute.AssemblyFileName); Assembly relatedAssembly; - try - { - // Perform a cursory check to determine if the Assembly has already been loaded - // before going to disk. In the ordinary case, related parts that are part of - // application's reference closure should already be loaded. - relatedAssembly = Assembly.Load(relatedAssemblyName); - relatedAssemblies.Add(relatedAssembly); - continue; - } - catch (IOException) - { - // The assembly isn't already loaded. Patience, we'll attempt to load it from disk next. - } - var relatedAssemblyLocation = Path.Combine(assemblyDirectory, attribute.AssemblyFileName + ".dll"); - if (!fileExists(relatedAssemblyLocation)) + if (fileExists(relatedAssemblyLocation)) { - if (throwOnError) + relatedAssembly = assemblyLoadContext.LoadFromAssemblyPath(relatedAssemblyLocation); + } + else + { + try { - throw new FileNotFoundException( - Resources.FormatRelatedAssemblyAttribute_CouldNotBeFound(attribute.AssemblyFileName, assemblyName, assemblyDirectory), - relatedAssemblyLocation); + var relatedAssemblyName = new AssemblyName(attribute.AssemblyFileName); + relatedAssembly = assemblyLoadContext.LoadFromAssemblyName(relatedAssemblyName); } - else + catch when (!throwOnError) { + // Ignore assembly load failures when throwOnError = false. continue; } } - relatedAssembly = loadFile(relatedAssemblyLocation); relatedAssemblies.Add(relatedAssembly); } return relatedAssemblies; } + + internal class AssemblyLoadContextWrapper + { + private readonly AssemblyLoadContext _loadContext; + + public AssemblyLoadContextWrapper(AssemblyLoadContext loadContext) + { + _loadContext = loadContext; + } + + public virtual Assembly LoadFromAssemblyName(AssemblyName assemblyName) + => _loadContext.LoadFromAssemblyName(assemblyName); + + public virtual Assembly LoadFromAssemblyPath(string assemblyPath) + => _loadContext.LoadFromAssemblyPath(assemblyPath); + } } } diff --git a/src/Mvc/Mvc.Core/test/ApplicationParts/RelatedAssemblyPartTest.cs b/src/Mvc/Mvc.Core/test/ApplicationParts/RelatedAssemblyPartTest.cs index 33d5e9d0e5..5e80484c66 100644 --- a/src/Mvc/Mvc.Core/test/ApplicationParts/RelatedAssemblyPartTest.cs +++ b/src/Mvc/Mvc.Core/test/ApplicationParts/RelatedAssemblyPartTest.cs @@ -1,10 +1,12 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.IO; using System.Reflection; using System.Reflection.Emit; +using System.Runtime.Loader; using Xunit; namespace Microsoft.AspNetCore.Mvc.ApplicationParts @@ -43,7 +45,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts public void GetRelatedAssemblies_ThrowsIfAssemblyCannotBeFound() { // Arrange - 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."; var assemblyPath = Path.Combine(AssemblyDirectory, "MyAssembly.dll"); var assembly = new TestAssembly { @@ -51,27 +52,32 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts }; // Act & Assert - var ex = Assert.Throws(() => RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true)); - Assert.Equal(expected, ex.Message); - Assert.Equal(Path.Combine(AssemblyDirectory, "DoesNotExist.dll"), ex.FileName); + Assert.Throws(() => RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true)); } [Fact] - public void GetRelatedAssemblies_LoadsRelatedAssembly() + public void GetRelatedAssemblies_ReadsAssemblyFromLoadContext_IfItAlreadyExists() { // Arrange - var destination = Path.Combine(AssemblyDirectory, "RelatedAssembly.dll"); + 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."; + var assemblyPath = Path.Combine(AssemblyDirectory, "MyAssembly.dll"); + var relatedAssembly = typeof(RelatedAssemblyPartTest).Assembly; var assembly = new TestAssembly { - AttributeAssembly = "RelatedAssembly", + AttributeAssembly = "RelatedAssembly" }; - var relatedAssembly = typeof(RelatedAssemblyPartTest).Assembly; - - var result = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true, file => true, file => + var loadContext = new TestableAssemblyLoadContextWrapper { - Assert.Equal(file, destination); - return relatedAssembly; - }); + Assemblies = + { + ["RelatedAssembly"] = relatedAssembly, + } + }; + + // Act + var result = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true, file => false, loadContext); + + // Assert Assert.Equal(new[] { relatedAssembly }, result); } @@ -94,5 +100,21 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts return new[] { attribute }; } } + + private class TestableAssemblyLoadContextWrapper : RelatedAssemblyAttribute.AssemblyLoadContextWrapper + { + public TestableAssemblyLoadContextWrapper() : base(AssemblyLoadContext.Default) + { + } + + public Dictionary Assemblies { get; } = new Dictionary(); + + public override Assembly LoadFromAssemblyPath(string assemblyPath) => throw new NotSupportedException(); + + public override Assembly LoadFromAssemblyName(AssemblyName assemblyName) + { + return Assemblies[assemblyName.Name]; + } + } } } diff --git a/src/ProjectTemplates/BlazorTemplates.Tests/Infrastructure/GenerateTestProps.targets b/src/ProjectTemplates/BlazorTemplates.Tests/Infrastructure/GenerateTestProps.targets index a97cf0dbd2..10ce80aec3 100644 --- a/src/ProjectTemplates/BlazorTemplates.Tests/Infrastructure/GenerateTestProps.targets +++ b/src/ProjectTemplates/BlazorTemplates.Tests/Infrastructure/GenerateTestProps.targets @@ -35,7 +35,7 @@ MicrosoftNETSdkRazorPackageVersion=$(MicrosoftNETSdkRazorPackageVersion); MicrosoftAspNetCoreAppRefPackageVersion=$(MicrosoftAspNetCoreAppRefPackageVersion); MicrosoftAspNetCoreAppRuntimePackageVersion=@(_RuntimePackageVersionInfo->'%(PackageVersion)'); - SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers); + SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers.Trim()); DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework); RepoRoot=$(RepoRoot); Configuration=$(Configuration); diff --git a/src/ProjectTemplates/Shared/AspNetProcess.cs b/src/ProjectTemplates/Shared/AspNetProcess.cs index 7412e3b369..afc34a3f30 100644 --- a/src/ProjectTemplates/Shared/AspNetProcess.cs +++ b/src/ProjectTemplates/Shared/AspNetProcess.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -59,9 +60,21 @@ namespace Templates.Test.Helpers output.WriteLine("Running ASP.NET Core application..."); - var arguments = published ? $"exec {dllPath}" : "run --no-build"; + string process; + string arguments; + if (published) + { + // When publishingu used the app host to run the app. This makes it easy to consistently run for regular and single-file publish + process = OperatingSystem.IsWindows() ? dllPath + ".exe" : dllPath; + arguments = null; + } + else + { + process = DotNetMuxer.MuxerPathOrDefault(); + arguments = "run --no-build"; + } - logger?.LogInformation($"AspNetProcess - process: {DotNetMuxer.MuxerPathOrDefault()} arguments: {arguments}"); + logger?.LogInformation($"AspNetProcess - process: {process} arguments: {arguments}"); var finalEnvironmentVariables = new Dictionary(environmentVariables) { @@ -69,7 +82,7 @@ namespace Templates.Test.Helpers ["ASPNETCORE_Kestrel__Certificates__Default__Password"] = _developmentCertificate.CertificatePassword, }; - Process = ProcessEx.Run(output, workingDirectory, DotNetMuxer.MuxerPathOrDefault(), arguments, envVars: finalEnvironmentVariables); + Process = ProcessEx.Run(output, workingDirectory, process, arguments, envVars: finalEnvironmentVariables); logger?.LogInformation("AspNetProcess - process started"); diff --git a/src/ProjectTemplates/Shared/Project.cs b/src/ProjectTemplates/Shared/Project.cs index cb28d482f4..fbfeee80c4 100644 --- a/src/ProjectTemplates/Shared/Project.cs +++ b/src/ProjectTemplates/Shared/Project.cs @@ -110,14 +110,16 @@ namespace Templates.Test.Helpers } } - internal async Task RunDotNetPublishAsync(IDictionary packageOptions = null, string additionalArgs = null) + internal async Task RunDotNetPublishAsync(IDictionary packageOptions = null, string additionalArgs = null, bool noRestore = true) { Output.WriteLine("Publishing ASP.NET Core application..."); // Avoid restoring as part of build or publish. These projects should have already restored as part of running dotnet new. Explicitly disabling restore // should avoid any global contention and we can execute a build or publish in a lock-free way - using var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), $"publish --no-restore -c Release /bl {additionalArgs}", packageOptions); + var restoreArgs = noRestore ? "--no-restore" : null; + + using var result = ProcessEx.Run(Output, TemplateOutputDir, DotNetMuxer.MuxerPathOrDefault(), $"publish {restoreArgs} -c Release /bl {additionalArgs}", packageOptions); await result.Exited; CaptureBinLogOnFailure(result); return new ProcessResult(result); @@ -188,7 +190,7 @@ namespace Templates.Test.Helpers ["ASPNETCORE_Logging__Console__FormatterOptions__IncludeScopes"] = "true", }; - var projectDll = $"{ProjectName}.dll"; + var projectDll = Path.Combine(TemplatePublishDir, ProjectName); return new AspNetProcess(Output, TemplatePublishDir, projectDll, environment, published: true, hasListeningUri: hasListeningUri); } diff --git a/src/ProjectTemplates/test/Infrastructure/GenerateTestProps.targets b/src/ProjectTemplates/test/Infrastructure/GenerateTestProps.targets index a97cf0dbd2..10ce80aec3 100644 --- a/src/ProjectTemplates/test/Infrastructure/GenerateTestProps.targets +++ b/src/ProjectTemplates/test/Infrastructure/GenerateTestProps.targets @@ -35,7 +35,7 @@ MicrosoftNETSdkRazorPackageVersion=$(MicrosoftNETSdkRazorPackageVersion); MicrosoftAspNetCoreAppRefPackageVersion=$(MicrosoftAspNetCoreAppRefPackageVersion); MicrosoftAspNetCoreAppRuntimePackageVersion=@(_RuntimePackageVersionInfo->'%(PackageVersion)'); - SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers); + SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers.Trim()); DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework); RepoRoot=$(RepoRoot); Configuration=$(Configuration); diff --git a/src/ProjectTemplates/test/MvcTemplateTest.cs b/src/ProjectTemplates/test/MvcTemplateTest.cs index 15a1c87300..9136d26717 100644 --- a/src/ProjectTemplates/test/MvcTemplateTest.cs +++ b/src/ProjectTemplates/test/MvcTemplateTest.cs @@ -223,6 +223,77 @@ namespace Templates.Test } } + [ConditionalFact] + [SkipOnHelix("cert failure", Queues = "OSX.1014.Amd64;OSX.1014.Amd64.Open")] + public async Task MvcTemplate_SingleFileExe() + { + // This test verifies publishing an MVC app as a single file exe works. We'll limit testing + // this to a few operating systems to make our lives easier. + string runtimeIdentifer; + if (OperatingSystem.IsWindows()) + { + runtimeIdentifer = "win-x64"; + } + else if (OperatingSystem.IsLinux()) + { + runtimeIdentifer = "linux-x64"; + } + else + { + return; + } + + Project = await ProjectFactory.GetOrCreateProject("mvcindividualuld", Output); + Project.RuntimeIdentifier = runtimeIdentifer; + + var createResult = await Project.RunDotNetNewAsync("mvc", auth: "Individual", useLocalDB: true); + Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", Project, createResult)); + + var publishResult = await Project.RunDotNetPublishAsync(additionalArgs: $"/p:PublishSingleFile=true -r {runtimeIdentifer}", noRestore: false); + Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", Project, publishResult)); + + var pages = new[] + { + new Page + { + // Verify a view from the app works + Url = PageUrls.HomeUrl, + Links = new [] + { + PageUrls.HomeUrl, + PageUrls.RegisterUrl, + PageUrls.LoginUrl, + PageUrls.HomeUrl, + PageUrls.PrivacyUrl, + PageUrls.DocsUrl, + PageUrls.PrivacyUrl + } + }, + new Page + { + // Verify a view from a RCL (in this case IdentityUI) works + Url = PageUrls.RegisterUrl, + Links = new [] + { + PageUrls.HomeUrl, + PageUrls.RegisterUrl, + PageUrls.LoginUrl, + PageUrls.HomeUrl, + PageUrls.PrivacyUrl, + PageUrls.ExternalArticle, + PageUrls.PrivacyUrl + } + }, + }; + + using var aspNetProcess = Project.StartPublishedProjectAsync(); + Assert.False( + aspNetProcess.Process.HasExited, + ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", Project, aspNetProcess.Process)); + + await aspNetProcess.AssertPagesOk(pages); + } + [Fact] [QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/23993")] public async Task MvcTemplate_RazorRuntimeCompilation_BuildsAndPublishes()