Skip to content
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
52 changes: 37 additions & 15 deletions src/Mvc/Mvc.Core/src/ApplicationParts/RelatedAssemblyAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,11 +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.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.AspNetCore.Mvc.Core;

namespace Microsoft.AspNetCore.Mvc.ApplicationParts
Expand All @@ -16,8 +17,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class RelatedAssemblyAttribute : Attribute
{
private static readonly Func<string, Assembly> AssemblyLoadFileDelegate = Assembly.LoadFile;

/// <summary>
/// Initializes a new instance of <see cref="RelatedAssemblyAttribute"/>.
/// </summary>
Expand Down Expand Up @@ -50,14 +49,15 @@ public static IReadOnlyList<Assembly> GetRelatedAssemblies(Assembly assembly, bo
throw new ArgumentNullException(nameof(assembly));
}

return GetRelatedAssemblies(assembly, throwOnError, File.Exists, AssemblyLoadFileDelegate);
var loadContext = AssemblyLoadContext.GetLoadContext(assembly) ?? AssemblyLoadContext.Default;
return GetRelatedAssemblies(assembly, throwOnError, File.Exists, new AssemblyLoadContextWrapper(loadContext));
}

internal static IReadOnlyList<Assembly> GetRelatedAssemblies(
Assembly assembly,
bool throwOnError,
Func<string, bool> fileExists,
Func<string, Assembly> loadFile)
AssemblyLoadContextWrapper assemblyLoadContext)
{
if (assembly == null)
{
Expand All @@ -66,7 +66,7 @@ internal static IReadOnlyList<Assembly> GetRelatedAssemblies(

// MVC will specifically look for related parts in the same physical directory as the assembly.
// No-op if the assembly does not have a location.
if (assembly.IsDynamic || string.IsNullOrEmpty(assembly.Location))
if (assembly.IsDynamic)
{
return Array.Empty<Assembly>();
}
Expand All @@ -78,8 +78,10 @@ internal static IReadOnlyList<Assembly> GetRelatedAssemblies(
}

var assemblyName = assembly.GetName().Name;
var assemblyLocation = assembly.Location;
var assemblyDirectory = Path.GetDirectoryName(assemblyLocation);
// Assembly.Location may be null for a single-file exe. In this case, attempt to look for related parts in the app's base directory
var assemblyDirectory = string.IsNullOrEmpty(assembly.Location) ?
AppContext.BaseDirectory :
Path.GetDirectoryName(assembly.Location);

var relatedAssemblies = new List<Assembly>();
for (var i = 0; i < attributes.Length; i++)
Expand All @@ -91,26 +93,46 @@ internal static IReadOnlyList<Assembly> GetRelatedAssemblies(
Resources.FormatRelatedAssemblyAttribute_AssemblyCannotReferenceSelf(nameof(RelatedAssemblyAttribute), assemblyName));
}

Assembly relatedAssembly;
var relatedAssemblyLocation = Path.Combine(assemblyDirectory, attribute.AssemblyFileName + ".dll");
if (!fileExists(relatedAssemblyLocation))
if (fileExists(relatedAssemblyLocation))
{
relatedAssembly = assemblyLoadContext.LoadFromAssemblyPath(relatedAssemblyLocation);
}
else
{
if (throwOnError)
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;
}
}

var 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);
}
}
}
50 changes: 36 additions & 14 deletions src/Mvc/Mvc.Core/test/ApplicationParts/RelatedAssemblyPartTest.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -43,35 +45,39 @@ public void GetRelatedAssemblies_ThrowsIfRelatedAttributeReferencesSelf()
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
{
AttributeAssembly = "DoesNotExist"
};

// Act & Assert
var ex = Assert.Throws<FileNotFoundException>(() => RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true));
Assert.Equal(expected, ex.Message);
Assert.Equal(Path.Combine(AssemblyDirectory, "DoesNotExist.dll"), ex.FileName);
Assert.Throws<FileNotFoundException>(() => 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);
}

Expand All @@ -94,5 +100,21 @@ public override object[] GetCustomAttributes(Type attributeType, bool inherit)
return new[] { attribute };
}
}

