diff --git a/Blazor.sln b/Blazor.sln index 6206b2d6f1..23ed9142f4 100644 --- a/Blazor.sln +++ b/Blazor.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27019.1 +VisualStudioVersion = 15.0.27004.2005 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{F5FDD4E5-6A52-4A86-BE5E-5E42CB1DC8DA}" EndProject @@ -38,12 +38,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostedInAspNet.Client", "sa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostedInAspNet.Server", "samples\HostedInAspNet.Server\HostedInAspNet.Server.csproj", "{F8996835-41F7-4663-91DF-3B5652ADC37D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Blazor", "src\Microsoft.Blazor\Microsoft.Blazor.csproj", "{7FD8C650-74B3-4153-AEA1-00F4F6AF393D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Blazor", "src\Microsoft.Blazor\Microsoft.Blazor.csproj", "{7FD8C650-74B3-4153-AEA1-00F4F6AF393D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StandaloneApp", "samples\StandaloneApp\StandaloneApp.csproj", "{B241434A-1642-44CC-AE9A-2012B5C5BD02}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MonoSanity", "MonoSanity", "{2A076721-6081-4517-8329-B9E5110D6DAC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Blazor.Server.Test", "Microsoft.Blazor.Server.Test\Microsoft.Blazor.Server.Test.csproj", "{71AF445F-0903-4743-B047-44B3B2C19DC9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -102,6 +104,10 @@ Global {B241434A-1642-44CC-AE9A-2012B5C5BD02}.Debug|Any CPU.Build.0 = Debug|Any CPU {B241434A-1642-44CC-AE9A-2012B5C5BD02}.Release|Any CPU.ActiveCfg = Release|Any CPU {B241434A-1642-44CC-AE9A-2012B5C5BD02}.Release|Any CPU.Build.0 = Release|Any CPU + {71AF445F-0903-4743-B047-44B3B2C19DC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71AF445F-0903-4743-B047-44B3B2C19DC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71AF445F-0903-4743-B047-44B3B2C19DC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71AF445F-0903-4743-B047-44B3B2C19DC9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -123,6 +129,7 @@ Global {7FD8C650-74B3-4153-AEA1-00F4F6AF393D} = {B867E038-B3CE-43E3-9292-61568C46CDEB} {B241434A-1642-44CC-AE9A-2012B5C5BD02} = {F5FDD4E5-6A52-4A86-BE5E-5E42CB1DC8DA} {2A076721-6081-4517-8329-B9E5110D6DAC} = {F5FDD4E5-6A52-4A86-BE5E-5E42CB1DC8DA} + {71AF445F-0903-4743-B047-44B3B2C19DC9} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3} diff --git a/Microsoft.Blazor.Server.Test/Microsoft.Blazor.Server.Test.csproj b/Microsoft.Blazor.Server.Test/Microsoft.Blazor.Server.Test.csproj new file mode 100644 index 0000000000..87ad644bcd --- /dev/null +++ b/Microsoft.Blazor.Server.Test/Microsoft.Blazor.Server.Test.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + + diff --git a/Microsoft.Blazor.Server.Test/ReferencedAssemblyFileProviderTest.cs b/Microsoft.Blazor.Server.Test/ReferencedAssemblyFileProviderTest.cs new file mode 100644 index 0000000000..42ca74ccee --- /dev/null +++ b/Microsoft.Blazor.Server.Test/ReferencedAssemblyFileProviderTest.cs @@ -0,0 +1,25 @@ +// 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.Linq; +using Xunit; + +namespace Microsoft.Blazor.Server.Test +{ + public class ReferencedAssemblyFileProviderTest + { + [Fact] + public void FindsEntrypointAssemblyAndReferencedAssemblies() + { + var provider = new ReferencedAssemblyFileProvider(); + var contents = provider.GetDirectoryContents(string.Empty).OrderBy(i => i.Name).ToList(); + Assert.Collection(contents, + item => { Assert.Equal("HostedInAspNet.Client.dll", item.PhysicalPath); }, + item => { Assert.Equal("mscorlib.dll", item.PhysicalPath); }, + item => { Assert.Equal("System.Console.dll", item.PhysicalPath); }, + item => { Assert.Equal("System.Core.dll", item.PhysicalPath); }, + item => { Assert.Equal("System.dll", item.PhysicalPath); }, + item => { Assert.Equal("System.Runtime.dll", item.PhysicalPath); }); + } + } +} diff --git a/samples/HostedInAspNet.Client/Program.cs b/samples/HostedInAspNet.Client/Program.cs index bd6ec10054..8055591e70 100644 --- a/samples/HostedInAspNet.Client/Program.cs +++ b/samples/HostedInAspNet.Client/Program.cs @@ -5,7 +5,7 @@ using System; namespace HostedInAspNet.Client { - class Program + public class Program { static void Main(string[] args) { diff --git a/src/Microsoft.Blazor.Browser/BlazorBrowserFileProvider.cs b/src/Microsoft.Blazor.Browser/BlazorBrowserFileProvider.cs index e83c802ebc..1d6232020d 100644 --- a/src/Microsoft.Blazor.Browser/BlazorBrowserFileProvider.cs +++ b/src/Microsoft.Blazor.Browser/BlazorBrowserFileProvider.cs @@ -13,8 +13,14 @@ namespace Microsoft.Blazor.Browser typeof(BlazorBrowserFileProvider).Assembly, "blazor"); + public static BlazorBrowserFileProvider Instance = new BlazorBrowserFileProvider(); + + private BlazorBrowserFileProvider() + { + } + public IFileInfo GetFileInfo(string subpath) - =>_embeddedFiles.GetFileInfo(subpath.Replace('/', '$')); + => _embeddedFiles.GetFileInfo(subpath.Replace('/', '$')); public IDirectoryContents GetDirectoryContents(string subpath) => throw new NotImplementedException(); // Don't need to support this diff --git a/src/Microsoft.Blazor.Mono/MonoStaticFileProvider.cs b/src/Microsoft.Blazor.Mono/MonoStaticFileProvider.cs index 231c217509..06f8505e87 100644 --- a/src/Microsoft.Blazor.Mono/MonoStaticFileProvider.cs +++ b/src/Microsoft.Blazor.Mono/MonoStaticFileProvider.cs @@ -13,6 +13,12 @@ namespace Microsoft.Blazor.Mono typeof(MonoStaticFileProvider).Assembly, "mono"); + public static MonoStaticFileProvider Instance = new MonoStaticFileProvider(); + + private MonoStaticFileProvider() + { + } + public IFileInfo GetFileInfo(string subpath) { // EmbeddedFileProvider can't find resources whose names include '/' (or '\'), @@ -22,7 +28,7 @@ namespace Microsoft.Blazor.Mono } public IDirectoryContents GetDirectoryContents(string subpath) - => throw new NotImplementedException(); // Don't need to support this + => _embeddedFiles.GetDirectoryContents(subpath); public IChangeToken Watch(string filter) => throw new NotImplementedException(); // Don't need to support this diff --git a/src/Microsoft.Blazor.Server/BlazorAppBuilderExtensions.cs b/src/Microsoft.Blazor.Server/BlazorAppBuilderExtensions.cs index 68932c449a..0b1627734b 100644 --- a/src/Microsoft.Blazor.Server/BlazorAppBuilderExtensions.cs +++ b/src/Microsoft.Blazor.Server/BlazorAppBuilderExtensions.cs @@ -18,8 +18,8 @@ namespace Microsoft.AspNetCore.Builder { RequestPath = "/_framework", FileProvider = new CompositeFileProvider( - new MonoStaticFileProvider(), - new BlazorBrowserFileProvider()), + MonoStaticFileProvider.Instance, + BlazorBrowserFileProvider.Instance), ContentTypeProvider = CreateContentTypeProvider(), }); } diff --git a/src/Microsoft.Blazor.Server/Microsoft.Blazor.Server.csproj b/src/Microsoft.Blazor.Server/Microsoft.Blazor.Server.csproj index f57984dbac..5609517a53 100644 --- a/src/Microsoft.Blazor.Server/Microsoft.Blazor.Server.csproj +++ b/src/Microsoft.Blazor.Server/Microsoft.Blazor.Server.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Microsoft.Blazor.Server/Properties/AssemblyInfo.cs b/src/Microsoft.Blazor.Server/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..015cbd4cb3 --- /dev/null +++ b/src/Microsoft.Blazor.Server/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Blazor.Server.Test")] diff --git a/src/Microsoft.Blazor.Server/ReferencedAssemblyFileProvider.cs b/src/Microsoft.Blazor.Server/ReferencedAssemblyFileProvider.cs new file mode 100644 index 0000000000..8f1018d61d --- /dev/null +++ b/src/Microsoft.Blazor.Server/ReferencedAssemblyFileProvider.cs @@ -0,0 +1,169 @@ +// 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.IO; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; +using System.Collections.Generic; +using System.Collections; +using System.Linq; +using Mono.Cecil; +using Microsoft.Blazor.Mono; + +namespace Microsoft.Blazor.Server +{ + internal class ReferencedAssemblyFileProvider : IFileProvider + { + private static readonly Dictionary _frameworkDlls = ReadFrameworkDlls(); + private readonly Contents _referencedAssemblyContents = new Contents(FindReferencedAssemblies()); + private readonly Contents _emptyContents = new Contents(null); + + public IDirectoryContents GetDirectoryContents(string subpath) + { + return subpath == string.Empty + ? _referencedAssemblyContents + : _emptyContents; + } + + public IFileInfo GetFileInfo(string subpath) + { + throw new System.NotImplementedException(); + } + + public IChangeToken Watch(string filter) + { + throw new System.NotImplementedException(); + } + + private static IEnumerable FindReferencedAssemblies() + { + var foundAssemblies = new Dictionary(); + FindReferencedAssembliesRecursive( + AssemblyDefinition.ReadAssembly(typeof(TApp).Assembly.Location), + foundAssemblies); + return foundAssemblies.Values; + } + + private static void FindReferencedAssembliesRecursive(AssemblyDefinition root, IDictionary results) + { + results.Add(root.Name.Name, root); + + foreach (var module in root.Modules) + { + foreach (var referenceName in module.AssemblyReferences) + { + if (!results.ContainsKey(referenceName.Name)) + { + var resolvedReference = FindAssembly(referenceName, module.AssemblyResolver); + + // 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. + if (resolvedReference != null) + { + FindReferencedAssembliesRecursive(resolvedReference, results); + } + } + } + } + } + + private static AssemblyDefinition FindAssembly(AssemblyNameReference name, IAssemblyResolver nativeResolver) + { + try + { + return _frameworkDlls.TryGetValue($"{name.Name}.dll", out var fileInfo) + ? AssemblyDefinition.ReadAssembly(fileInfo.CreateReadStream()) + : AllowNativeDllResolution(name) ? nativeResolver.Resolve(name) : null; + } + catch (AssemblyResolutionException) + { + return null; + } + } + + private static bool AllowNativeDllResolution(AssemblyNameReference name) + { + // System.* assemblies must only be resolved from the browser-reachable FrameworkFiles. + // It's no use resolving them using the native resolver, because those files wouldn't + // be accessible at runtime anyway. + return !name.Name.StartsWith("System.", StringComparison.Ordinal); + } + + private static Dictionary ReadFrameworkDlls() + { + // TODO: Stop leaking knowledge of the Microsoft.Blazor.Mono file provider internal + // structure into this unrelated class. Currently it's needed because that file provider + // doesn't support proper directory hierarchies and therefore keeps all files in the + // top-level directory, putting '$' into filenames in place of directories. + // To fix this, make MonoStaticFileProvider expose a regular directory structure, and + // then change this method to walk it recursively. + + return MonoStaticFileProvider.Instance + .GetDirectoryContents(string.Empty) + .Where(file => file.Name.EndsWith(".dll", StringComparison.Ordinal)) + .ToDictionary(MonoEmbeddedResourceToFilename, file => file); + + string MonoEmbeddedResourceToFilename(IFileInfo fileInfo) + { + var name = fileInfo.Name; + var lastDirSeparatorPos = name.LastIndexOf('$'); + return name.Substring(lastDirSeparatorPos + 1); + } + } + + private class Contents : IDirectoryContents + { + private readonly bool _exists; + private readonly IReadOnlyDictionary _items; + + public Contents(IEnumerable assemblies) + { + _exists = assemblies != null; + _items = (assemblies ?? Enumerable.Empty()) + .Select(assembly => new AssemblyFileInfo(assembly)) + .ToDictionary(item => item.Name, item => (IFileInfo)item); + } + + public bool Exists => _exists; + + public IEnumerator GetEnumerator() => _items.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _items.Values.GetEnumerator(); + } + + private class AssemblyFileInfo : IFileInfo + { + private readonly byte[] _data; + + public bool Exists => true; + + public long Length => _data.Length; + + public string PhysicalPath => Name; + + public string Name { get; } + + public DateTimeOffset LastModified => default(DateTimeOffset); + + public bool IsDirectory => false; + + public Stream CreateReadStream() + { + return new MemoryStream(_data); + } + + public AssemblyFileInfo(AssemblyDefinition assembly) + { + Name = $"{assembly.Name.Name}.dll"; + + using (var ms = new MemoryStream()) + { + assembly.Write(ms); + _data = ms.GetBuffer(); + } + } + } + } +} diff --git a/test/Microsoft.Blazor.Mono.Test/MonoStaticFileProviderTest.cs b/test/Microsoft.Blazor.Mono.Test/MonoStaticFileProviderTest.cs index fe3e175817..d4f255ac22 100644 --- a/test/Microsoft.Blazor.Mono.Test/MonoStaticFileProviderTest.cs +++ b/test/Microsoft.Blazor.Mono.Test/MonoStaticFileProviderTest.cs @@ -10,8 +10,6 @@ namespace Microsoft.Blazor.Mono.Test [Fact] public void SuppliesMonoFiles() { - var provider = new MonoStaticFileProvider(); - // This is not an exhaustive list. The set of BCL facade types is long and // will probably change. This test is just to verify the resource embedding // and filename mapping is working correctly. @@ -26,7 +24,7 @@ namespace Microsoft.Blazor.Mono.Test foreach (var name in expectedFiles) { - var fileInfo = provider.GetFileInfo(name); + var fileInfo = MonoStaticFileProvider.Instance.GetFileInfo(name); Assert.True(fileInfo.Exists); Assert.False(fileInfo.IsDirectory); Assert.True(fileInfo.Length > 0); @@ -36,8 +34,6 @@ namespace Microsoft.Blazor.Mono.Test [Fact] public void DoesNotSupplyUnexpectedFiles() { - var provider = new MonoStaticFileProvider(); - var notExpectedFiles = new[] { "", @@ -51,7 +47,7 @@ namespace Microsoft.Blazor.Mono.Test foreach (var name in notExpectedFiles) { - var fileInfo = provider.GetFileInfo(name); + var fileInfo = MonoStaticFileProvider.Instance.GetFileInfo(name); Assert.False(fileInfo.Exists); } }