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
+