diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs index 6426753d4b..d3b0f0fc8a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs @@ -12,8 +12,27 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts /// /// An backed by an . /// - public class AssemblyPart : ApplicationPart, IApplicationPartTypeProvider, ICompilationReferencesProvider + public class AssemblyPart : + ApplicationPart, + IApplicationPartTypeProvider, + ICompilationReferencesProvider, + IViewsProvider { + /// + /// Gets the suffix for the view assembly. + /// + public static readonly string PrecompiledViewsAssemblySuffix = ".PrecompiledViews"; + + /// + /// Gets the namespace for the type in the view assembly. + /// + public static readonly string ViewInfoContainerNamespace = "AspNetCore"; + + /// + /// Gets the type name for the view collection type in the view assembly. + /// + public static readonly string ViewInfoContainerTypeName = "__PrecompiledViewCollection"; + /// /// Initalizes a new instance. /// @@ -41,6 +60,27 @@ public AssemblyPart(Assembly assembly) /// public IEnumerable Types => Assembly.DefinedTypes; + /// + public IEnumerable Views + { + get + { + var precompiledAssemblyName = new AssemblyName(Assembly.FullName); + precompiledAssemblyName.Name = precompiledAssemblyName.Name + PrecompiledViewsAssemblySuffix; + + var typeName = $"{ViewInfoContainerNamespace}.{ViewInfoContainerTypeName},{precompiledAssemblyName}"; + var viewInfoContainerTypeName = Type.GetType(typeName); + + if (viewInfoContainerTypeName == null) + { + return null; + } + + var precompiledViews = (ViewInfoContainer)Activator.CreateInstance(viewInfoContainerTypeName); + return precompiledViews.ViewInfos; + } + } + /// public IEnumerable GetReferencePaths() { diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IViewsProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IViewsProvider.cs new file mode 100644 index 0000000000..862401593e --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IViewsProvider.cs @@ -0,0 +1,18 @@ +// 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.Mvc.ApplicationParts +{ + /// + /// Exposes a sequence of views associated with an . + /// + public interface IViewsProvider + { + /// + /// Gets the sequence of . + /// + IEnumerable Views { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ViewInfo.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ViewInfo.cs new file mode 100644 index 0000000000..eea6f630bc --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ViewInfo.cs @@ -0,0 +1,34 @@ +// 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.Mvc.ApplicationParts +{ + /// + /// Provides information for precompiled views. + /// + public class ViewInfo + { + /// + /// Creates a new instance of . + /// + /// The path of the view. + /// The view . + public ViewInfo(string path, Type type) + { + Path = path; + Type = type; + } + + /// + /// The path of the view. + /// + public string Path { get; } + + /// + /// The view . + /// + public Type Type { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ViewInfoContainer.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ViewInfoContainer.cs new file mode 100644 index 0000000000..22c5d39f22 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ViewInfoContainer.cs @@ -0,0 +1,27 @@ +// 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.Mvc.ApplicationParts +{ + /// + /// A container for instances. + /// + public class ViewInfoContainer + { + /// + /// Initializes a new instance of . + /// + /// The sequence of . + public ViewInfoContainer(IReadOnlyList views) + { + ViewInfos = views; + } + + /// + /// The of . + /// + public IReadOnlyList ViewInfos { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/IDesignTimeMvcBuilderConfiguration.cs b/src/Microsoft.AspNetCore.Mvc.Core/IDesignTimeMvcBuilderConfiguration.cs new file mode 100644 index 0000000000..312642b052 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/IDesignTimeMvcBuilderConfiguration.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// Configures the . Implement this interface to enable design-time configuration + /// (for instance during pre-compilation of views) of . + /// + public interface IDesignTimeMvcBuilderConfiguration + { + /// + /// Configures the . + /// + /// The . + void ConfigureMvc(IMvcBuilder builder); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeature.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeature.cs new file mode 100644 index 0000000000..3f1b5045ae --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeature.cs @@ -0,0 +1,14 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.Razor.Compilation +{ + public class ViewsFeature + { + public IDictionary Views { get; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs new file mode 100644 index 0000000000..360992886d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.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.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ApplicationParts; + +namespace Microsoft.AspNetCore.Mvc.Razor.Compilation +{ + /// + /// An for . + /// + public class ViewsFeatureProvider : IApplicationFeatureProvider + { + /// + public void PopulateFeature(IEnumerable parts, ViewsFeature feature) + { + foreach (var provider in parts.OfType()) + { + var precompiledViews = provider.Views; + if (precompiledViews != null) + { + foreach (var viewInfo in precompiledViews) + { + feature.Views[viewInfo.Path] = viewInfo.Type; + } + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs index f944874d6f..4c71427961 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs @@ -72,6 +72,11 @@ private static void AddRazorViewEngineFeatureProviders(IMvcCoreBuilder builder) { builder.PartManager.FeatureProviders.Add(new MetadataReferenceFeatureProvider()); } + + if (!builder.PartManager.FeatureProviders.OfType().Any()) + { + builder.PartManager.FeatureProviders.Add(new ViewsFeatureProvider()); + } } /// @@ -127,6 +132,8 @@ public static IMvcCoreBuilder InitializeTagHelper( // Internal for testing. internal static void AddRazorViewEngineServices(IServiceCollection services) { + services.TryAddSingleton(); + services.TryAddSingleton(); // This caches compilation related details that are valid across the lifetime of the application. services.TryAddSingleton(); @@ -165,7 +172,7 @@ internal static void AddRazorViewEngineServices(IServiceCollection services) // creating the singleton RazorViewEngine instance. services.TryAddTransient(); services.TryAddTransient(); - services.TryAddTransient(); + services.TryAddTransient(); // This caches Razor page activation details that are valid for the lifetime of the application. services.TryAddSingleton(); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CSharpCompiler.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CSharpCompiler.cs new file mode 100644 index 0000000000..3100ef3eab --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CSharpCompiler.cs @@ -0,0 +1,51 @@ +// 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.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public class CSharpCompiler + { + private readonly CSharpCompilationOptions _compilationOptions; + private readonly CSharpParseOptions _parseOptions; + private readonly RazorReferenceManager _referenceManager; + private readonly DebugInformationFormat _pdbFormat = +#if NET451 + SymbolsUtility.SupportsFullPdbGeneration() ? + DebugInformationFormat.Pdb : + DebugInformationFormat.PortablePdb; +#else + DebugInformationFormat.PortablePdb; +#endif + + public CSharpCompiler(RazorReferenceManager manager, IOptions optionsAccessor) + { + _referenceManager = manager; + _compilationOptions = optionsAccessor.Value.CompilationOptions; + _parseOptions = optionsAccessor.Value.ParseOptions; + EmitOptions = new EmitOptions(debugInformationFormat: _pdbFormat); + } + + public EmitOptions EmitOptions { get; } + + public SyntaxTree CreateSyntaxTree(SourceText sourceText) + { + return CSharpSyntaxTree.ParseText( + sourceText, + options: _parseOptions); + } + + public CSharpCompilation CreateCompilation(string assemblyName) + { + return CSharpCompilation.Create( + assemblyName, + options: _compilationOptions, + references: _referenceManager.CompilationReferences); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs index 575f1e5728..0a08695649 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs @@ -42,22 +42,22 @@ public CompilerCache(IFileProvider fileProvider) /// /// Initializes a new instance of populated with precompiled views - /// specified by . + /// specified by . /// /// used to locate Razor views. - /// A mapping of application relative paths of view to the precompiled view - /// s. + /// A mapping of application relative paths of view to s that + /// have already been compiled. public CompilerCache( IFileProvider fileProvider, - IDictionary precompiledViews) + IDictionary views) : this(fileProvider) { - if (precompiledViews == null) + if (views == null) { - throw new ArgumentNullException(nameof(precompiledViews)); + throw new ArgumentNullException(nameof(views)); } - foreach (var item in precompiledViews) + foreach (var item in views) { var cacheEntry = new CompilerCacheResult(item.Key, new CompilationResult(item.Value)); _cache.Set(GetNormalizedPath(item.Key), Task.FromResult(cacheEntry)); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultCompilerCacheProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultCompilerCacheProvider.cs index f38ddd4457..ba328ca136 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultCompilerCacheProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultCompilerCacheProvider.cs @@ -1,7 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; namespace Microsoft.AspNetCore.Mvc.Razor.Internal { @@ -13,10 +14,15 @@ public class DefaultCompilerCacheProvider : ICompilerCacheProvider /// /// Initializes a new instance of . /// + /// The /// The . - public DefaultCompilerCacheProvider(IRazorViewEngineFileProviderAccessor fileProviderAccessor) + public DefaultCompilerCacheProvider( + ApplicationPartManager applicationPartManager, + IRazorViewEngineFileProviderAccessor fileProviderAccessor) { - Cache = new CompilerCache(fileProviderAccessor.FileProvider); + var feature = new ViewsFeature(); + applicationPartManager.PopulateFeature(feature); + Cache = new CompilerCache(fileProviderAccessor.FileProvider, feature.Views); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs index 833c541949..ec0cedfa27 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRoslynCompilationService.cs @@ -8,13 +8,10 @@ using System.Linq; using System.Reflection; using System.Text; -using System.Threading; using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; @@ -33,59 +30,31 @@ public class DefaultRoslynCompilationService : ICompilationService // error CS0246: The type or namespace name 'T' could not be found (are you missing a using directive // or an assembly reference?) private const string CS0246 = nameof(CS0246); - private readonly DebugInformationFormat _pdbFormat = -#if NET451 - SymbolsUtility.SupportsFullPdbGeneration() ? - DebugInformationFormat.Pdb : - DebugInformationFormat.PortablePdb; -#else - DebugInformationFormat.PortablePdb; -#endif - private readonly ApplicationPartManager _partManager; + + private readonly CSharpCompiler _compiler; private readonly IFileProvider _fileProvider; - private readonly Action _compilationCallback; - private readonly CSharpParseOptions _parseOptions; - private readonly CSharpCompilationOptions _compilationOptions; - private readonly IList _additionalMetadataReferences; private readonly ILogger _logger; - private object _compilationReferencesLock = new object(); - private bool _compilationReferencesInitialized; - private IList _compilationReferences; + private readonly Action _compilationCallback; /// /// Initalizes a new instance of the class. /// - /// The . + /// The . /// Accessor to . /// The . /// The . public DefaultRoslynCompilationService( - ApplicationPartManager partManager, - IOptions optionsAccessor, + CSharpCompiler compiler, IRazorViewEngineFileProviderAccessor fileProviderAccessor, + IOptions optionsAccessor, ILoggerFactory loggerFactory) { - _partManager = partManager; + _compiler = compiler; _fileProvider = fileProviderAccessor.FileProvider; _compilationCallback = optionsAccessor.Value.CompilationCallback; - _parseOptions = optionsAccessor.Value.ParseOptions; - _compilationOptions = optionsAccessor.Value.CompilationOptions; - _additionalMetadataReferences = optionsAccessor.Value.AdditionalCompilationReferences; _logger = loggerFactory.CreateLogger(); } - private IList CompilationReferences - { - get - { - return LazyInitializer.EnsureInitialized( - ref _compilationReferences, - ref _compilationReferencesInitialized, - ref _compilationReferencesLock, - GetCompilationReferences); - } - } - /// public CompilationResult Compile(RelativeFileInfo fileInfo, string compilationContent) { @@ -100,28 +69,10 @@ public CompilationResult Compile(RelativeFileInfo fileInfo, string compilationCo } _logger.GeneratedCodeToAssemblyCompilationStart(fileInfo.RelativePath); - var startTimestamp = _logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : 0; var assemblyName = Path.GetRandomFileName(); - - var sourceText = SourceText.From(compilationContent, Encoding.UTF8); - var syntaxTree = CSharpSyntaxTree.ParseText( - sourceText, - path: assemblyName, - options: _parseOptions); - - var compilation = CSharpCompilation.Create( - assemblyName, - options: _compilationOptions, - syntaxTrees: new[] { syntaxTree }, - references: CompilationReferences); - - compilation = Rewrite(compilation); - - var compilationContext = new RoslynCompilationContext(compilation); - _compilationCallback(compilationContext); - compilation = compilationContext.Compilation; + var compilation = CreateCompilation(compilationContent, assemblyName); using (var assemblyStream = new MemoryStream()) { @@ -130,7 +81,7 @@ public CompilationResult Compile(RelativeFileInfo fileInfo, string compilationCo var result = compilation.Emit( assemblyStream, pdbStream, - options: new EmitOptions(debugInformationFormat: _pdbFormat)); + options: _compiler.EmitOptions); if (!result.Success) { @@ -144,8 +95,9 @@ public CompilationResult Compile(RelativeFileInfo fileInfo, string compilationCo assemblyStream.Seek(0, SeekOrigin.Begin); pdbStream.Seek(0, SeekOrigin.Begin); - var assembly = LoadStream(assemblyStream, pdbStream); + var assembly = LoadAssembly(assemblyStream, pdbStream); var type = assembly.GetExportedTypes().FirstOrDefault(a => !a.IsNested); + _logger.GeneratedCodeToAssemblyCompilationEnd(fileInfo.RelativePath, startTimestamp); return new CompilationResult(type); @@ -153,50 +105,19 @@ public CompilationResult Compile(RelativeFileInfo fileInfo, string compilationCo } } - /// - /// Gets the sequence of instances used for compilation. - /// - /// The instances. - protected virtual IList GetCompilationReferences() - { - var feature = new MetadataReferenceFeature(); - _partManager.PopulateFeature(feature); - var applicationReferences = feature.MetadataReferences; - - if (_additionalMetadataReferences.Count == 0) - { - return applicationReferences; - } - - var compilationReferences = new List(applicationReferences.Count + _additionalMetadataReferences.Count); - compilationReferences.AddRange(applicationReferences); - compilationReferences.AddRange(_additionalMetadataReferences); - - return compilationReferences; - } - - private Assembly LoadStream(MemoryStream assemblyStream, MemoryStream pdbStream) - { -#if NET451 - return Assembly.Load(assemblyStream.ToArray(), pdbStream.ToArray()); -#else - return System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromStream(assemblyStream, pdbStream); -#endif - } - - private CSharpCompilation Rewrite(CSharpCompilation compilation) + private CSharpCompilation CreateCompilation(string compilationContent, string assemblyName) { - var rewrittenTrees = new List(); - foreach (var tree in compilation.SyntaxTrees) - { - var semanticModel = compilation.GetSemanticModel(tree, ignoreAccessibility: true); - var rewriter = new ExpressionRewriter(semanticModel); - - var rewrittenTree = tree.WithRootAndOptions(rewriter.Visit(tree.GetRoot()), tree.Options); - rewrittenTrees.Add(rewrittenTree); - } + var sourceText = SourceText.From(compilationContent, Encoding.UTF8); + var syntaxTree = _compiler.CreateSyntaxTree(sourceText).WithFilePath(assemblyName); + var compilation = _compiler + .CreateCompilation(assemblyName) + .AddSyntaxTrees(syntaxTree); + compilation = ExpressionRewriter.Rewrite(compilation); - return compilation.RemoveAllSyntaxTrees().AddSyntaxTrees(rewrittenTrees); + var compilationContext = new RoslynCompilationContext(compilation); + _compilationCallback(compilationContext); + compilation = compilationContext.Compilation; + return compilation; } // Internal for unit testing @@ -265,6 +186,17 @@ private static bool IsError(Diagnostic diagnostic) return diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error; } + public static Assembly LoadAssembly(MemoryStream assemblyStream, MemoryStream pdbStream) + { + var assembly = +#if NET451 + Assembly.Load(assemblyStream.ToArray(), pdbStream.ToArray()); +#else + System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromStream(assemblyStream, pdbStream); +#endif + return assembly; + } + private static string ReadFileContentsSafely(IFileProvider fileProvider, string filePath) { var fileInfo = fileProvider.GetFileInfo(filePath); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ExpressionRewriter.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ExpressionRewriter.cs index 30b94a9f98..0c54c7e4ce 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ExpressionRewriter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ExpressionRewriter.cs @@ -1,7 +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; using System.Collections.Generic; using System.Diagnostics; using System.Linq.Expressions; @@ -32,6 +31,21 @@ public ExpressionRewriter(SemanticModel semanticModel) private List> Expressions { get; } + public static CSharpCompilation Rewrite(CSharpCompilation compilation) + { + var rewrittenTrees = new List(); + foreach (var tree in compilation.SyntaxTrees) + { + var semanticModel = compilation.GetSemanticModel(tree, ignoreAccessibility: true); + var rewriter = new ExpressionRewriter(semanticModel); + + var rewrittenTree = tree.WithRootAndOptions(rewriter.Visit(tree.GetRoot()), tree.Options); + rewrittenTrees.Add(rewrittenTree); + } + + return compilation.RemoveAllSyntaxTrees().AddSyntaxTrees(rewrittenTrees); + } + public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) { if (IsInsideClass) diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorReferenceManager.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorReferenceManager.cs new file mode 100644 index 0000000000..2e21de7705 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorReferenceManager.cs @@ -0,0 +1,59 @@ +// 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.Threading; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public class RazorReferenceManager + { + private readonly ApplicationPartManager _partManager; + private readonly IList _additionalMetadataReferences; + private object _compilationReferencesLock = new object(); + private bool _compilationReferencesInitialized; + private IList _compilationReferences; + + public RazorReferenceManager( + ApplicationPartManager partManager, + IOptions optionsAccessor) + { + _partManager = partManager; + _additionalMetadataReferences = optionsAccessor.Value.AdditionalCompilationReferences; + } + + public IList CompilationReferences + { + get + { + return LazyInitializer.EnsureInitialized( + ref _compilationReferences, + ref _compilationReferencesInitialized, + ref _compilationReferencesLock, + GetCompilationReferences); + } + } + + private IList GetCompilationReferences() + { + var feature = new MetadataReferenceFeature(); + _partManager.PopulateFeature(feature); + var applicationReferences = feature.MetadataReferences; + + if (_additionalMetadataReferences.Count == 0) + { + return applicationReferences; + } + + var compilationReferences = new List(applicationReferences.Count + _additionalMetadataReferences.Count); + compilationReferences.AddRange(applicationReferences); + compilationReferences.AddRange(_additionalMetadataReferences); + + return compilationReferences; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs new file mode 100644 index 0000000000..77250475e1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs @@ -0,0 +1,66 @@ +// 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; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Compilation +{ + public class ViewsFeatureProviderTest + { + [Fact] + public void PopulateFeature_ReturnsEmptySequenceIfNoAssemblyPartHasViewAssembly() + { + // Arrange + var applicationPartManager = new ApplicationPartManager(); + applicationPartManager.ApplicationParts.Add( + new AssemblyPart(typeof(ViewsFeatureProviderTest).GetTypeInfo().Assembly)); + applicationPartManager.FeatureProviders.Add(new ViewsFeatureProvider()); + var feature = new MetadataReferenceFeature(); + + // Act + applicationPartManager.PopulateFeature(feature); + + // Assert + Assert.Empty(feature.MetadataReferences); + } + + [Fact] + public void PopulateFeature_ReturnsViewsFromAllAvailableApplicationParts() + { + // Arrange + var applicationPart1 = new Mock(); + var viewsProvider1 = applicationPart1 + .As() + .SetupGet(p => p.Views) + .Returns(new[] + { + new ViewInfo("/Views/test/Index.cshtml", typeof(object)) + }); + var applicationPart2 = new Mock(); + var viewsProvider2 = applicationPart2 + .As() + .SetupGet(p => p.Views) + .Returns(new[] + { + new ViewInfo("/Areas/Admin/Views/Index.cshtml", typeof(string)), + new ViewInfo("/Areas/Admin/Views/About.cshtml", typeof(int)) + }); + + + var applicationPartManager = new ApplicationPartManager(); + applicationPartManager.ApplicationParts.Add(applicationPart1.Object); + applicationPartManager.ApplicationParts.Add(applicationPart2.Object); + applicationPartManager.FeatureProviders.Add(new ViewsFeatureProvider()); + var feature = new MetadataReferenceFeature(); + + // Act + applicationPartManager.PopulateFeature(feature); + + // Assert + Assert.Empty(feature.MetadataReferences); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs index b880459544..ed47de593e 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRoslynCompilationServiceTest.cs @@ -2,8 +2,6 @@ // 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.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Razor.Compilation; @@ -297,34 +295,6 @@ public void Compile_SucceedsIfReferencesAreAddedInCallback() Assert.NotNull(result.CompiledType); } - [Fact] - public void GetCompilationReferences_CombinesApplicationPartAndOptionMetadataReferences() - { - // Arrange - var options = new RazorViewEngineOptions(); - var objectAssemblyLocation = typeof(object).GetTypeInfo().Assembly.Location; - var objectAssemblyMetadataReference = MetadataReference.CreateFromFile(objectAssemblyLocation); - options.AdditionalCompilationReferences.Add(objectAssemblyMetadataReference); - var applicationPartManager = GetApplicationPartManager(); - var compilationService = new TestRoslynCompilationService(applicationPartManager, options); - var feature = new MetadataReferenceFeature(); - applicationPartManager.PopulateFeature(feature); - var partReferences = feature.MetadataReferences; - var expectedReferences = new List(); - expectedReferences.AddRange(partReferences); - expectedReferences.Add(objectAssemblyMetadataReference); - var expectedReferenceDisplays = expectedReferences.Select(reference => reference.Display); - - // Act - var references = compilationService.GetCompilationReferencesPublic(); - var referenceDisplays = references.Select(reference => reference.Display); - - // Assert - Assert.NotNull(references); - Assert.NotEmpty(references); - Assert.Equal(expectedReferenceDisplays, referenceDisplays); - } - private static DiagnosticDescriptor GetDiagnosticDescriptor(string messageFormat) { return new DiagnosticDescriptor( @@ -377,22 +347,15 @@ private static DefaultRoslynCompilationService GetRoslynCompilationService( { partManager = partManager ?? GetApplicationPartManager(); options = options ?? GetOptions(); + var optionsAccessor = GetAccessor(options); + var referenceManager = new RazorReferenceManager(partManager, optionsAccessor); + var compiler = new CSharpCompiler(referenceManager, optionsAccessor); return new DefaultRoslynCompilationService( - partManager, - GetAccessor(options), + compiler, GetFileProviderAccessor(fileProvider), + optionsAccessor, NullLoggerFactory.Instance); } - - private class TestRoslynCompilationService : DefaultRoslynCompilationService - { - public TestRoslynCompilationService(ApplicationPartManager partManager, RazorViewEngineOptions options) - : base(partManager, GetAccessor(options), GetFileProviderAccessor(), NullLoggerFactory.Instance) - { - } - - public IList GetCompilationReferencesPublic() => GetCompilationReferences(); - } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ReferenceManagerTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ReferenceManagerTest.cs new file mode 100644 index 0000000000..12fe6d81d6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/ReferenceManagerTest.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Test.Internal +{ + public class ReferenceManagerTest + { + [Fact] + public void GetCompilationReferences_CombinesApplicationPartAndOptionMetadataReferences() + { + // Arrange + var options = new RazorViewEngineOptions(); + var objectAssemblyLocation = typeof(object).GetTypeInfo().Assembly.Location; + var objectAssemblyMetadataReference = MetadataReference.CreateFromFile(objectAssemblyLocation); + options.AdditionalCompilationReferences.Add(objectAssemblyMetadataReference); + + var applicationPartManager = GetApplicationPartManager(); + var feature = new MetadataReferenceFeature(); + applicationPartManager.PopulateFeature(feature); + var partReferences = feature.MetadataReferences; + var expectedReferenceDisplays = partReferences + .Concat(new[] { objectAssemblyMetadataReference }) + .Select(r => r.Display); + var referenceManager = new RazorReferenceManager(applicationPartManager, GetAccessor(options)); + + // Act + var references = referenceManager.CompilationReferences; + var referenceDisplays = references.Select(reference => reference.Display); + + // Assert + Assert.Equal(expectedReferenceDisplays, referenceDisplays); + } + + private static ApplicationPartManager GetApplicationPartManager() + { + var applicationPartManager = new ApplicationPartManager(); + var assembly = typeof(DefaultRoslynCompilationServiceTest).GetTypeInfo().Assembly; + applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly)); + applicationPartManager.FeatureProviders.Add(new MetadataReferenceFeatureProvider()); + + return applicationPartManager; + } + + private static IOptions GetAccessor(RazorViewEngineOptions options) + { + var optionsAccessor = new Mock>(); + optionsAccessor.SetupGet(a => a.Value).Returns(options); + return optionsAccessor.Object; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs index 4ad71818eb..ea1adf99f7 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -211,7 +211,8 @@ public void AddMvcTwice_DoesNotAddApplicationFeatureProvidersTwice() feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), - feature => Assert.IsType(feature)); + feature => Assert.IsType(feature), + feature => Assert.IsType(feature)); } [Fact]