Improve assembly resolution. Standalone hosting now works again.

This commit is contained in:
Steve Sanderson 2017-12-11 21:06:37 +00:00
parent 9ec79ae9f7
commit aff369e86d
9 changed files with 121 additions and 145 deletions

View File

@ -16,11 +16,9 @@ namespace Microsoft.Blazor.Server.Test
[Fact] [Fact]
public void RootDirContainsOnlyBinDir() public void RootDirContainsOnlyBinDir()
{ {
var (entrypoint, entrypointData) = GetBclAssemblyForTest("mscorlib");
var provider = new ReferencedAssemblyFileProvider( var provider = new ReferencedAssemblyFileProvider(
entrypoint, "mscorlib",
entrypointData, new ReferencedAssemblyResolver(MonoStaticFileProvider.BclFiles, string.Empty));
MonoStaticFileProvider.BclFiles);
Assert.Collection(provider.GetDirectoryContents("/"), item => Assert.Collection(provider.GetDirectoryContents("/"), item =>
{ {
Assert.Equal("/_bin", item.PhysicalPath); Assert.Equal("/_bin", item.PhysicalPath);
@ -31,11 +29,9 @@ namespace Microsoft.Blazor.Server.Test
[Fact] [Fact]
public void FindsReferencedAssemblyGraphSimple() public void FindsReferencedAssemblyGraphSimple()
{ {
var (entrypoint, entrypointData) = GetBclAssemblyForTest("System.Linq.Expressions");
var provider = new ReferencedAssemblyFileProvider( var provider = new ReferencedAssemblyFileProvider(
entrypoint, "System.Linq.Expressions",
entrypointData, new ReferencedAssemblyResolver(MonoStaticFileProvider.BclFiles, string.Empty));
MonoStaticFileProvider.BclFiles);
var contents = provider.GetDirectoryContents("/_bin").OrderBy(i => i.Name).ToList(); var contents = provider.GetDirectoryContents("/_bin").OrderBy(i => i.Name).ToList();
Assert.Collection(contents, Assert.Collection(contents,
item => { Assert.Equal("/_bin/mscorlib.dll", item.PhysicalPath); }, item => { Assert.Equal("/_bin/mscorlib.dll", item.PhysicalPath); },
@ -48,11 +44,12 @@ namespace Microsoft.Blazor.Server.Test
public void FindsReferencedAssemblyGraphRealistic() public void FindsReferencedAssemblyGraphRealistic()
{ {
// Arrange // Arrange
var standaloneAppAssemblyLocation = typeof(StandaloneApp.Program).Assembly.Location; var standaloneAppAssembly = typeof(StandaloneApp.Program).Assembly;
var provider = new ReferencedAssemblyFileProvider( var provider = new ReferencedAssemblyFileProvider(
AssemblyDefinition.ReadAssembly(standaloneAppAssemblyLocation), standaloneAppAssembly.GetName().Name,
File.ReadAllBytes(standaloneAppAssemblyLocation), new ReferencedAssemblyResolver(
MonoStaticFileProvider.BclFiles); MonoStaticFileProvider.BclFiles,
Path.GetDirectoryName(standaloneAppAssembly.Location)));
var expectedContents = new[] var expectedContents = new[]
{ {
/* /*

View File

@ -25,7 +25,7 @@ namespace HostedInAspNet.Server
app.UseBlazorDevelopmentServer("../HostedInAspNet.Client"); app.UseBlazorDevelopmentServer("../HostedInAspNet.Client");
} }
app.UseBlazor(clientAssembly: typeof(Client.Program).Assembly); app.UseBlazor(clientAssemblyPath: typeof(Client.Program).Assembly.Location);
} }
} }
} }

View File

@ -3,9 +3,6 @@
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using MonoSanityClient;
using System.IO;
using System.Net.Mime;
namespace MonoSanity namespace MonoSanity
{ {
@ -15,7 +12,7 @@ namespace MonoSanity
{ {
app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage();
app.UseFileServer(); app.UseFileServer();
app.UseBlazor(clientAssembly: typeof(MonoSanityClient.Examples).Assembly); app.UseBlazor(clientAssemblyPath: typeof(MonoSanityClient.Examples).Assembly.Location);
} }
} }
} }

View File

@ -6,6 +6,7 @@
</head> </head>
<body> <body>
<h1>Hello</h1> <h1>Hello</h1>
<script src="/_framework/blazor.js"></script> <script src="/_framework/blazor.js" main="StandaloneApp.dll"
references="Microsoft.Blazor.dll, netstandard.dll"></script>
</body> </body>
</html> </html>

View File

@ -21,7 +21,7 @@ namespace Microsoft.Blazor.DevHost.Server
{ {
app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage();
app.UseBlazorDevelopmentServer("."); app.UseBlazorDevelopmentServer(".");
app.UseBlazor(clientAssembly: FindClientAssembly(app)); app.UseBlazor(clientAssemblyPath: FindClientAssembly(app).Location);
} }
private static Assembly FindClientAssembly(IApplicationBuilder app) private static Assembly FindClientAssembly(IApplicationBuilder app)

View File

@ -13,12 +13,12 @@ namespace Microsoft.AspNetCore.Builder
{ {
public static void UseBlazor( public static void UseBlazor(
this IApplicationBuilder applicationBuilder, this IApplicationBuilder applicationBuilder,
Assembly clientAssembly) string clientAssemblyPath)
{ {
applicationBuilder.UseStaticFiles(new StaticFileOptions applicationBuilder.UseStaticFiles(new StaticFileOptions
{ {
RequestPath = "/_framework", RequestPath = "/_framework",
FileProvider = ClientFileProvider.Instantiate(clientAssembly), FileProvider = ClientFileProvider.Instantiate(clientAssemblyPath),
ContentTypeProvider = CreateContentTypeProvider(), ContentTypeProvider = CreateContentTypeProvider(),
}); });
} }

View File

@ -4,16 +4,21 @@
using Microsoft.Blazor.Browser; using Microsoft.Blazor.Browser;
using Microsoft.Blazor.Mono; using Microsoft.Blazor.Mono;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using System.IO;
using System.Reflection; using System.Reflection;
namespace Microsoft.Blazor.Server.ClientFilesystem namespace Microsoft.Blazor.Server.ClientFilesystem
{ {
internal static class ClientFileProvider internal static class ClientFileProvider
{ {
public static IFileProvider Instantiate(Assembly clientApp) public static IFileProvider Instantiate(string clientAssemblyPath)
=> new CompositeFileProvider( => new CompositeFileProvider(
MonoStaticFileProvider.JsFiles, MonoStaticFileProvider.JsFiles,
BlazorBrowserFileProvider.Instance, BlazorBrowserFileProvider.Instance,
new ReferencedAssemblyFileProvider(clientApp, MonoStaticFileProvider.BclFiles)); new ReferencedAssemblyFileProvider(
Path.GetFileNameWithoutExtension(clientAssemblyPath),
new ReferencedAssemblyResolver(
MonoStaticFileProvider.BclFiles,
Path.GetDirectoryName(clientAssemblyPath))));
} }
} }

View File

@ -2,13 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO; using System.IO;
using Microsoft.Extensions.FileProviders;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Mono.Cecil; using Mono.Cecil;
using Microsoft.Blazor.Internal.Common.FileProviders; using Microsoft.Blazor.Internal.Common.FileProviders;
using System.Reflection;
using System;
namespace Microsoft.Blazor.Server.ClientFilesystem namespace Microsoft.Blazor.Server.ClientFilesystem
{ {
@ -16,152 +13,57 @@ namespace Microsoft.Blazor.Server.ClientFilesystem
{ {
private const string ClientBinDir = "_bin"; private const string ClientBinDir = "_bin";
public ReferencedAssemblyFileProvider(Assembly assembly, IFileProvider clientBcl) : this( public ReferencedAssemblyFileProvider(string rootAssemblyName, ReferencedAssemblyResolver resolver)
AssemblyDefinition.ReadAssembly(assembly.Location), : base(ComputeContents(rootAssemblyName, resolver))
File.ReadAllBytes(assembly.Location),
clientBcl)
{
}
public ReferencedAssemblyFileProvider(
AssemblyDefinition entrypoint,
byte[] entrypointData,
IFileProvider clientBcl)
: base(ComputeContents(entrypoint, entrypointData, clientBcl))
{ {
} }
private static IEnumerable<(string, Stream)> ComputeContents( private static IEnumerable<(string, Stream)> ComputeContents(
AssemblyDefinition entrypoint, string rootAssemblyName,
byte[] entrypointData, ReferencedAssemblyResolver resolver)
IFileProvider clientBcl)
{ {
var foundAssemblies = new Dictionary<string, ReferencedAssembly>(); var foundAssemblies = new Dictionary<string, ReferencedAssemblyInfo>();
AddWithReferencesRecursive(new ReferencedAssembly(entrypoint, entrypointData), clientBcl, foundAssemblies); AddWithReferencesRecursive(rootAssemblyName, resolver, foundAssemblies);
return foundAssemblies.Values.Select(assembly => ( return foundAssemblies.Values.Select(assembly => (
$"/{ClientBinDir}/{assembly.Name}.dll", $"/{ClientBinDir}/{assembly.Definition.Name.Name}.dll",
(Stream)new MemoryStream(assembly.Data))); (Stream)new MemoryStream(assembly.Data)));
} }
private static void AddWithReferencesRecursive( private static void AddWithReferencesRecursive(
ReferencedAssembly root, string name,
IFileProvider clientBcl, ReferencedAssemblyResolver resolver,
IDictionary<string, ReferencedAssembly> results) IDictionary<string, ReferencedAssemblyInfo> results)
{ {
results.Add(root.Name, root); if (resolver.TryResolve(name, out var assemblyBytes))
foreach (var module in root.Definition.Modules)
{ {
foreach (var referenceName in module.AssemblyReferences) var assemblyInfo = new ReferencedAssemblyInfo(assemblyBytes);
results.Add(assemblyInfo.Definition.Name.Name, assemblyInfo);
var childReferencesToAdd = assemblyInfo.Definition.Modules
.SelectMany(module => module.AssemblyReferences)
.Select(childReference => childReference.Name)
.Where(childReferenceName => !results.ContainsKey(childReferenceName));
foreach (var childReferenceName in childReferencesToAdd)
{ {
if (!results.ContainsKey(referenceName.Name)) AddWithReferencesRecursive(childReferenceName, resolver, results);
{
var resolvedReference = ResolveReference(clientBcl, module, referenceName);
if (resolvedReference != null)
{
AddWithReferencesRecursive(resolvedReference, clientBcl, results);
}
}
} }
} }
} }
private static ReferencedAssembly ResolveReference(IFileProvider clientBcl, ModuleDefinition module, AssemblyNameReference referenceName) private class ReferencedAssemblyInfo
{ {
if (SearchInFileProvider(clientBcl, string.Empty, $"{referenceName.Name}.dll", out var bclFile))
{
// Where possible, we resolve references to client BCL assemblies
return new ReferencedAssembly(bclFile);
}
else
{
try
{
// If it's not a client BCL assembly, maybe we can resolve it natively
// (e.g., if it's in the app's bin directory, or a NuGet package)
var nativelyResolved = module.AssemblyResolver.Resolve(referenceName);
return AllowServingAssembly(nativelyResolved)
? new ReferencedAssembly(
nativelyResolved,
File.ReadAllBytes(nativelyResolved.MainModule.FileName))
: null;
}
catch (AssemblyResolutionException)
{
// Some of the referenced assemblies aren't included in the Mono BCL, e.g.,
// Mono.Security.dll which is referenced from System.dll. These ones are not
// required at runtime, so just skip them.
return null;
}
}
}
private static bool AllowServingAssembly(AssemblyDefinition nativelyResolvedAssembly)
{
// When we use the native assembly resolver, it might return something from a NuGet
// packages folder which we do want to serve, or it might return something from the
// core .NET BCL which we *don't* want to serve (because the core BCL assemblies
// should come from the Mono WASM distribution only). Currently there isn't a good
// way to differentiate these cases, so as a temporary heuristic, assume anything
// named System.* shouldn't be resolved natively.
return !nativelyResolvedAssembly.MainModule.Name.StartsWith(
"System.",
StringComparison.Ordinal);
}
private static bool SearchInFileProvider(IFileProvider fileProvider, string searchRootDirNoTrailingSlash, string name, out IFileInfo file)
{
var possibleFullPath = $"{searchRootDirNoTrailingSlash}/{name}";
var possibleResult = fileProvider.GetFileInfo(possibleFullPath);
if (possibleResult.Exists)
{
file = possibleResult;
return true;
}
var childDirs = fileProvider.GetDirectoryContents(searchRootDirNoTrailingSlash)
.Where(item => item.IsDirectory);
foreach (var childDir in childDirs)
{
if (SearchInFileProvider(fileProvider, childDir.PhysicalPath, name, out file))
{
return true;
}
}
file = null;
return false;
}
private class ReferencedAssembly
{
public string Name { get; }
public byte[] Data { get; } public byte[] Data { get; }
public AssemblyDefinition Definition { get; } public AssemblyDefinition Definition { get; }
public ReferencedAssembly(AssemblyDefinition definition, byte[] rawData) public ReferencedAssemblyInfo(byte[] rawData)
{ {
Name = definition.Name.Name;
Data = rawData; Data = rawData;
Definition = definition;
}
public ReferencedAssembly(IFileInfo fileInfo) using (var ms = new MemoryStream(rawData))
{
using (var ms = new MemoryStream())
using (var readStream = fileInfo.CreateReadStream())
{ {
readStream.CopyTo(ms); Definition = AssemblyDefinition.ReadAssembly(ms);
Data = ms.ToArray();
} }
using (var readStream = new MemoryStream(Data))
{
Definition = AssemblyDefinition.ReadAssembly(readStream);
}
Name = Definition.Name.Name;
} }
} }
} }

View File

@ -0,0 +1,74 @@
// 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 Microsoft.Extensions.FileProviders;
using System.IO;
using System.Linq;
namespace Microsoft.Blazor.Server.ClientFilesystem
{
internal class ReferencedAssemblyResolver
{
private readonly IFileProvider _bcl;
private readonly string _searchDirectory;
public ReferencedAssemblyResolver(IFileProvider bcl, string searchDirectory)
{
_bcl = bcl;
_searchDirectory = searchDirectory;
}
public bool TryResolve(string name, out byte[] assemblyBytes)
{
var filename = $"{name}.dll";
if (SearchInFileProvider(_bcl, string.Empty, filename, out var fileInfo))
{
using (var ms = new MemoryStream())
using (var fileInfoStream = fileInfo.CreateReadStream())
{
fileInfoStream.CopyTo(ms);
assemblyBytes = ms.ToArray();
return true;
}
}
else
{
var searchDirPath = Path.Combine(_searchDirectory, filename);
if (File.Exists(searchDirPath))
{
assemblyBytes = File.ReadAllBytes(searchDirPath);
return true;
}
else
{
assemblyBytes = null;
return false;
}
}
}
private static bool SearchInFileProvider(IFileProvider fileProvider, string searchRootDirNoTrailingSlash, string name, out IFileInfo file)
{
var possibleFullPath = $"{searchRootDirNoTrailingSlash}/{name}";
var possibleResult = fileProvider.GetFileInfo(possibleFullPath);
if (possibleResult.Exists)
{
file = possibleResult;
return true;
}
var childDirs = fileProvider.GetDirectoryContents(searchRootDirNoTrailingSlash)
.Where(item => item.IsDirectory);
foreach (var childDir in childDirs)
{
if (SearchInFileProvider(fileProvider, childDir.PhysicalPath, name, out file))
{
return true;
}
}
file = null;
return false;
}
}
}