private class TestableAssemblyLoadContextWrapper : RelatedAssemblyAttribute.AssemblyLoadContextWrapper
{
public TestableAssemblyLoadContextWrapper() : base(AssemblyLoadContext.Default)
{
}

public Dictionary<string, Assembly> Assemblies { get; } = new Dictionary<string, Assembly>();

public override Assembly LoadFromAssemblyPath(string assemblyPath) => throw new NotSupportedException();

public override Assembly LoadFromAssemblyName(AssemblyName assemblyName)
{
return Assemblies[assemblyName.Name];
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
MicrosoftNETSdkRazorPackageVersion=$(MicrosoftNETSdkRazorPackageVersion);
MicrosoftAspNetCoreAppRefPackageVersion=$(MicrosoftAspNetCoreAppRefPackageVersion);
MicrosoftAspNetCoreAppRuntimePackageVersion=@(_RuntimePackageVersionInfo->'%(PackageVersion)');
SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers);
SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers.Trim());
DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework);
RepoRoot=$(RepoRoot);
Configuration=$(Configuration);
Expand Down
28 changes: 25 additions & 3 deletions src/ProjectTemplates/Shared/AspNetProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,6 +42,7 @@ public AspNetProcess(
IDictionary<string, string> environmentVariables,
bool published,
bool hasListeningUri = true,
bool usePublishedAppHost = false,
ILogger logger = null)
{
_developmentCertificate = DevelopmentCertificate.Create(workingDirectory);
Expand All @@ -59,17 +61,37 @@ public AspNetProcess(

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

var arguments = published ? $"exec {dllPath}" : "run --no-build";
string process;
string arguments;
if (published)
{
if (usePublishedAppHost)
{
// When publishingu used the app host to run the app. This makes it easy to consistently run for regular and single-file publish
process = Path.ChangeExtension(dllPath, OperatingSystem.IsWindows() ? ".exe" : null);
arguments = null;
}
else
{
process = DotNetMuxer.MuxerPathOrDefault();
arguments = $"exec {dllPath}";
}
}
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<string, string>(environmentVariables)
{
["ASPNETCORE_Kestrel__Certificates__Default__Path"] = _developmentCertificate.CertificatePath,
["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");

Expand Down
12 changes: 7 additions & 5 deletions src/ProjectTemplates/Shared/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,16 @@ internal async Task<ProcessResult> RunDotNetNewAsync(
}
}

internal async Task<ProcessResult> RunDotNetPublishAsync(IDictionary<string, string> packageOptions = null, string additionalArgs = null)
internal async Task<ProcessResult> RunDotNetPublishAsync(IDictionary<string, string> 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);
Expand Down Expand Up @@ -177,7 +179,7 @@ internal AspNetProcess StartBuiltProjectAsync(bool hasListeningUri = true, ILogg
return new AspNetProcess(Output, TemplateOutputDir, projectDll, environment, published: false, hasListeningUri: hasListeningUri, logger: logger);
}

internal AspNetProcess StartPublishedProjectAsync(bool hasListeningUri = true)
internal AspNetProcess StartPublishedProjectAsync(bool hasListeningUri = true, bool usePublishedAppHost = false)
{
var environment = new Dictionary<string, string>
{
Expand All @@ -188,8 +190,8 @@ internal AspNetProcess StartPublishedProjectAsync(bool hasListeningUri = true)
["ASPNETCORE_Logging__Console__FormatterOptions__IncludeScopes"] = "true",
};

var projectDll = $"{ProjectName}.dll";
return new AspNetProcess(Output, TemplatePublishDir, projectDll, environment, published: true, hasListeningUri: hasListeningUri);
var projectDll = Path.Combine(TemplatePublishDir, $"{ProjectName}.dll");
return new AspNetProcess(Output, TemplatePublishDir, projectDll, environment, published: true, hasListeningUri: hasListeningUri, usePublishedAppHost: usePublishedAppHost);
}

internal async Task<ProcessResult> RunDotNetEfCreateMigrationAsync(string migrationName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
MicrosoftNETSdkRazorPackageVersion=$(MicrosoftNETSdkRazorPackageVersion);
MicrosoftAspNetCoreAppRefPackageVersion=$(MicrosoftAspNetCoreAppRefPackageVersion);
MicrosoftAspNetCoreAppRuntimePackageVersion=@(_RuntimePackageVersionInfo->'%(PackageVersion)');
SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers);
SupportedRuntimeIdentifiers=$(SupportedRuntimeIdentifiers.Trim());
DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework);
RepoRoot=$(RepoRoot);
Configuration=$(Configuration);
Expand Down
71 changes: 71 additions & 0 deletions src/ProjectTemplates/test/MvcTemplateTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,77 @@ public async Task MvcTemplate_IndividualAuth(bool useLocalDB)
}
}

[ConditionalFact(Skip = "https://github.com/dotnet/aspnetcore/issues/25103")]
[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("mvcsinglefileexe", 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(usePublishedAppHost: true);
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()
Expand Down
Loading