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

View File

@ -25,7 +25,7 @@ namespace HostedInAspNet.Server
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.Hosting;
using MonoSanityClient;
using System.IO;
using System.Net.Mime;
namespace MonoSanity
{
@ -15,7 +12,7 @@ namespace MonoSanity
{
app.UseDeveloperExceptionPage();
app.UseFileServer();
app.UseBlazor(clientAssembly: typeof(MonoSanityClient.Examples).Assembly);
app.UseBlazor(clientAssemblyPath: typeof(MonoSanityClient.Examples).Assembly.Location);
}
}
}

View File

@ -6,6 +6,7 @@
</head>
<body>
<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>
</html>

View File

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

View File

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

View File

@ -4,16 +4,21 @@
using Microsoft.Blazor.Browser;
using Microsoft.Blazor.Mono;
using Microsoft.Extensions.FileProviders;
using System.IO;
using System.Reflection;
namespace Microsoft.Blazor.Server.ClientFilesystem
{
internal static class ClientFileProvider
{
public static IFileProvider Instantiate(Assembly clientApp)
public static IFileProvider Instantiate(string clientAssemblyPath)
=> new CompositeFileProvider(
MonoStaticFileProvider.JsFiles,
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.
using System.IO;
using Microsoft.Extensions.FileProviders;
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
using Microsoft.Blazor.Internal.Common.FileProviders;
using System.Reflection;
using System;
namespace Microsoft.Blazor.Server.ClientFilesystem
{
@ -16,152 +13,57 @@ namespace Microsoft.Blazor.Server.ClientFilesystem
{
private const string ClientBinDir = "_bin";
public ReferencedAssemblyFileProvider(Assembly assembly, IFileProvider clientBcl) : this(
AssemblyDefinition.ReadAssembly(assembly.Location),
File.ReadAllBytes(assembly.Location),
clientBcl)
{
}
public ReferencedAssemblyFileProvider(
AssemblyDefinition entrypoint,
byte[] entrypointData,
IFileProvider clientBcl)
: base(ComputeContents(entrypoint, entrypointData, clientBcl))
public ReferencedAssemblyFileProvider(string rootAssemblyName, ReferencedAssemblyResolver resolver)
: base(ComputeContents(rootAssemblyName, resolver))
{
}
private static IEnumerable<(string, Stream)> ComputeContents(
AssemblyDefinition entrypoint,
byte[] entrypointData,
IFileProvider clientBcl)
string rootAssemblyName,
ReferencedAssemblyResolver resolver)
{
var foundAssemblies = new Dictionary<string, ReferencedAssembly>();
AddWithReferencesRecursive(new ReferencedAssembly(entrypoint, entrypointData), clientBcl, foundAssemblies);
var foundAssemblies = new Dictionary<string, ReferencedAssemblyInfo>();
AddWithReferencesRecursive(rootAssemblyName, resolver, foundAssemblies);
return foundAssemblies.Values.Select(assembly => (
$"/{ClientBinDir}/{assembly.Name}.dll",
$"/{ClientBinDir}/{assembly.Definition.Name.Name}.dll",
(Stream)new MemoryStream(assembly.Data)));
}
private static void AddWithReferencesRecursive(
ReferencedAssembly root,
IFileProvider clientBcl,
IDictionary<string, ReferencedAssembly> results)
string name,
ReferencedAssemblyResolver resolver,
IDictionary<string, ReferencedAssemblyInfo> results)
{
results.Add(root.Name, root);
foreach (var module in root.Definition.Modules)
if (resolver.TryResolve(name, out var assemblyBytes))
{
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))
{
var resolvedReference = ResolveReference(clientBcl, module, referenceName);
if (resolvedReference != null)
{
AddWithReferencesRecursive(resolvedReference, clientBcl, results);
}
}
AddWithReferencesRecursive(childReferenceName, resolver, 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 AssemblyDefinition Definition { get; }
public ReferencedAssembly(AssemblyDefinition definition, byte[] rawData)
public ReferencedAssemblyInfo(byte[] rawData)
{
Name = definition.Name.Name;
Data = rawData;
Definition = definition;
}
public ReferencedAssembly(IFileInfo fileInfo)
{
using (var ms = new MemoryStream())
using (var readStream = fileInfo.CreateReadStream())
using (var ms = new MemoryStream(rawData))
{
readStream.CopyTo(ms);
Data = ms.ToArray();
Definition = AssemblyDefinition.ReadAssembly(ms);
}
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;
}
}
}