From aff369e86dd21ce0bac549759fa3719663e4e6dc Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 11 Dec 2017 21:06:37 +0000 Subject: [PATCH] Improve assembly resolution. Standalone hosting now works again. --- .../ReferencedAssemblyFileProviderTest.cs | 21 ++- samples/HostedInAspNet.Server/Startup.cs | 2 +- samples/MonoSanity/Startup.cs | 5 +- samples/StandaloneApp/wwwroot/index.html | 3 +- .../Server/Startup.cs | 2 +- .../BlazorAppBuilderExtensions.cs | 4 +- .../ClientFilesystem/ClientFileProvider.cs | 9 +- .../ReferencedAssemblyFileProvider.cs | 146 +++--------------- .../ReferencedAssemblyResolver.cs | 74 +++++++++ 9 files changed, 121 insertions(+), 145 deletions(-) create mode 100644 src/Microsoft.Blazor.Server/ClientFilesystem/ReferencedAssemblyResolver.cs diff --git a/Microsoft.Blazor.Server.Test/ReferencedAssemblyFileProviderTest.cs b/Microsoft.Blazor.Server.Test/ReferencedAssemblyFileProviderTest.cs index 7482f8dccb..5ef483f062 100644 --- a/Microsoft.Blazor.Server.Test/ReferencedAssemblyFileProviderTest.cs +++ b/Microsoft.Blazor.Server.Test/ReferencedAssemblyFileProviderTest.cs @@ -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[] { /* diff --git a/samples/HostedInAspNet.Server/Startup.cs b/samples/HostedInAspNet.Server/Startup.cs index 92ceba8905..1729f0bce0 100644 --- a/samples/HostedInAspNet.Server/Startup.cs +++ b/samples/HostedInAspNet.Server/Startup.cs @@ -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); } } } diff --git a/samples/MonoSanity/Startup.cs b/samples/MonoSanity/Startup.cs index d40144e6fe..baa1b9db57 100644 --- a/samples/MonoSanity/Startup.cs +++ b/samples/MonoSanity/Startup.cs @@ -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); } } } diff --git a/samples/StandaloneApp/wwwroot/index.html b/samples/StandaloneApp/wwwroot/index.html index 715729c43c..048bb1aa3f 100644 --- a/samples/StandaloneApp/wwwroot/index.html +++ b/samples/StandaloneApp/wwwroot/index.html @@ -6,6 +6,7 @@

Hello

- + diff --git a/src/Microsoft.Blazor.DevHost/Server/Startup.cs b/src/Microsoft.Blazor.DevHost/Server/Startup.cs index 94af9dca25..81a1d435a1 100644 --- a/src/Microsoft.Blazor.DevHost/Server/Startup.cs +++ b/src/Microsoft.Blazor.DevHost/Server/Startup.cs @@ -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) diff --git a/src/Microsoft.Blazor.Server/BlazorAppBuilderExtensions.cs b/src/Microsoft.Blazor.Server/BlazorAppBuilderExtensions.cs index 0d5c9af277..48227cc5c1 100644 --- a/src/Microsoft.Blazor.Server/BlazorAppBuilderExtensions.cs +++ b/src/Microsoft.Blazor.Server/BlazorAppBuilderExtensions.cs @@ -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(), }); } diff --git a/src/Microsoft.Blazor.Server/ClientFilesystem/ClientFileProvider.cs b/src/Microsoft.Blazor.Server/ClientFilesystem/ClientFileProvider.cs index 2788717999..68909bbdae 100644 --- a/src/Microsoft.Blazor.Server/ClientFilesystem/ClientFileProvider.cs +++ b/src/Microsoft.Blazor.Server/ClientFilesystem/ClientFileProvider.cs @@ -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)))); } } diff --git a/src/Microsoft.Blazor.Server/ClientFilesystem/ReferencedAssemblyFileProvider.cs b/src/Microsoft.Blazor.Server/ClientFilesystem/ReferencedAssemblyFileProvider.cs index 6716138bf9..311b69ac13 100644 --- a/src/Microsoft.Blazor.Server/ClientFilesystem/ReferencedAssemblyFileProvider.cs +++ b/src/Microsoft.Blazor.Server/ClientFilesystem/ReferencedAssemblyFileProvider.cs @@ -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(); - AddWithReferencesRecursive(new ReferencedAssembly(entrypoint, entrypointData), clientBcl, foundAssemblies); + var foundAssemblies = new Dictionary(); + 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 results) + string name, + ReferencedAssemblyResolver resolver, + IDictionary 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; } } } diff --git a/src/Microsoft.Blazor.Server/ClientFilesystem/ReferencedAssemblyResolver.cs b/src/Microsoft.Blazor.Server/ClientFilesystem/ReferencedAssemblyResolver.cs new file mode 100644 index 0000000000..51b4e7191a --- /dev/null +++ b/src/Microsoft.Blazor.Server/ClientFilesystem/ReferencedAssemblyResolver.cs @@ -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; + } + } +}