diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ExtensionInitializer.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ExtensionInitializer.cs new file mode 100644 index 0000000000..eeb3246f74 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/ExtensionInitializer.cs @@ -0,0 +1,15 @@ +// 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.AspNetCore.Razor.Language; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions +{ + internal class ExtensionInitializer : RazorExtensionInitializer + { + public override void Initialize(RazorProjectEngineBuilder builder) + { + RazorExtensions.Register(builder); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs index 6be3a1f170..b5caca17dd 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/Properties/AssemblyInfo.cs @@ -2,6 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; + +[assembly: ProvideRazorExtensionInitializer("MVC-2.0", typeof(ExtensionInitializer))] +[assembly: ProvideRazorExtensionInitializer("MVC-2.1", typeof(ExtensionInitializer))] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Extensions.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Razor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs index c22878bd09..abf1b447f0 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/RazorExtensions.cs @@ -25,6 +25,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions InheritsDirective.Register(builder); SectionDirective.Register(builder); + builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); + builder.AddTargetExtension(new ViewComponentTagHelperTargetExtension()); builder.AddTargetExtension(new TemplateTargetExtension() { diff --git a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.props b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.props index 8d2ac3b630..ef8766cdf2 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.props +++ b/src/Microsoft.AspNetCore.Mvc.Razor.Extensions/build/netstandard2.0/Microsoft.AspNetCore.Mvc.Razor.props @@ -10,6 +10,9 @@ Set the primary configuration supported by this pacakge as the default configuration for Razor. --> MVC-2.1 + + + <_MvcExtensionAssemblyPath Condition="'$(_MvcExtensionAssemblyPath)'==''">$(MSBuildThisFileDirectory)..\..\lib\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll @@ -26,7 +29,7 @@ Microsoft.AspNetCore.Mvc.Razor.Extensions - $(MSBuildThisFileDirectory)..\..\lib\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll + $(_MvcExtensionAssemblyPath) \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets index 699b4608f3..8215f37eaf 100644 --- a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets +++ b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets @@ -73,6 +73,9 @@ UseServer="$(UseRazorBuildServer)" ForceServer="$(_RazorForceBuildServer)" PipeName="$(_RazorBuildServerPipeName)" + Version="$(RazorLangVersion)" + Configuration="@(ResolvedRazorConfiguration)" + Extensions="@(ResolvedRazorExtension)" Assemblies="@(RazorReferencePath)" ProjectRoot="$(MSBuildProjectDirectory)" TagHelperManifest="$(_RazorTagHelperOutputCache)"> @@ -121,6 +124,9 @@ UseServer="$(UseRazorBuildServer)" ForceServer="$(_RazorForceBuildServer)" PipeName="$(_RazorBuildServerPipeName)" + Version="$(RazorLangVersion)" + Configuration="@(ResolvedRazorConfiguration)" + Extensions="@(ResolvedRazorExtension)" Sources="@(RazorGenerateWithTargetPath)" ProjectRoot="$(MSBuildProjectDirectory)" TagHelperManifest="$(_RazorTagHelperOutputCache)" /> diff --git a/src/Microsoft.AspNetCore.Razor.Language/AssemblyExtension.cs b/src/Microsoft.AspNetCore.Razor.Language/AssemblyExtension.cs new file mode 100644 index 0000000000..b93323e018 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/AssemblyExtension.cs @@ -0,0 +1,31 @@ +// 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.Reflection; + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal class AssemblyExtension : RazorExtension + { + public AssemblyExtension(string extensionName, Assembly assembly) + { + if (extensionName == null) + { + throw new ArgumentNullException(nameof(extensionName)); + } + + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + ExtensionName = extensionName; + Assembly = assembly; + } + + public override string ExtensionName { get; } + + public Assembly Assembly { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/EmptyProjectFileSystem.cs b/src/Microsoft.AspNetCore.Razor.Language/EmptyProjectFileSystem.cs new file mode 100644 index 0000000000..61ced1271b --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/EmptyProjectFileSystem.cs @@ -0,0 +1,23 @@ +// 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.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNetCore.Razor.Language +{ + internal class EmptyProjectFileSystem : RazorProjectFileSystem + { + public override IEnumerable EnumerateItems(string basePath) + { + NormalizeAndEnsureValidPath(basePath); + return Enumerable.Empty(); + } + + public override RazorProjectItem GetItem(string path) + { + NormalizeAndEnsureValidPath(path); + return new NotFoundProjectItem(string.Empty, path); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/ProvideRazorExtensionInitializerAttribute.cs b/src/Microsoft.AspNetCore.Razor.Language/ProvideRazorExtensionInitializerAttribute.cs new file mode 100644 index 0000000000..374419a676 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/ProvideRazorExtensionInitializerAttribute.cs @@ -0,0 +1,31 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Language +{ + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] + public class ProvideRazorExtensionInitializerAttribute : Attribute + { + public ProvideRazorExtensionInitializerAttribute(string extensionName, Type initializerType) + { + if (extensionName == null) + { + throw new ArgumentNullException(nameof(extensionName)); + } + + if (initializerType == null) + { + throw new ArgumentNullException(nameof(initializerType)); + } + + ExtensionName = extensionName; + InitializerType = initializerType; + } + + public string ExtensionName { get; } + + public Type InitializerType { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorExtensionInitializer.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorExtensionInitializer.cs new file mode 100644 index 0000000000..5117115af6 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorExtensionInitializer.cs @@ -0,0 +1,10 @@ +// 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. + +namespace Microsoft.AspNetCore.Razor.Language +{ + public abstract class RazorExtensionInitializer + { + public abstract void Initialize(RazorProjectEngineBuilder builder); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngine.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngine.cs index 09ae61dc1f..e76bdffc94 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectEngine.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language.Extensions; @@ -71,9 +72,17 @@ namespace Microsoft.AspNetCore.Razor.Language var builder = new DefaultRazorProjectEngineBuilder(configuration, fileSystem); + // The intialization order is somewhat important. + // + // Defaults -> Extensions -> Additional customization + // + // This allows extensions to rely on default features, and customizations to override choices made by + // extensions. RazorEngine.AddDefaultPhases(builder.Phases); AddDefaultsFeatures(builder.Features); + LoadExtensions(builder, configuration.Extensions); + configure?.Invoke(builder); return builder.Build(); @@ -142,19 +151,38 @@ namespace Microsoft.AspNetCore.Razor.Language }); } - internal static void AddDefaultRuntimeFeatures(RazorConfiguration configuration, ICollection features) + private static void LoadExtensions(RazorProjectEngineBuilder builder, IReadOnlyList extensions) { - // Configure options - features.Add(new DefaultRazorParserOptionsFeature(designTime: false, version: configuration.LanguageVersion)); - features.Add(new DefaultRazorCodeGenerationOptionsFeature(designTime: false)); - } + for (var i = 0; i < extensions.Count; i++) + { + // For now we only handle AssemblyExtension - which is not user-constructable. We're keeping a tight + // lid on how things work until we add official support for extensibility everywhere. So, this is + // intentionally inflexible for the time being. + var extension = extensions[i] as AssemblyExtension; + if (extension == null) + { + continue; + } - internal static void AddDefaultDesignTimeFeatures(RazorConfiguration configuration, ICollection features) - { - // Configure options - features.Add(new DefaultRazorParserOptionsFeature(designTime: true, version: configuration.LanguageVersion)); - features.Add(new DefaultRazorCodeGenerationOptionsFeature(designTime: true)); - features.Add(new SuppressChecksumOptionsFeature()); + // It's not an error to have an assembly with no initializers. This is useful to specify a dependency + // that doesn't really provide any Razor configuration. + var attributes = extension.Assembly.GetCustomAttributes(); + foreach (var attribute in attributes) + { + // Using extension names and requiring them to line up allows a single assembly to ship multiple + // extensions/initializers for different configurations. + if (!string.Equals(attribute.ExtensionName, extension.ExtensionName, StringComparison.Ordinal)) + { + continue; + } + + // There's no real protection/exception handling here because this set isn't really user-extensible + // right now. This would be a great place to add some additional diagnostics and hardening in the + // future. + var initializer = (RazorExtensionInitializer)Activator.CreateInstance(attribute.InitializerType); + initializer.Initialize(builder); + } + } } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectFileSystem.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectFileSystem.cs index a95de255df..799c6bda65 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorProjectFileSystem.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorProjectFileSystem.cs @@ -7,6 +7,8 @@ namespace Microsoft.AspNetCore.Razor.Language { public abstract class RazorProjectFileSystem : RazorProject { + internal static readonly RazorProjectFileSystem Empty = new EmptyProjectFileSystem(); + /// /// Create a Razor project file system based off of a root directory. /// diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs b/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs index 7389725f94..03cef74692 100644 --- a/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs +++ b/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs @@ -12,6 +12,18 @@ namespace Microsoft.AspNetCore.Razor.Tasks private const string GeneratedOutput = "GeneratedOutput"; private const string TargetPath = "TargetPath"; private const string FullPath = "FullPath"; + private const string Identity = "Identity"; + private const string AssemblyName = "AssemblyName"; + private const string AssemblyFilePath = "AssemblyFilePath"; + + [Required] + public string Version { get; set; } + + [Required] + public ITaskItem[] Configuration { get; set; } + + [Required] + public ITaskItem[] Extensions { get; set; } [Required] public ITaskItem[] Sources { get; set; } @@ -36,6 +48,16 @@ namespace Microsoft.AspNetCore.Razor.Tasks } } + for (var i = 0; i < Extensions.Length; i++) + { + if (!EnsureRequiredMetadata(Extensions[i], Identity) || + !EnsureRequiredMetadata(Extensions[i], AssemblyName) || + !EnsureRequiredMetadata(Extensions[i], AssemblyFilePath)) + { + return false; + } + } + return base.ValidateParameters(); } @@ -65,6 +87,21 @@ namespace Microsoft.AspNetCore.Razor.Tasks builder.AppendLine("-t"); builder.AppendLine(TagHelperManifest); + builder.AppendLine("-v"); + builder.AppendLine(Version); + + builder.AppendLine("-c"); + builder.AppendLine(Configuration[0].GetMetadata(Identity)); + + for (var i = 0; i < Extensions.Length; i++) + { + builder.AppendLine("-n"); + builder.AppendLine(Extensions[i].GetMetadata(Identity)); + + builder.AppendLine("-e"); + builder.AppendLine(Path.GetFullPath(Extensions[i].GetMetadata(AssemblyFilePath))); + } + return builder.ToString(); } diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs b/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs index 4064163c34..99d523a2b7 100644 --- a/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs +++ b/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs @@ -10,6 +10,19 @@ namespace Microsoft.AspNetCore.Razor.Tasks { public class RazorTagHelper : DotNetToolTask { + private const string Identity = "Identity"; + private const string AssemblyName = "AssemblyName"; + private const string AssemblyFilePath = "AssemblyFilePath"; + + [Required] + public string Version { get; set; } + + [Required] + public ITaskItem[] Configuration { get; set; } + + [Required] + public ITaskItem[] Extensions { get; set; } + [Required] public string[] Assemblies { get; set; } @@ -51,6 +64,21 @@ namespace Microsoft.AspNetCore.Razor.Tasks builder.AppendLine("-p"); builder.AppendLine(ProjectRoot); + builder.AppendLine("-v"); + builder.AppendLine(Version); + + builder.AppendLine("-c"); + builder.AppendLine(Configuration[0].GetMetadata(Identity)); + + for (var i = 0; i < Extensions.Length; i++) + { + builder.AppendLine("-n"); + builder.AppendLine(Extensions[i].GetMetadata(Identity)); + + builder.AppendLine("-e"); + builder.AppendLine(Path.GetFullPath(Extensions[i].GetMetadata(AssemblyFilePath))); + } + return builder.ToString(); } } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Application.cs b/src/Microsoft.AspNetCore.Razor.Tools/Application.cs index 483ab8c64c..06da6a9ec4 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/Application.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/Application.cs @@ -12,9 +12,11 @@ namespace Microsoft.AspNetCore.Razor.Tools { internal class Application : CommandLineApplication { - public Application(CancellationToken cancellationToken) + public Application(CancellationToken cancellationToken, ExtensionAssemblyLoader loader, ExtensionDependencyChecker checker) { CancellationToken = cancellationToken; + Checker = checker; + Loader = loader; Name = "rzc"; FullName = "Microsoft ASP.NET Core Razor CLI tool"; @@ -31,6 +33,10 @@ namespace Microsoft.AspNetCore.Razor.Tools public CancellationToken CancellationToken { get; } + public ExtensionAssemblyLoader Loader { get; } + + public ExtensionDependencyChecker Checker { get; } + public new int Execute(params string[] args) { try diff --git a/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs b/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs index c4b96df6b2..553e9f5a87 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs @@ -19,6 +19,21 @@ namespace Microsoft.AspNetCore.Razor.Tools private class DefaultCompilerHost : CompilerHost { + public DefaultCompilerHost() + { + // The loader needs to live for the lifetime of the server. + // + // This means that if a request tries to use a set of binaries that are inconsistent with what + // the server already has, then it will be rejected to try again on the client. + // + // We also check each set of extensions for missing depenencies individually, so that we can + // consistently reject a request that doesn't specify everything it needs. Otherwise the request + // could succeed sometimes if it relies on transient state. + Loader = new DefaultExtensionAssemblyLoader(Path.Combine(Path.GetTempPath(), "Razor-Server")); + } + + public ExtensionAssemblyLoader Loader { get; } + public override ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken) { if (!TryParseArguments(request, out var parsed)) @@ -28,28 +43,23 @@ namespace Microsoft.AspNetCore.Razor.Tools var exitCode = 0; var output = string.Empty; - var app = new Application(cancellationToken); var commandArgs = parsed.args.ToArray(); + var writer = ServerLogger.IsLoggingEnabled ? new StringWriter() : TextWriter.Null; + + var checker = new DefaultExtensionDependencyChecker(Loader, writer); + var app = new Application(cancellationToken, Loader, checker) + { + Out = writer, + Error = writer, + }; + + exitCode = app.Execute(commandArgs); + if (ServerLogger.IsLoggingEnabled) { - using (var writer = new StringWriter()) - { - app.Out = writer; - app.Error = writer; - exitCode = app.Execute(commandArgs); - output = writer.ToString(); - ServerLogger.Log(output); - } - } - else - { - using (var writer = new StreamWriter(Stream.Null)) - { - app.Out = writer; - app.Error = writer; - exitCode = app.Execute(commandArgs); - } + output = writer.ToString(); + ServerLogger.Log(output); } return new CompletedServerResponse(exitCode, utf8output: false, output: string.Empty); diff --git a/src/Microsoft.AspNetCore.Razor.Tools/DefaultExtensionAssemblyLoader.cs b/src/Microsoft.AspNetCore.Razor.Tools/DefaultExtensionAssemblyLoader.cs new file mode 100644 index 0000000000..05dc4a09eb --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/DefaultExtensionAssemblyLoader.cs @@ -0,0 +1,241 @@ +// 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.Collections.Immutable; +using System.IO; +using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using System.Runtime.Loader; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal class DefaultExtensionAssemblyLoader : ExtensionAssemblyLoader + { + private readonly string _baseDirectory; + + private readonly object _lock = new object(); + private readonly Dictionary _loadedByPath; + private readonly Dictionary _loadedByIdentity; + private readonly Dictionary _identityCache; + private readonly Dictionary> _wellKnownAssemblies; + + private ShadowCopyManager _shadowCopyManager; + + public DefaultExtensionAssemblyLoader(string baseDirectory) + { + _baseDirectory = baseDirectory; + + _loadedByPath = new Dictionary(StringComparer.OrdinalIgnoreCase); + _loadedByIdentity = new Dictionary(); + _identityCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + _wellKnownAssemblies = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + LoadContext = new ExtensionAssemblyLoadContext(AssemblyLoadContext.GetLoadContext(typeof(ExtensionAssemblyLoader).Assembly), this); + } + + protected AssemblyLoadContext LoadContext { get; } + + public override void AddAssemblyLocation(string filePath) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + if (!Path.IsPathRooted(filePath)) + { + throw new ArgumentException(nameof(filePath)); + } + + var assemblyName = Path.GetFileNameWithoutExtension(filePath); + lock (_lock) + { + if (!_wellKnownAssemblies.TryGetValue(assemblyName, out var paths)) + { + paths = new List(); + _wellKnownAssemblies.Add(assemblyName, paths); + } + + if (!paths.Contains(filePath)) + { + paths.Add(filePath); + } + } + } + + public override Assembly Load(string assemblyName) + { + if (!AssemblyIdentity.TryParseDisplayName(assemblyName, out var identity)) + { + return null; + } + + lock (_lock) + { + // First, check if this loader already loaded the requested assembly: + if (_loadedByIdentity.TryGetValue(identity, out var assembly)) + { + return assembly; + } + + // Second, check if an assembly file of the same simple name was registered with the loader: + if (_wellKnownAssemblies.TryGetValue(identity.Name, out var paths)) + { + // Multiple assemblies of the same simple name but different identities might have been registered. + // Load the one that matches the requested identity (if any). + foreach (var path in paths) + { + var candidateIdentity = GetIdentity(path); + + if (identity.Equals(candidateIdentity)) + { + return LoadFromPathUnsafe(path, candidateIdentity); + } + } + } + + // We only support loading by name from 'well-known' paths. If you need to load something by + // name and you get here, then that means we don't know where to look. + return null; + } + } + + public override Assembly LoadFromPath(string filePath) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + if (!Path.IsPathRooted(filePath)) + { + throw new ArgumentException(nameof(filePath)); + } + + lock (_lock) + { + return LoadFromPathUnsafe(filePath, identity: null); + } + } + + private Assembly LoadFromPathUnsafe(string filePath, AssemblyIdentity identity) + { + // If we've already loaded the assembly by path there should be nothing else to do, + // all of our data is up to date. + if (_loadedByPath.TryGetValue(filePath, out var entry)) + { + return entry.assembly; + } + + // If we've already loaded the assembly by identity, then we might has some updating + // to do. + identity = identity ?? GetIdentity(filePath); + if (identity != null && _loadedByIdentity.TryGetValue(identity, out var assembly)) + { + // An assembly file might be replaced by another file with a different identity. + // Last one wins. + _loadedByPath[filePath] = (assembly, identity); + return assembly; + } + + // Ok we don't have this cached. Let's actually try to load the assembly. + assembly = LoadFromPathUnsafeCore(CopyAssembly(filePath)); + + identity = identity ?? AssemblyIdentity.FromAssemblyDefinition(assembly); + + // It's possible an assembly was loaded by two different paths. Just use the original then. + if (_loadedByIdentity.TryGetValue(identity, out var duplicate)) + { + assembly = duplicate; + } + else + { + _loadedByIdentity.Add(identity, assembly); + } + + _loadedByPath[filePath] = (assembly, identity); + return assembly; + } + + private AssemblyIdentity GetIdentity(string filePath) + { + if (!_identityCache.TryGetValue(filePath, out var identity)) + { + identity = ReadAssemblyIdentity(filePath); + _identityCache.Add(filePath, identity); + } + + return identity; + } + + protected virtual string CopyAssembly(string filePath) + { + if (_baseDirectory == null) + { + // Don't shadow-copy when base directory is null. This means we're running as a CLI not + // a server. + return filePath; + } + + if (_shadowCopyManager == null) + { + _shadowCopyManager = new ShadowCopyManager(_baseDirectory); + } + + return _shadowCopyManager.AddAssembly(filePath); + } + + protected virtual Assembly LoadFromPathUnsafeCore(string filePath) + { + return LoadContext.LoadFromAssemblyPath(filePath); + } + + private static AssemblyIdentity ReadAssemblyIdentity(string filePath) + { + try + { + using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) + using (var reader = new PEReader(stream)) + { + var metadataReader = reader.GetMetadataReader(); + return metadataReader.GetAssemblyIdentity(); + } + } + catch + { + } + + return null; + } + + private class ExtensionAssemblyLoadContext : AssemblyLoadContext + { + private readonly AssemblyLoadContext _parent; + private readonly DefaultExtensionAssemblyLoader _loader; + + public ExtensionAssemblyLoadContext(AssemblyLoadContext parent, DefaultExtensionAssemblyLoader loader) + { + _parent = parent; + _loader = loader; + } + + protected override Assembly Load(AssemblyName assemblyName) + { + // Try to load from well-known paths. This will be called when loading a dependency of an extension. + var assembly = _loader.Load(assemblyName.ToString()); + if (assembly != null) + { + return assembly; + } + + // If we don't have an entry, then fall back to the default load context. This allows extensions + // to resolve assemblies that are provided by the host. + return _parent.LoadFromAssemblyName(assemblyName); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tools/DefaultExtensionDependencyChecker.cs b/src/Microsoft.AspNetCore.Razor.Tools/DefaultExtensionDependencyChecker.cs new file mode 100644 index 0000000000..f5ce49abac --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/DefaultExtensionDependencyChecker.cs @@ -0,0 +1,155 @@ +// 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.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal class DefaultExtensionDependencyChecker : ExtensionDependencyChecker + { + // These are treated as prefixes. So `Microsoft.CodeAnalysis.Razor` would be assumed to work. + private static readonly string[] DefaultIgnoredAssemblies = new string[] + { + "mscorlib", + "netstandard", + "System", + "Microsoft.CodeAnalysis", + "Microsoft.AspNetCore.Razor.Language", + }; + + private readonly ExtensionAssemblyLoader _loader; + private readonly TextWriter _output; + private readonly string[] _ignoredAssemblies; + + public DefaultExtensionDependencyChecker( + ExtensionAssemblyLoader loader, + TextWriter output, + string[] ignoredAssemblies = null) + { + _loader = loader; + _output = output; + _ignoredAssemblies = ignoredAssemblies ?? DefaultIgnoredAssemblies; + } + + public override bool Check(IEnumerable assmblyFilePaths) + { + try + { + return CheckCore(assmblyFilePaths); + } + catch (Exception ex) + { + _output.WriteLine("Exception performing Extension dependency check:"); + _output.WriteLine(ex.ToString()); + return false; + } + } + + private bool CheckCore(IEnumerable assemblyFilePaths) + { + var items = assemblyFilePaths.Select(a => ExtensionVerificationItem.Create(a)).ToArray(); + var assemblies = new HashSet(items.Select(i => i.Identity)); + + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + _output.WriteLine($"Verifying assembly at {item.FilePath}"); + + if (!Path.IsPathRooted(item.FilePath)) + { + _output.WriteLine($"The file path '{item.FilePath}' is not a rooted path. File paths must be absolute and fully-qualified."); + return false; + } + + foreach (var reference in item.References) + { + if (_ignoredAssemblies.Any(n => reference.Name.StartsWith(n))) + { + // This is on the allow list, keep going. + continue; + } + + if (assemblies.Contains(reference)) + { + // This was also provided as a dependency, keep going. + continue; + } + + // If we get here we can't resolve this assembly. This is an error. + _output.WriteLine($"Extension assembly '{item.Identity.Name}' depends on '{reference.ToString()} which is missing."); + return false; + } + } + + // Assuming we get this far, the set of assemblies we have is at least a coherent set (barring + // version conflicts). Register all of the paths with the loader so they can find each other by + // name. + for (var i = 0; i < items.Length; i++) + { + _loader.AddAssemblyLocation(items[i].FilePath); + } + + // Now try to load everything. This has the side effect of resolving all of these items + // in the loader's caches. + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + item.Assembly = _loader.LoadFromPath(item.FilePath); + } + + // Third, check that the MVIDs of the files on disk match the MVIDs of the loaded assemblies. + for (var i = 0; i < items.Length; i++) + { + var item = items[i]; + if (item.Mvid != item.Assembly.ManifestModule.ModuleVersionId) + { + _output.WriteLine($"Extension assembly '{item.Identity.Name}' at '{item.FilePath}' has a different ModuleVersionId than loaded assembly '{item.Assembly.FullName}'"); + return false; + } + } + + return true; + } + + private class ExtensionVerificationItem + { + public static ExtensionVerificationItem Create(string filePath) + { + using (var peReader = new PEReader(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))) + { + var metadataReader = peReader.GetMetadataReader(); + var identity = metadataReader.GetAssemblyIdentity(); + var mvid = metadataReader.GetGuid(metadataReader.GetModuleDefinition().Mvid); + var references = metadataReader.GetReferencedAssembliesOrThrow(); + + return new ExtensionVerificationItem(filePath, identity, mvid, references.ToArray()); + } + } + + private ExtensionVerificationItem(string filePath, AssemblyIdentity identity, Guid mvid, AssemblyIdentity[] references) + { + FilePath = filePath; + Identity = identity; + Mvid = mvid; + References = references; + } + + public string FilePath { get; } + + public Assembly Assembly { get; set; } + + public AssemblyIdentity Identity { get; } + + public Guid Mvid { get; } + + public IReadOnlyList References { get; } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs b/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs index 2b3cee612e..9ca5b3d8bb 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; @@ -26,6 +25,10 @@ namespace Microsoft.AspNetCore.Razor.Tools Assemblies = Argument("assemblies", "assemblies to search for tag helpers", multipleValues: true); TagHelperManifest = Option("-o", "output file", CommandOptionType.SingleValue); ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue); + Version = Option("-v|--version", "Razor language version", CommandOptionType.SingleValue); + Configuration = Option("-c", "Razor configuration name", CommandOptionType.SingleValue); + ExtensionNames = Option("-n", "extension name", CommandOptionType.MultipleValue); + ExtensionFilePaths = Option("-e", "extension file path", CommandOptionType.MultipleValue); } public CommandArgument Assemblies { get; } @@ -34,6 +37,14 @@ namespace Microsoft.AspNetCore.Razor.Tools public CommandOption ProjectDirectory { get; } + public CommandOption Version { get; } + + public CommandOption Configuration { get; } + + public CommandOption ExtensionNames { get; } + + public CommandOption ExtensionFilePaths { get; } + protected override bool ValidateArguments() { if (string.IsNullOrEmpty(TagHelperManifest.Value())) @@ -53,12 +64,61 @@ namespace Microsoft.AspNetCore.Razor.Tools ProjectDirectory.Values.Add(Environment.CurrentDirectory); } + if (string.IsNullOrEmpty(Version.Value())) + { + Error.WriteLine($"{Version.ValueName} must be specified."); + return false; + } + else if (!RazorLanguageVersion.TryParse(Version.Value(), out _)) + { + Error.WriteLine($"{Version.ValueName} is not a valid language version."); + return false; + } + + if (string.IsNullOrEmpty(Configuration.Value())) + { + Error.WriteLine($"{Configuration.ValueName} must be specified."); + return false; + } + + if (ExtensionNames.Values.Count != ExtensionFilePaths.Values.Count) + { + Error.WriteLine($"{ExtensionNames.ValueName} and {ExtensionFilePaths.ValueName} should have the same number of values."); + } + + foreach (var filePath in ExtensionFilePaths.Values) + { + if (!Path.IsPathRooted(filePath)) + { + Error.WriteLine($"Extension file paths must be fully-qualified, absolute paths."); + return false; + } + } + + if (!Parent.Checker.Check(ExtensionFilePaths.Values)) + { + Error.WriteLine($"Extenions could not be loaded. See output for details."); + return false; + } + return true; } protected override Task ExecuteCoreAsync() { + // Loading all of the extensions should succeed as the dependency checker will have already + // loaded them. + var extensions = new RazorExtension[ExtensionNames.Values.Count]; + for (var i = 0; i < ExtensionNames.Values.Count; i++) + { + extensions[i] = new AssemblyExtension(ExtensionNames.Values[i], Parent.Loader.LoadFromPath(ExtensionFilePaths.Values[i])); + } + + var version = RazorLanguageVersion.Parse(Version.Value()); + var configuration = new RazorConfiguration(version, Configuration.Value(), extensions); + var result = ExecuteCore( + configuration: configuration, projectDirectory: ProjectDirectory.Value(), outputFilePath: TagHelperManifest.Value(), assemblies: Assemblies.Values.ToArray()); @@ -66,7 +126,7 @@ namespace Microsoft.AspNetCore.Razor.Tools return Task.FromResult(result); } - private int ExecuteCore(string projectDirectory, string outputFilePath, string[] assemblies) + private int ExecuteCore(RazorConfiguration configuration, string projectDirectory, string outputFilePath, string[] assemblies) { outputFilePath = Path.Combine(projectDirectory, outputFilePath); @@ -76,19 +136,14 @@ namespace Microsoft.AspNetCore.Razor.Tools metadataReferences[i] = MetadataReference.CreateFromFile(assemblies[i]); } - var engine = RazorEngine.Create((b) => + var engine = RazorProjectEngine.Create(configuration, RazorProjectFileSystem.Empty, b => { - RazorExtensions.Register(b); - b.Features.Add(new DefaultMetadataReferenceFeature() { References = metadataReferences }); b.Features.Add(new CompilationTagHelperFeature()); - - // TagHelperDescriptorProviders (actually do tag helper discovery) b.Features.Add(new DefaultTagHelperDescriptorProvider()); - b.Features.Add(new ViewComponentTagHelperDescriptorProvider()); }); - var feature = engine.Features.OfType().Single(); + var feature = engine.Engine.Features.OfType().Single(); var tagHelpers = feature.GetDescriptors(); using (var stream = new MemoryStream()) diff --git a/src/Microsoft.AspNetCore.Razor.Tools/ExtensionAssemblyLoader.cs b/src/Microsoft.AspNetCore.Razor.Tools/ExtensionAssemblyLoader.cs new file mode 100644 index 0000000000..071eec2f82 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/ExtensionAssemblyLoader.cs @@ -0,0 +1,16 @@ +// 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.Reflection; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal abstract class ExtensionAssemblyLoader + { + public abstract void AddAssemblyLocation(string filePath); + + public abstract Assembly Load(string assemblyName); + + public abstract Assembly LoadFromPath(string filePath); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tools/ExtensionDependencyChecker.cs b/src/Microsoft.AspNetCore.Razor.Tools/ExtensionDependencyChecker.cs new file mode 100644 index 0000000000..02fd86d9e8 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/ExtensionDependencyChecker.cs @@ -0,0 +1,12 @@ +// 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.Collections.Generic; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal abstract class ExtensionDependencyChecker + { + public abstract bool Check(IEnumerable extensionFilePaths); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs b/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs index 0981cc1a03..d19c566b76 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc.Razor.Extensions; using Microsoft.AspNetCore.Razor.Language; using Microsoft.Extensions.CommandLineUtils; using Microsoft.VisualStudio.LanguageServices.Razor; @@ -14,13 +13,6 @@ using Newtonsoft.Json; namespace Microsoft.AspNetCore.Razor.Tools { - internal class Builder - { - public static Builder Make(CommandBase result) => null; - - public static Builder Make(T result) => null; - } - internal class GenerateCommand : CommandBase { public GenerateCommand(Application parent) @@ -31,6 +23,10 @@ namespace Microsoft.AspNetCore.Razor.Tools RelativePaths = Option("-r", "Relative path", CommandOptionType.MultipleValue); ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue); TagHelperManifest = Option("-t", "tag helper manifest file", CommandOptionType.SingleValue); + Version = Option("-v|--version", "Razor language version", CommandOptionType.SingleValue); + Configuration = Option("-c", "Razor configuration name", CommandOptionType.SingleValue); + ExtensionNames = Option("-n", "extension name", CommandOptionType.MultipleValue); + ExtensionFilePaths = Option("-e", "extension file path", CommandOptionType.MultipleValue); } public CommandOption Sources { get; } @@ -43,9 +39,29 @@ namespace Microsoft.AspNetCore.Razor.Tools public CommandOption TagHelperManifest { get; } + public CommandOption Version { get; } + + public CommandOption Configuration { get; } + + public CommandOption ExtensionNames { get; } + + public CommandOption ExtensionFilePaths { get; } + protected override Task ExecuteCoreAsync() { + // Loading all of the extensions should succeed as the dependency checker will have already + // loaded them. + var extensions = new RazorExtension[ExtensionNames.Values.Count]; + for (var i = 0; i < ExtensionNames.Values.Count; i++) + { + extensions[i] = new AssemblyExtension(ExtensionNames.Values[i], Parent.Loader.LoadFromPath(ExtensionFilePaths.Values[i])); + } + + var version = RazorLanguageVersion.Parse(Version.Value()); + var configuration = new RazorConfiguration(version, Configuration.Value(), extensions); + var result = ExecuteCore( + configuration: configuration, projectDirectory: ProjectDirectory.Value(), tagHelperManifest: TagHelperManifest.Value(), sources: Sources.Values, @@ -78,10 +94,48 @@ namespace Microsoft.AspNetCore.Razor.Tools ProjectDirectory.Values.Add(Environment.CurrentDirectory); } + if (string.IsNullOrEmpty(Version.Value())) + { + Error.WriteLine($"{Version.ValueName} must be specified."); + return false; + } + else if (!RazorLanguageVersion.TryParse(Version.Value(), out _)) + { + Error.WriteLine($"{Version.ValueName} is not a valid language version."); + return false; + } + + if (string.IsNullOrEmpty(Configuration.Value())) + { + Error.WriteLine($"{Configuration.ValueName} must be specified."); + return false; + } + + if (ExtensionNames.Values.Count != ExtensionFilePaths.Values.Count) + { + Error.WriteLine($"{ExtensionNames.ValueName} and {ExtensionFilePaths.ValueName} should have the same number of values."); + } + + foreach (var filePath in ExtensionFilePaths.Values) + { + if (!Path.IsPathRooted(filePath)) + { + Error.WriteLine($"Extension file paths must be fully-qualified, absolute paths."); + return false; + } + } + + if (!Parent.Checker.Check(ExtensionFilePaths.Values)) + { + Error.WriteLine($"Extensions could not be loaded. See output for details."); + return false; + } + return true; } private int ExecuteCore( + RazorConfiguration configuration, string projectDirectory, string tagHelperManifest, List sources, @@ -97,14 +151,13 @@ namespace Microsoft.AspNetCore.Razor.Tools GetVirtualRazorProjectSystem(inputItems), RazorProjectFileSystem.Create(projectDirectory), }); - var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, compositeFileSystem, b => + + var engine = RazorProjectEngine.Create(configuration, compositeFileSystem, b => { - RazorExtensions.Register(b); - b.Features.Add(new StaticTagHelperFeature() { TagHelpers = tagHelpers, }); }); - var results = GenerateCode(projectEngine, inputItems); + var results = GenerateCode(engine, inputItems); var success = true; @@ -175,14 +228,14 @@ namespace Microsoft.AspNetCore.Razor.Tools return items; } - private OutputItem[] GenerateCode(RazorProjectEngine projectEngine, SourceItem[] inputs) + private OutputItem[] GenerateCode(RazorProjectEngine engine, SourceItem[] inputs) { var outputs = new OutputItem[inputs.Length]; Parallel.For(0, outputs.Length, new ParallelOptions() { MaxDegreeOfParallelism = Debugger.IsAttached ? 1 : 4 }, i => { var inputItem = inputs[i]; - var projectItem = projectEngine.FileSystem.GetItem(inputItem.FilePath); - var codeDocument = projectEngine.Process(projectItem); + + var codeDocument = engine.Process(engine.FileSystem.GetItem(inputItem.FilePath)); var csharpDocument = codeDocument.GetCSharpDocument(); outputs[i] = new OutputItem(inputItem, csharpDocument); }); diff --git a/src/Microsoft.AspNetCore.Razor.Tools/MetadataReaderExtensions.cs b/src/Microsoft.AspNetCore.Razor.Tools/MetadataReaderExtensions.cs new file mode 100644 index 0000000000..da1bddb865 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/MetadataReaderExtensions.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft. 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.Collections.Immutable; +using System.Reflection; +using System.Reflection.Metadata; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal static class MetadataReaderExtensions + { + internal static AssemblyIdentity GetAssemblyIdentity(this MetadataReader reader) + { + if (!reader.IsAssembly) + { + throw new BadImageFormatException(); + } + + var definition = reader.GetAssemblyDefinition(); + + return CreateAssemblyIdentity( + reader, + definition.Version, + definition.Flags, + definition.PublicKey, + definition.Name, + definition.Culture, + isReference: false); + } + + internal static AssemblyIdentity[] GetReferencedAssembliesOrThrow(this MetadataReader reader) + { + var references = new List(); + + foreach (var referenceHandle in reader.AssemblyReferences) + { + var reference = reader.GetAssemblyReference(referenceHandle); + references.Add(CreateAssemblyIdentity( + reader, + reference.Version, + reference.Flags, + reference.PublicKeyOrToken, + reference.Name, + reference.Culture, + isReference: true)); + } + + return references.ToArray(); + } + + private static AssemblyIdentity CreateAssemblyIdentity( + MetadataReader reader, + Version version, + AssemblyFlags flags, + BlobHandle publicKey, + StringHandle name, + StringHandle culture, + bool isReference) + { + var publicKeyOrToken = reader.GetBlobContent(publicKey); + bool hasPublicKey; + + if (isReference) + { + hasPublicKey = (flags & AssemblyFlags.PublicKey) != 0; + } + else + { + // Assembly definitions never contain a public key token, they only can have a full key or nothing, + // so the flag AssemblyFlags.PublicKey does not make sense for them and is ignored. + // See Ecma-335, Partition II Metadata, 22.2 "Assembly : 0x20". + // This also corresponds to the behavior of the native C# compiler and sn.exe tool. + hasPublicKey = !publicKeyOrToken.IsEmpty; + } + + if (publicKeyOrToken.IsEmpty) + { + publicKeyOrToken = default; + } + + return new AssemblyIdentity( + name: reader.GetString(name), + version: version, + cultureName: culture.IsNil ? null : reader.GetString(culture), + publicKeyOrToken: publicKeyOrToken, + hasPublicKey: hasPublicKey, + isRetargetable: (flags & AssemblyFlags.Retargetable) != 0, + contentType: (AssemblyContentType)((int)(flags & AssemblyFlags.ContentTypeMask) >> 9)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj b/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj index fe2b7c52b9..6aae7e8d50 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj +++ b/src/Microsoft.AspNetCore.Razor.Tools/Microsoft.AspNetCore.Razor.Tools.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Program.cs b/src/Microsoft.AspNetCore.Razor.Tools/Program.cs index 8a23dd91b9..0b3b980c3a 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/Program.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/Program.cs @@ -15,7 +15,11 @@ namespace Microsoft.AspNetCore.Razor.Tools var cancel = new CancellationTokenSource(); Console.CancelKeyPress += (sender, e) => { cancel.Cancel(); }; - var application = new Application(cancel.Token); + // Prevent shadow copying. + var loader = new DefaultExtensionAssemblyLoader(baseDirectory: null); + var checker = new DefaultExtensionDependencyChecker(loader, Console.Error); + + var application = new Application(cancel.Token, loader, checker); return application.Execute(args); } } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/ShadowCopyManager.cs b/src/Microsoft.AspNetCore.Razor.Tools/ShadowCopyManager.cs new file mode 100644 index 0000000000..317dc5bd74 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/ShadowCopyManager.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.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + // Note that this class has no thread-safety guarantees. The caller should use a lock + // if concurrency is required. + internal class ShadowCopyManager : IDisposable + { + // Note that this class uses the *existance* of the Mutex to lock a directory. + // + // Nothing in this code actually ever acquires the Mutex, we just try to see if it exists + // already. + private readonly Mutex _mutex; + + private int _counter; + + public ShadowCopyManager(string baseDirectory = null) + { + BaseDirectory = baseDirectory ?? Path.Combine(Path.GetTempPath(), "Razor", "ShadowCopy"); + + var guid = Guid.NewGuid().ToString("N").ToLowerInvariant(); + UniqueDirectory = Path.Combine(BaseDirectory, guid); + + _mutex = new Mutex(initiallyOwned: false, name: guid); + + Directory.CreateDirectory(UniqueDirectory); + } + + public string BaseDirectory { get; } + + public string UniqueDirectory { get; } + + public string AddAssembly(string filePath) + { + var assemblyDirectory = CreateUniqueDirectory(); + + var destination = Path.Combine(assemblyDirectory, Path.GetFileName(filePath)); + CopyFile(filePath, destination); + + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath); + var resourcesNameWithoutExtension = fileNameWithoutExtension + ".resources"; + var resourcesNameWithExtension = resourcesNameWithoutExtension + ".dll"; + + foreach (var directory in Directory.EnumerateDirectories(Path.GetDirectoryName(filePath))) + { + var directoryName = Path.GetFileName(directory); + + var resourcesPath = Path.Combine(directory, resourcesNameWithExtension); + if (File.Exists(resourcesPath)) + { + var resourcesShadowCopyPath = Path.Combine(assemblyDirectory, directoryName, resourcesNameWithExtension); + CopyFile(resourcesPath, resourcesShadowCopyPath); + } + + resourcesPath = Path.Combine(directory, resourcesNameWithoutExtension, resourcesNameWithExtension); + if (File.Exists(resourcesPath)) + { + var resourcesShadowCopyPath = Path.Combine(assemblyDirectory, directoryName, resourcesNameWithoutExtension, resourcesNameWithExtension); + CopyFile(resourcesPath, resourcesShadowCopyPath); + } + } + + return destination; + } + + public void Dispose() + { + _mutex.ReleaseMutex(); + } + + public Task PurgeUnusedDirectoriesAsync() + { + return Task.Run((Action)PurgeUnusedDirectories); + } + + private string CreateUniqueDirectory() + { + var id = _counter++; + + var directory = Path.Combine(UniqueDirectory, id.ToString()); + Directory.CreateDirectory(directory); + return directory; + } + + private void CopyFile(string originalPath, string shadowCopyPath) + { + var directory = Path.GetDirectoryName(shadowCopyPath); + Directory.CreateDirectory(directory); + + File.Copy(originalPath, shadowCopyPath); + + MakeWritable(new FileInfo(shadowCopyPath)); + } + + private void MakeWritable(string directoryPath) + { + var directory = new DirectoryInfo(directoryPath); + + foreach (var file in directory.EnumerateFiles(searchPattern: "*", searchOption: SearchOption.AllDirectories)) + { + MakeWritable(file); + } + } + + private void MakeWritable(FileInfo file) + { + try + { + if (file.IsReadOnly) + { + file.IsReadOnly = false; + } + } + catch + { + // There are many reasons this could fail. Ignore it and keep going. + } + } + + private void PurgeUnusedDirectories() + { + IEnumerable directories; + try + { + directories = Directory.EnumerateDirectories(BaseDirectory); + } + catch (DirectoryNotFoundException) + { + return; + } + + foreach (var directory in directories) + { + Mutex mutex = null; + try + { + // We only want to try deleting the directory if no-one else is currently using it. + // + // Note that the mutex name is the name of the directory. This is OK because we're using + // GUIDs as directory/mutex names. + if (!Mutex.TryOpenExisting(Path.GetFileName(directory).ToLowerInvariant(), out mutex)) + { + MakeWritable(directory); + Directory.Delete(directory, recursive: true); + } + } + catch + { + // If something goes wrong we will leave it to the next run to clean up. + // Just swallow the exception and move on. + } + finally + { + if (mutex != null) + { + mutex.Dispose(); + } + } + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerTestFixture.cs b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerTestFixture.cs index f63de011a4..d2d0aa98e5 100644 --- a/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerTestFixture.cs +++ b/test/Microsoft.AspNetCore.Razor.Design.Test/IntegrationTests/BuildServerTestFixture.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Threading; using Microsoft.AspNetCore.Razor.Tools; +using Moq; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { @@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests throw new TimeoutException($"Shutting down the build server at pipe {PipeName} took longer than expected."); }); - var application = new Application(cts.Token); + var application = new Application(cts.Token, Mock.Of(), Mock.Of()); var exitCode = application.Execute("shutdown", "-w", "-p", PipeName); if (exitCode != 0) { diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs index 39ff443eb5..f16fff0274 100644 --- a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs +++ b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/TestRazorProjectFileSystem.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Razor.Language { internal class TestRazorProjectFileSystem : DefaultRazorProjectFileSystem { - public static RazorProjectFileSystem Empty = new TestRazorProjectFileSystem(); + public new static RazorProjectFileSystem Empty = new TestRazorProjectFileSystem(); private readonly Dictionary _lookup; diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultExtensionAssemblyLoaderTest.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultExtensionAssemblyLoaderTest.cs new file mode 100644 index 0000000000..ad619e5570 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultExtensionAssemblyLoaderTest.cs @@ -0,0 +1,128 @@ +// 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.IO; +using System.Text; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + public class DefaultExtensionAssemblyLoaderTest + { + [Fact] + public void LoadFromPath_CanLoadAssembly() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + + // Act + var assembly = loader.LoadFromPath(alphaFilePath); + + // Assert + Assert.NotNull(assembly); + } + } + + [Fact] + public void LoadFromPath_DoesNotAddDuplicates_AfterLoadingByName() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + var alphaFilePath2 = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha2.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + loader.AddAssemblyLocation(alphaFilePath); + + var assembly1 = loader.Load("Alpha"); + + // Act + var assembly2 = loader.LoadFromPath(alphaFilePath2); + + // Assert + Assert.Same(assembly1, assembly2); + } + } + + [Fact] + public void LoadFromPath_DoesNotAddDuplicates_AfterLoadingByPath() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + var alphaFilePath2 = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha2.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + var assembly1 = loader.LoadFromPath(alphaFilePath); + + // Act + var assembly2 = loader.LoadFromPath(alphaFilePath2); + + // Assert + Assert.Same(assembly1, assembly2); + } + } + + [Fact] + public void Load_CanLoadAssemblyByName_AfterLoadingByPath() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + var assembly1 = loader.LoadFromPath(alphaFilePath); + + // Act + var assembly2 = loader.Load(assembly1.FullName); + + // Assert + Assert.Same(assembly1, assembly2); + } + } + + [Fact] + public void LoadFromPath_WithDependencyPathsSpecified_CanLoadAssemblyDependencies() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + var betaFilePath = LoaderTestResources.Beta.WriteToFile(directory.DirectoryPath, "Beta.dll"); + var gammaFilePath = LoaderTestResources.Gamma.WriteToFile(directory.DirectoryPath, "Gamma.dll"); + var deltaFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + loader.AddAssemblyLocation(gammaFilePath); + loader.AddAssemblyLocation(deltaFilePath); + + // Act + var alpha = loader.LoadFromPath(alphaFilePath); + var beta = loader.LoadFromPath(betaFilePath); + + // Assert + var builder = new StringBuilder(); + + var a = alpha.CreateInstance("Alpha.A"); + a.GetType().GetMethod("Write").Invoke(a, new object[] { builder, "Test A" }); + + var b = beta.CreateInstance("Beta.B"); + b.GetType().GetMethod("Write").Invoke(b, new object[] { builder, "Test B" }); + var expected = @"Delta: Gamma: Alpha: Test A +Delta: Gamma: Beta: Test B +"; + + var actual = builder.ToString(); + + Assert.Equal(expected, actual); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultExtensionDependencyCheckerTest.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultExtensionDependencyCheckerTest.cs new file mode 100644 index 0000000000..72d719fdc8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/DefaultExtensionDependencyCheckerTest.cs @@ -0,0 +1,111 @@ +// 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 Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + public class DefaultExtensionDependencyCheckerTest + { + [Fact] + public void Check_ReturnsFalse_WithMissingDependency() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var output = new StringWriter(); + + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + var checker = new DefaultExtensionDependencyChecker(loader, output); + + // Act + var result = checker.Check(new[] { alphaFilePath, }); + + // Assert + Assert.False(result, "Check should not have passed: " + output.ToString()); + } + } + + [Fact] + public void Check_ReturnsTrue_WithAllDependenciesProvided() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var output = new StringWriter(); + + var alphaFilePath = LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + var betaFilePath = LoaderTestResources.Beta.WriteToFile(directory.DirectoryPath, "Beta.dll"); + var gammaFilePath = LoaderTestResources.Gamma.WriteToFile(directory.DirectoryPath, "Gamma.dll"); + var deltaFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + var checker = new DefaultExtensionDependencyChecker(loader, output); + + // Act + var result = checker.Check(new[] { alphaFilePath, betaFilePath, gammaFilePath, deltaFilePath, }); + + // Assert + Assert.True(result, "Check should have passed: " + output.ToString()); + } + } + + [Fact] + public void Check_ReturnsFalse_WhenAssemblyHasDifferentMVID() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var output = new StringWriter(); + + // Load Beta.dll from the future Alpha.dll path to prime the assembly loader + var alphaFilePath = LoaderTestResources.Beta.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + var betaFilePath = LoaderTestResources.Beta.WriteToFile(directory.DirectoryPath, "Beta.dll"); + var gammaFilePath = LoaderTestResources.Gamma.WriteToFile(directory.DirectoryPath, "Gamma.dll"); + var deltaFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + var loader = new TestDefaultExtensionAssemblyLoader(Path.Combine(directory.DirectoryPath, "shadow")); + var checker = new DefaultExtensionDependencyChecker(loader, output); + + // This will cause the loader to cache some inconsistent information. + loader.LoadFromPath(alphaFilePath); + LoaderTestResources.Alpha.WriteToFile(directory.DirectoryPath, "Alpha.dll"); + + // Act + var result = checker.Check(new[] { alphaFilePath, gammaFilePath, deltaFilePath, }); + + // Assert + Assert.False(result, "Check should not have passed: " + output.ToString()); + } + } + + [Fact] + public void Check_ReturnsFalse_WhenLoaderThrows() + { + using (var directory = TempDirectory.Create()) + { + // Arrange + var output = new StringWriter(); + + var deltaFilePath = LoaderTestResources.Delta.WriteToFile(directory.DirectoryPath, "Delta.dll"); + + var loader = new Mock(); + loader + .Setup(l => l.LoadFromPath(It.IsAny())) + .Throws(new InvalidOperationException()); + var checker = new DefaultExtensionDependencyChecker(loader.Object, output); + + // Act + var result = checker.Check(new[] { deltaFilePath, }); + + // Assert + Assert.False(result, "Check should not have passed: " + output.ToString()); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/ServerUtilities.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/ServerUtilities.cs index 473b608142..b91723c9d0 100644 --- a/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/ServerUtilities.cs +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/Infrastructure/ServerUtilities.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using Moq; namespace Microsoft.AspNetCore.Razor.Tools { @@ -116,7 +117,7 @@ namespace Microsoft.AspNetCore.Razor.Tools CancellationToken ct, EventBus eventBus, TimeSpan? keepAlive) - : base(new Application(ct)) + : base(new Application(ct, Mock.Of(), Mock.Of())) { _host = host; _compilerHost = compilerHost; diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/LoaderTestResources.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/LoaderTestResources.cs new file mode 100644 index 0000000000..9478c46684 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/LoaderTestResources.cs @@ -0,0 +1,146 @@ +// 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 System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal static class LoaderTestResources + { + static LoaderTestResources() + { + Delta = CreateAssemblyBlob("Delta", Array.Empty(), @" +using System.Text; + +namespace Delta +{ + public class D + { + public void Write(StringBuilder sb, string s) + { + sb.AppendLine(""Delta: "" + s); + } + } +} +"); + + Gamma = CreateAssemblyBlob("Gamma", new[] { Delta, }, @" +using System.Text; +using Delta; + +namespace Gamma +{ + public class G + { + public void Write(StringBuilder sb, string s) + { + D d = new D(); + + d.Write(sb, ""Gamma: "" + s); + } + } +} +"); + + Alpha = CreateAssemblyBlob("Alpha", new[] { Gamma, }, @" +using System.Text; +using Gamma; + +namespace Alpha +{ + public class A + { + public void Write(StringBuilder sb, string s) + { + G g = new G(); + + g.Write(sb, ""Alpha: "" + s); + } + } +} +"); + + Beta = CreateAssemblyBlob("Beta", new[] { Gamma, }, @" +using System.Text; +using Gamma; + +namespace Beta +{ + public class B + { + public void Write(StringBuilder sb, string s) + { + G g = new G(); + + g.Write(sb, ""Beta: "" + s); + } + } +} +"); + } + + public static AssemblyBlob Alpha { get; } + + public static AssemblyBlob Beta { get; } + + public static AssemblyBlob Delta { get; } + + public static AssemblyBlob Gamma { get; } + + private static AssemblyBlob CreateAssemblyBlob(string assemblyName, AssemblyBlob[] references, string text) + { + var defaultReferences = new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + }; + + var compilation = CSharpCompilation.Create( + assemblyName, + new[] { CSharpSyntaxTree.ParseText(text) }, + references.Select(r => r.ToMetadataReference()).Concat(defaultReferences), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + using (var assemblyStream = new MemoryStream()) + using (var symbolStream = new MemoryStream()) + { + var result = compilation.Emit(assemblyStream, symbolStream); + Assert.Empty(result.Diagnostics); + + return new AssemblyBlob(assemblyName, assemblyStream.GetBuffer(), symbolStream.GetBuffer()); + } + } + + public class AssemblyBlob + { + public AssemblyBlob(string assemblyName, byte[] assemblyBytes, byte[] symbolBytes) + { + AssemblyName = assemblyName; + AssemblyBytes = assemblyBytes; + SymbolBytes = symbolBytes; + } + + public string AssemblyName { get; } + + public byte[] AssemblyBytes { get; } + + public byte[] SymbolBytes { get; } + + public MetadataReference ToMetadataReference() + { + return MetadataReference.CreateFromImage(AssemblyBytes); + } + + internal string WriteToFile(string directoryPath, string fileName) + { + var filePath = Path.Combine(directoryPath, fileName); + File.WriteAllBytes(filePath, AssemblyBytes); + return filePath; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/Microsoft.AspNetCore.Razor.Tools.Test.csproj b/test/Microsoft.AspNetCore.Razor.Tools.Test/Microsoft.AspNetCore.Razor.Tools.Test.csproj index 3778c1c8ca..df0bf3822a 100644 --- a/test/Microsoft.AspNetCore.Razor.Tools.Test/Microsoft.AspNetCore.Razor.Tools.Test.csproj +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/Microsoft.AspNetCore.Razor.Tools.Test.csproj @@ -17,4 +17,10 @@ + + + System + + + diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/Properties/AssemblyInfo.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..f0aa552b16 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/TempDirectory.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/TempDirectory.cs new file mode 100644 index 0000000000..d491248465 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/TempDirectory.cs @@ -0,0 +1,30 @@ +// 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; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal class TempDirectory : IDisposable + { + public static TempDirectory Create() + { + var directoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); + Directory.CreateDirectory(directoryPath); + return new TempDirectory(directoryPath); + } + + private TempDirectory(string directoryPath) + { + DirectoryPath = directoryPath; + } + + public string DirectoryPath { get; } + + public void Dispose() + { + Directory.Delete(DirectoryPath, recursive: true); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Tools.Test/TestDefaultExtensionAssemblyLoader.cs b/test/Microsoft.AspNetCore.Razor.Tools.Test/TestDefaultExtensionAssemblyLoader.cs new file mode 100644 index 0000000000..d272e1c005 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Tools.Test/TestDefaultExtensionAssemblyLoader.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.IO; +using System.Reflection; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal class TestDefaultExtensionAssemblyLoader : DefaultExtensionAssemblyLoader + { + public TestDefaultExtensionAssemblyLoader(string baseDirectory) + : base(baseDirectory) + { + } + + protected override Assembly LoadFromPathUnsafeCore(string filePath) + { + // Force a load from streams so we don't lock the files on disk. This way we can test + // shadow copying without leaving a mess behind. + var bytes = File.ReadAllBytes(filePath); + var stream = new MemoryStream(bytes); + return LoadContext.LoadFromStream(stream); + } + } +} diff --git a/test/testapps/AppWithP2PReference/AppWithP2PReference.csproj b/test/testapps/AppWithP2PReference/AppWithP2PReference.csproj index fe9d5d2940..6688f30d79 100644 --- a/test/testapps/AppWithP2PReference/AppWithP2PReference.csproj +++ b/test/testapps/AppWithP2PReference/AppWithP2PReference.csproj @@ -7,6 +7,11 @@ + + + + <_MvcExtensionAssemblyPath>$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\bin\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll + diff --git a/test/testapps/ClassLibrary/ClassLibrary.csproj b/test/testapps/ClassLibrary/ClassLibrary.csproj index 443d790efa..1aa8072b38 100644 --- a/test/testapps/ClassLibrary/ClassLibrary.csproj +++ b/test/testapps/ClassLibrary/ClassLibrary.csproj @@ -8,6 +8,11 @@ + + + + <_MvcExtensionAssemblyPath>$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\bin\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll + diff --git a/test/testapps/SimpleMvc/SimpleMvc.csproj b/test/testapps/SimpleMvc/SimpleMvc.csproj index 31c006b2f3..4e20c9b1c8 100644 --- a/test/testapps/SimpleMvc/SimpleMvc.csproj +++ b/test/testapps/SimpleMvc/SimpleMvc.csproj @@ -8,6 +8,11 @@ + + + + <_MvcExtensionAssemblyPath>$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\bin\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll + diff --git a/test/testapps/SimplePages/SimplePages.csproj b/test/testapps/SimplePages/SimplePages.csproj index 340a6f14db..654b8f63dc 100644 --- a/test/testapps/SimplePages/SimplePages.csproj +++ b/test/testapps/SimplePages/SimplePages.csproj @@ -8,6 +8,11 @@ + + + + <_MvcExtensionAssemblyPath>$(SolutionRoot)src\Microsoft.AspNetCore.Mvc.Razor.Extensions\bin\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Mvc.Razor.Extensions.dll +