From d6176ac7de1ef2dce64a4edc83700ed14de00532 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 8 Mar 2018 13:20:29 -0800 Subject: [PATCH] ApplicationPartFactory: The works --- build/dependencies.props | 3 +- korebuild-lock.txt | 4 +- samples/MvcSandbox/MvcSandbox.csproj | 1 + .../AdditionalAssemblyPart.cs | 25 - .../ApplicationAssembliesProvider.cs | 283 ++++++++++ .../ApplicationPartFactory.cs | 54 ++ .../ApplicationPartManager.cs | 17 + .../NullApplicationPartFactory.cs | 24 + .../ProvideApplicationPartFactoryAttribute.cs | 30 +- .../RelatedAssemblyAttribute.cs | 99 +++- .../MvcCoreServiceCollectionExtensions.cs | 6 +- .../DefaultAssemblyPartDiscoveryProvider.cs | 406 -------------- .../Properties/Resources.Designer.cs | 70 +++ .../Resources.resx | 17 +- ...iledRazorAssemblyApplicationPartFactory.cs | 4 +- .../CompiledRazorAssemblyPart.cs | 24 +- .../IRazorCompiledItemProvider.cs | 19 + .../RazorCompiledItemFeatureProvider.cs | 53 ++ .../Compilation/CompiledViewDescriptor.cs | 3 + .../Compilation/ViewsFeatureProvider.cs | 177 ++----- .../MvcRazorMvcCoreBuilderExtensions.cs | 12 + .../Internal/RazorViewCompiler.cs | 13 +- .../Properties/AssemblyInfo.cs | 1 + .../Internal/DefaultPageLoader.cs | 3 +- .../ApplicationAssembliesProviderTest.cs | 436 +++++++++++++++ .../ApplicationPartManagerTest.cs | 3 +- .../RelatedAssemblyPartTest.cs | 104 ++++ ...efaultAssemblyPartDiscoveryProviderTest.cs | 501 ------------------ .../RazorBuildTest.cs | 13 + .../RazorCompiledItemFeatureProviderTest.cs | 131 +++++ .../Compilation/ViewsFeatureProviderTest.cs | 280 ++-------- .../Internal/RazorViewCompilerTest.cs | 21 - .../MvcServiceCollectionExtensionsTest.cs | 3 + .../Views/Common/CommonView.cs | 37 ++ .../RazorBuildWebSite.Views/AssemblyInfo.cs | 11 + .../Views/Common/CommonView.cs | 40 ++ .../Controllers/CommonController.cs | 13 + 37 files changed, 1604 insertions(+), 1337 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AdditionalAssemblyPart.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationAssembliesProvider.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/NullApplicationPartFactory.cs delete mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultAssemblyPartDiscoveryProvider.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/IRazorCompiledItemProvider.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/RazorCompiledItemFeatureProvider.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/ApplicationAssembliesProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/RelatedAssemblyPartTest.cs delete mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultAssemblyPartDiscoveryProviderTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Razor.Test/ApplicationParts/RazorCompiledItemFeatureProviderTest.cs create mode 100644 test/WebSites/RazorBuildWebSite.PrecompiledViews/Views/Common/CommonView.cs create mode 100644 test/WebSites/RazorBuildWebSite.Views/AssemblyInfo.cs create mode 100644 test/WebSites/RazorBuildWebSite.Views/Views/Common/CommonView.cs create mode 100644 test/WebSites/RazorBuildWebSite/Controllers/CommonController.cs diff --git a/build/dependencies.props b/build/dependencies.props index 5b1810cf9c..bf8a0978e1 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,7 +4,7 @@ 0.10.13 - 2.1.0-preview2-15742 + 2.1.0-preview2-15744 2.1.0-preview2-30355 2.1.0-preview2-30355 2.1.0-preview2-30355 @@ -75,6 +75,7 @@ 2.0.0 2.1.0-preview2-26314-02 2.1.0-preview2-30355 + 2.1.0-preview2-30355 15.6.0 4.7.49 1.0.1 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index e40ef6651b..f531e7b0f7 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview2-15742 -commithash:21fbb0f2c3fe4a9216e2d59632b98cfd7d685962 +version:2.1.0-preview2-15744 +commithash:9e15cb6062ab5b9790d3fa699e018543a6950713 diff --git a/samples/MvcSandbox/MvcSandbox.csproj b/samples/MvcSandbox/MvcSandbox.csproj index 0ca6916c27..ccd3ba70ff 100644 --- a/samples/MvcSandbox/MvcSandbox.csproj +++ b/samples/MvcSandbox/MvcSandbox.csproj @@ -22,6 +22,7 @@ + diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AdditionalAssemblyPart.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AdditionalAssemblyPart.cs deleted file mode 100644 index 5bc8754ace..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AdditionalAssemblyPart.cs +++ /dev/null @@ -1,25 +0,0 @@ -// 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.Reflection; - -namespace Microsoft.AspNetCore.Mvc.ApplicationParts -{ - /// - /// An that was added by an assembly that referenced it through the use - /// of an assembly metadata attribute. - /// - public class AdditionalAssemblyPart : AssemblyPart, ICompilationReferencesProvider, IApplicationPartTypeProvider - { - /// - public AdditionalAssemblyPart(Assembly assembly) : base(assembly) - { - } - - IEnumerable ICompilationReferencesProvider.GetReferencePaths() => Array.Empty(); - - IEnumerable IApplicationPartTypeProvider.Types => Array.Empty(); - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationAssembliesProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationAssembliesProvider.cs new file mode 100644 index 0000000000..137159840a --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationAssembliesProvider.cs @@ -0,0 +1,283 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.Extensions.DependencyModel; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + internal class ApplicationAssembliesProvider + { + internal static HashSet ReferenceAssemblies { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Microsoft.AspNetCore.Mvc", + "Microsoft.AspNetCore.Mvc.Abstractions", + "Microsoft.AspNetCore.Mvc.ApiExplorer", + "Microsoft.AspNetCore.Mvc.Core", + "Microsoft.AspNetCore.Mvc.Cors", + "Microsoft.AspNetCore.Mvc.DataAnnotations", + "Microsoft.AspNetCore.Mvc.Formatters.Json", + "Microsoft.AspNetCore.Mvc.Formatters.Xml", + "Microsoft.AspNetCore.Mvc.Localization", + "Microsoft.AspNetCore.Mvc.Razor", + "Microsoft.AspNetCore.Mvc.Razor.Extensions", + "Microsoft.AspNetCore.Mvc.RazorPages", + "Microsoft.AspNetCore.Mvc.TagHelpers", + "Microsoft.AspNetCore.Mvc.ViewFeatures", + }; + + /// + /// Returns an ordered list of application assemblies. + /// + /// The order is as follows: + /// * Entry assembly + /// * Assemblies specified in the application's deps file ordered by name. + /// + /// Each assembly is immediately followed by assemblies specified by annotated ordered by name. + /// + /// + /// + public IEnumerable ResolveAssemblies(Assembly entryAssembly) + { + var dependencyContext = LoadDependencyContext(entryAssembly); + + IEnumerable assemblyItems; + + if (dependencyContext == null) + { + assemblyItems = new[] { GetAssemblyItem(entryAssembly) }; + } + else + { + assemblyItems = ResolveFromDependencyContext(dependencyContext); + } + + assemblyItems = assemblyItems + .OrderBy(item => item.Assembly == entryAssembly ? 0 : 1) + .ThenBy(item => item.Assembly.FullName, StringComparer.Ordinal); + + foreach (var item in assemblyItems) + { + yield return item.Assembly; + + foreach (var associatedAssembly in item.RelatedAssemblies.Distinct().OrderBy(assembly => assembly.FullName, StringComparer.Ordinal)) + { + yield return associatedAssembly; + } + } + } + + protected virtual DependencyContext LoadDependencyContext(Assembly assembly) => DependencyContext.Load(assembly); + + private List ResolveFromDependencyContext(DependencyContext dependencyContext) + { + var assemblyItems = new List(); + var relatedAssemblies = new Dictionary(); + + var candidateAssemblies = GetCandidateLibraries(dependencyContext) + .SelectMany(library => GetLibraryAssemblies(dependencyContext, library)); + + foreach (var assembly in candidateAssemblies) + { + var assemblyItem = GetAssemblyItem(assembly); + assemblyItems.Add(assemblyItem); + + foreach (var relatedAssembly in assemblyItem.RelatedAssemblies) + { + if (relatedAssemblies.TryGetValue(relatedAssembly, out var otherEntry)) + { + var message = string.Join( + Environment.NewLine, + Resources.FormatApplicationAssembliesProvider_DuplicateRelatedAssembly(relatedAssembly.FullName), + otherEntry.Assembly.FullName, + assembly.FullName); + + throw new InvalidOperationException(message); + } + + relatedAssemblies.Add(relatedAssembly, assemblyItem); + } + } + + // Remove any top level assemblies that appear as an associated assembly. + assemblyItems.RemoveAll(item => relatedAssemblies.ContainsKey(item.Assembly)); + + return assemblyItems; + } + + protected virtual IEnumerable GetLibraryAssemblies(DependencyContext dependencyContext, RuntimeLibrary runtimeLibrary) + { + foreach (var assemblyName in runtimeLibrary.GetDefaultAssemblyNames(dependencyContext)) + { + var assembly = Assembly.Load(assemblyName); + yield return assembly; + } + } + + protected virtual IReadOnlyList GetRelatedAssemblies(Assembly assembly) + { + // Do not require related assemblies to be available in the default code path. + return RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: false); + } + + private AssemblyItem GetAssemblyItem(Assembly assembly) + { + var relatedAssemblies = GetRelatedAssemblies(assembly); + + // Ensure we don't have any cycles. A cycle could be formed if a related assembly points to the primary assembly. + foreach (var relatedAssembly in relatedAssemblies) + { + if (relatedAssembly.IsDefined(typeof(RelatedAssemblyAttribute))) + { + throw new InvalidOperationException( + Resources.FormatApplicationAssembliesProvider_RelatedAssemblyCannotDefineAdditional(relatedAssembly.FullName, assembly.FullName)); + } + } + + return new AssemblyItem(assembly, relatedAssemblies); + } + + // Returns a list of libraries that references the assemblies in . + // By default it returns all assemblies that reference any of the primary MVC assemblies + // while ignoring MVC assemblies. + // Internal for unit testing + internal static IEnumerable GetCandidateLibraries(DependencyContext dependencyContext) + { + var candidatesResolver = new CandidateResolver(dependencyContext.RuntimeLibraries, ReferenceAssemblies); + return candidatesResolver.GetCandidates(); + } + + private class CandidateResolver + { + private readonly IDictionary _runtimeDependencies; + + public CandidateResolver(IReadOnlyList runtimeDependencies, ISet referenceAssemblies) + { + var dependenciesWithNoDuplicates = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var dependency in runtimeDependencies) + { + if (dependenciesWithNoDuplicates.ContainsKey(dependency.Name)) + { + throw new InvalidOperationException(Resources.FormatCandidateResolver_DifferentCasedReference(dependency.Name)); + } + dependenciesWithNoDuplicates.Add(dependency.Name, CreateDependency(dependency, referenceAssemblies)); + } + + _runtimeDependencies = dependenciesWithNoDuplicates; + } + + private Dependency CreateDependency(RuntimeLibrary library, ISet referenceAssemblies) + { + var classification = DependencyClassification.Unknown; + if (referenceAssemblies.Contains(library.Name)) + { + classification = DependencyClassification.MvcReference; + } + + return new Dependency(library, classification); + } + + private DependencyClassification ComputeClassification(string dependency) + { + if (!_runtimeDependencies.ContainsKey(dependency)) + { + // Library does not have runtime dependency. Since we can't infer + // anything about it's references, we'll assume it does not have a reference to Mvc. + return DependencyClassification.DoesNotReferenceMvc; + } + + var candidateEntry = _runtimeDependencies[dependency]; + if (candidateEntry.Classification != DependencyClassification.Unknown) + { + return candidateEntry.Classification; + } + else + { + var classification = DependencyClassification.DoesNotReferenceMvc; + foreach (var candidateDependency in candidateEntry.Library.Dependencies) + { + var dependencyClassification = ComputeClassification(candidateDependency.Name); + if (dependencyClassification == DependencyClassification.ReferencesMvc || + dependencyClassification == DependencyClassification.MvcReference) + { + classification = DependencyClassification.ReferencesMvc; + break; + } + } + + candidateEntry.Classification = classification; + + return classification; + } + } + + public IEnumerable GetCandidates() + { + foreach (var dependency in _runtimeDependencies) + { + if (ComputeClassification(dependency.Key) == DependencyClassification.ReferencesMvc) + { + yield return dependency.Value.Library; + } + } + } + + private class Dependency + { + public Dependency(RuntimeLibrary library, DependencyClassification classification) + { + Library = library; + Classification = classification; + } + + public RuntimeLibrary Library { get; } + + public DependencyClassification Classification { get; set; } + + public override string ToString() + { + return $"Library: {Library.Name}, Classification: {Classification}"; + } + } + + private enum DependencyClassification + { + Unknown = 0, + + /// + /// References (directly or transitively) one of the Mvc packages listed in + /// . + /// + ReferencesMvc = 1, + + /// + /// Does not reference (directly or transitively) one of the Mvc packages listed by + /// . + /// + DoesNotReferenceMvc = 2, + + /// + /// One of the references listed in . + /// + MvcReference = 3, + } + } + + private readonly struct AssemblyItem + { + public AssemblyItem(Assembly assembly, IReadOnlyList associatedAssemblies) + { + Assembly = assembly; + RelatedAssemblies = associatedAssemblies; + } + + public Assembly Assembly { get; } + + public IReadOnlyList RelatedAssemblies { get; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartFactory.cs index 88a93f9881..6524ba6bae 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartFactory.cs @@ -1,8 +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. +using System; using System.Collections.Generic; using System.Reflection; +using Microsoft.AspNetCore.Mvc.Core; namespace Microsoft.AspNetCore.Mvc.ApplicationParts { @@ -19,6 +21,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts { public static readonly string DefaultContextName = "Default"; + /// + /// Default implementation for . + /// + public static ApplicationPartFactory Default { get; } = new DefaultApplicationPartFactory(); + /// /// Gets one or more instances for the specified . /// @@ -27,5 +34,52 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts /// The context name. By default, value of this parameter is . /// public abstract IEnumerable GetApplicationParts(Assembly assembly, string context); + + /// + /// Gets the for the specified assembly. + /// + /// An assembly may specify an using . + /// Otherwise, is used. + /// + /// + /// The . + /// An instance of . + public static ApplicationPartFactory GetApplicationPartFactory(Assembly assembly) + { + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + var provideAttribute = assembly.GetCustomAttribute(); + if (provideAttribute == null) + { + return ApplicationPartFactory.Default; + } + + var type = provideAttribute.GetFactoryType(); + if (!typeof(ApplicationPartFactory).IsAssignableFrom(type)) + { + throw new InvalidOperationException(Resources.FormatApplicationPartFactory_InvalidFactoryType( + type, + nameof(ProvideApplicationPartFactoryAttribute), + typeof(ApplicationPartFactory))); + } + + return (ApplicationPartFactory)Activator.CreateInstance(type); + } + + private class DefaultApplicationPartFactory : ApplicationPartFactory + { + public override IEnumerable GetApplicationParts(Assembly assembly, string context) + { + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + yield return new AssemblyPart(assembly); + } + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartManager.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartManager.cs index 95a893037f..3de5a48338 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartManager.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartManager.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; namespace Microsoft.AspNetCore.Mvc.ApplicationParts { @@ -48,5 +49,21 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts provider.PopulateFeature(ApplicationParts, feature); } } + + internal void PopulateDefaultParts(string entryAssemblyName) + { + var entryAssembly = Assembly.Load(new AssemblyName(entryAssemblyName)); + var assembliesProvider = new ApplicationAssembliesProvider(); + var applicationAssemblies = assembliesProvider.ResolveAssemblies(entryAssembly); + + foreach (var assembly in applicationAssemblies) + { + var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly); + foreach (var part in partFactory.GetApplicationParts(assembly, context: ApplicationPartFactory.DefaultContextName)) + { + ApplicationParts.Add(part); + } + } + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/NullApplicationPartFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/NullApplicationPartFactory.cs new file mode 100644 index 0000000000..06e48018e5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/NullApplicationPartFactory.cs @@ -0,0 +1,24 @@ +// 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 System.Reflection; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + /// + /// An that produces no parts. + /// + /// This factory may be used to to preempt Mvc's default part discovery allowing for custom configuration at a later stage. + /// + /// + public class NullApplicationPartFactory : ApplicationPartFactory + { + /// + public override IEnumerable GetApplicationParts(Assembly assembly, string context) + { + return Enumerable.Empty(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ProvideApplicationPartFactoryAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ProvideApplicationPartFactoryAttribute.cs index ab5b464098..1ad9e57879 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ProvideApplicationPartFactoryAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ProvideApplicationPartFactoryAttribute.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNetCore.Mvc.Core; namespace Microsoft.AspNetCore.Mvc.ApplicationParts { @@ -11,15 +12,40 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] public sealed class ProvideApplicationPartFactoryAttribute : Attribute { + private readonly Type _applicationPartFactoryType; + private readonly string _applicationPartFactoryTypeName; + + /// + /// Creates a new instance of with the specified type. + /// + /// The factory type. public ProvideApplicationPartFactoryAttribute(Type factoryType) { - ApplicationPartFactoryType = factoryType ?? throw new ArgumentNullException(nameof(factoryType)); + _applicationPartFactoryType = factoryType ?? throw new ArgumentNullException(nameof(factoryType)); } + /// + /// Creates a new instance of with the specified type name. + /// + /// The assembly qualified type name. public ProvideApplicationPartFactoryAttribute(string factoryTypeName) { + if (string.IsNullOrEmpty(factoryTypeName)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(factoryTypeName)); + } + + _applicationPartFactoryTypeName = factoryTypeName; } - public Type ApplicationPartFactoryType { get; } + /// + /// Gets the factory type. + /// + /// + public Type GetFactoryType() + { + return _applicationPartFactoryType ?? + Type.GetType(_applicationPartFactoryTypeName, throwOnError: true); + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/RelatedAssemblyAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/RelatedAssemblyAttribute.cs index 153cbfcbe2..aa796cdb55 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/RelatedAssemblyAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/RelatedAssemblyAttribute.cs @@ -2,23 +2,110 @@ // 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 Microsoft.AspNetCore.Mvc.Core; namespace Microsoft.AspNetCore.Mvc.ApplicationParts { + /// + /// Specifies a assembly to load as part of MVC's assembly discovery mechanism. + /// [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] public sealed class RelatedAssemblyAttribute : Attribute { - public RelatedAssemblyAttribute(string name) + private static readonly Func AssemblyLoadFileDelegate = Assembly.LoadFile; + + /// + /// Initializes a new instance of . + /// + /// The file name, without extension, of the related assembly. + public RelatedAssemblyAttribute(string assemblyFileName) { - if (string.IsNullOrEmpty(name)) + if (string.IsNullOrEmpty(assemblyFileName)) { - // Temporary workaround until we have a new build of RazorSdk. - // TODO: Uncomment the below line. - // throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(name)); + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(assemblyFileName)); } + + AssemblyFileName = assemblyFileName; } - public string Name { get; } + /// + /// Gets the assembly file name without extension. + /// + public string AssemblyFileName { get; } + + /// + /// Gets instances specified by . + /// + /// The assembly containing instances. + /// Determines if the method throws if a related assembly could not be located. + /// Related instances. + public static IReadOnlyList GetRelatedAssemblies(Assembly assembly, bool throwOnError) + { + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + return GetRelatedAssemblies(assembly, throwOnError, AssemblyLoadFileDelegate); + } + + internal static IReadOnlyList GetRelatedAssemblies(Assembly assembly, bool throwOnError, Func loadFile) + { + if (assembly == null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + // MVC will specifically look for related parts in the same physical directory as the assembly. + // No-op if the assembly does not have a location. + if (assembly.IsDynamic || string.IsNullOrEmpty(assembly.CodeBase)) + { + return Array.Empty(); + } + + var attributes = assembly.GetCustomAttributes().ToArray(); + if (attributes.Length == 0) + { + return Array.Empty(); + } + + var assemblyName = assembly.GetName().Name; + var assemblyDirectory = Path.GetDirectoryName(assembly.CodeBase); + + var relatedAssemblies = new List(); + for (var i = 0; i < attributes.Length; i++) + { + var attribute = attributes[i]; + if (string.Equals(assemblyName, attribute.AssemblyFileName, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + Resources.FormatRelatedAssemblyAttribute_AssemblyCannotReferenceSelf(nameof(RelatedAssemblyAttribute), assemblyName)); + } + + var relatedAssemblyLocation = Path.Combine(assemblyDirectory, attribute.AssemblyFileName + ".dll"); + if (!File.Exists(relatedAssemblyLocation)) + { + if (throwOnError) + { + throw new FileNotFoundException( + Resources.FormatRelatedAssemblyAttribute_CouldNotBeFound(attribute.AssemblyFileName, assemblyName, assemblyDirectory), + relatedAssemblyLocation); + } + else + { + continue; + } + } + + var relatedAssembly = loadFile(relatedAssemblyLocation); + relatedAssemblies.Add(relatedAssembly); + } + + return relatedAssemblies; + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index da091e5d7d..b0a9a9f47a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -87,11 +87,7 @@ namespace Microsoft.Extensions.DependencyInjection return manager; } - var parts = DefaultAssemblyPartDiscoveryProvider.DiscoverAssemblyParts(entryAssemblyName); - foreach (var part in parts) - { - manager.ApplicationParts.Add(part); - } + manager.PopulateDefaultParts(entryAssemblyName); } return manager; diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultAssemblyPartDiscoveryProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultAssemblyPartDiscoveryProvider.cs deleted file mode 100644 index 645f1d6731..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultAssemblyPartDiscoveryProvider.cs +++ /dev/null @@ -1,406 +0,0 @@ -// 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 Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.AspNetCore.Mvc.Core; -using Microsoft.Extensions.DependencyModel; - -namespace Microsoft.AspNetCore.Mvc.Internal -{ - // Discovers assemblies that are part of the MVC application using the DependencyContext. - public static class DefaultAssemblyPartDiscoveryProvider - { - private static readonly string PrecompiledViewsAssemblySuffix = ".PrecompiledViews"; - private static readonly IReadOnlyList ViewAssemblySuffixes = new string[] - { - PrecompiledViewsAssemblySuffix, - ".Views", - }; - - private const string AdditionalReferenceKey = "Microsoft.AspNetCore.Mvc.AdditionalReference"; - private static readonly char[] MetadataSeparators = new[] { ',' }; - - internal static HashSet ReferenceAssemblies { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "Microsoft.AspNetCore.All", - "Microsoft.AspNetCore.Mvc", - "Microsoft.AspNetCore.Mvc.Abstractions", - "Microsoft.AspNetCore.Mvc.ApiExplorer", - "Microsoft.AspNetCore.Mvc.Core", - "Microsoft.AspNetCore.Mvc.Cors", - "Microsoft.AspNetCore.Mvc.DataAnnotations", - "Microsoft.AspNetCore.Mvc.Formatters.Json", - "Microsoft.AspNetCore.Mvc.Formatters.Xml", - "Microsoft.AspNetCore.Mvc.Localization", - "Microsoft.AspNetCore.Mvc.Razor", - "Microsoft.AspNetCore.Mvc.Razor.Extensions", - "Microsoft.AspNetCore.Mvc.RazorPages", - "Microsoft.AspNetCore.Mvc.TagHelpers", - "Microsoft.AspNetCore.Mvc.ViewFeatures" - }; - - // For testing purposes only. - internal static Func AssemblyLoader { get; set; } = Assembly.LoadFile; - internal static Func AssemblyResolver { get; set; } = File.Exists; - - public static IEnumerable DiscoverAssemblyParts(string entryPointAssemblyName) - { - // We need to produce a stable order of the parts that is given by: - // 1) Parts that are not additional parts go before parts that are additional parts. - // 2) The entry point part goes before any other part in the system. - // 3) The entry point additional parts go before any other additional parts. - // 4) Parts are finally ordered by their full name to produce a stable ordering. - var entryAssembly = Assembly.Load(new AssemblyName(entryPointAssemblyName)); - var context = DependencyContext.Load(entryAssembly); - - var candidateAssemblies = new SortedSet( - GetCandidateAssemblies(entryAssembly, context), - FullNameAssemblyComparer.Instance); - - var (additionalReferences, entryAssemblyAdditionalReferences) = ResolveAdditionalReferences( - entryAssembly, - candidateAssemblies); - - candidateAssemblies.Remove(entryAssembly); - candidateAssemblies.ExceptWith(additionalReferences); - candidateAssemblies.ExceptWith(entryAssemblyAdditionalReferences); - - // Create the list of assembly parts. - return CreateParts(); - - IEnumerable CreateParts() - { - yield return new AssemblyPart(entryAssembly); - foreach (var candidateAssembly in candidateAssemblies) - { - yield return new AssemblyPart(candidateAssembly); - } - foreach (var entryAdditionalAssembly in entryAssemblyAdditionalReferences) - { - yield return new AdditionalAssemblyPart(entryAdditionalAssembly); - } - foreach (var additionalAssembly in additionalReferences) - { - yield return new AdditionalAssemblyPart(additionalAssembly); - } - } - } - - internal static AdditionalReferencesPair ResolveAdditionalReferences( - Assembly entryAssembly, - SortedSet candidateAssemblies) - { - var additionalAssemblyReferences = candidateAssemblies - .Select(ca => - (assembly: ca, - metadata: ca.GetCustomAttributes() - .Where(ama => ama.Key.Equals(AdditionalReferenceKey, StringComparison.Ordinal)).ToArray())); - - // Find out all the additional references defined by the assembly. - // [assembly: AssemblyMetadataAttribute("Microsoft.AspNetCore.Mvc.AdditionalReference", "Library.PrecompiledViews.dll,true|false")] - var additionalReferences = new SortedSet(FullNameAssemblyComparer.Instance); - var entryAssemblyAdditionalReferences = new SortedSet(FullNameAssemblyComparer.Instance); - foreach (var (assembly, metadata) in additionalAssemblyReferences) - { - if (metadata.Length > 0) - { - foreach (var metadataAttribute in metadata) - { - AddAdditionalReference( - LoadFromMetadata(assembly, metadataAttribute), - entryAssembly, - assembly, - additionalReferences, - entryAssemblyAdditionalReferences); - } - } - else - { - // Fallback to loading the views like in previous versions if the additional reference metadata - // attribute is not present. - AddAdditionalReference( - LoadFromConvention(assembly), - entryAssembly, - assembly, - additionalReferences, - entryAssemblyAdditionalReferences); - } - } - - return new AdditionalReferencesPair - { - AdditionalAssemblies = additionalReferences, - EntryAssemblyAdditionalAssemblies = entryAssemblyAdditionalReferences - }; - } - - private static Assembly LoadFromMetadata(Assembly assembly, AssemblyMetadataAttribute metadataAttribute) - { - var (metadataPath, metadataIncludeByDefault) = ParseMetadataAttribute(metadataAttribute); - if (metadataPath == null || - metadataIncludeByDefault == null || - !string.Equals(metadataIncludeByDefault, "true", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - var fileName = Path.GetFileName(metadataPath); - var filePath = Path.Combine(Path.GetDirectoryName(assembly.Location), fileName); - var additionalAssembly = LoadAssembly(filePath); - - if (additionalAssembly == null) - { - return null; - } - - return additionalAssembly; - } - - private static (string metadataPath, string metadataIncludeByDefault) ParseMetadataAttribute( - AssemblyMetadataAttribute metadataAttribute) - { - var data = metadataAttribute.Value.Split(MetadataSeparators); - if (data.Length != 2 || string.IsNullOrWhiteSpace(data[0]) || string.IsNullOrWhiteSpace(data[1])) - { - return default; - } - - return (data[0], data[1]); - } - - private static Assembly LoadAssembly(string filePath) - { - Assembly viewsAssembly = null; - if (AssemblyResolver(filePath)) - { - try - { - viewsAssembly = AssemblyLoader(filePath); - } - catch (FileLoadException) - { - // Don't throw if assembly cannot be loaded. This can happen if the file is not a managed assembly. - } - } - - return viewsAssembly; - } - - private static Assembly LoadFromConvention(Assembly assembly) - { - if (assembly.IsDynamic || string.IsNullOrEmpty(assembly.Location)) - { - return null; - } - - for (var i = 0; i < ViewAssemblySuffixes.Count; i++) - { - var fileName = assembly.GetName().Name + ViewAssemblySuffixes[i] + ".dll"; - var filePath = Path.Combine(Path.GetDirectoryName(assembly.Location), fileName); - - var viewsAssembly = LoadAssembly(filePath); - if (viewsAssembly != null) - { - return viewsAssembly; - } - } - - return null; - } - - private static void AddAdditionalReference( - Assembly additionalReference, - Assembly entryAssembly, - Assembly assembly, - SortedSet additionalReferences, - SortedSet entryAssemblyAdditionalReferences) - { - if (additionalReference == null || - additionalReferences.Contains(additionalReference) || - entryAssemblyAdditionalReferences.Contains(additionalReference)) - { - return; - } - - if (assembly.Equals(entryAssembly)) - { - entryAssemblyAdditionalReferences.Add(additionalReference); - } - else - { - additionalReferences.Add(additionalReference); - } - } - - internal class AdditionalReferencesPair - { - public SortedSet AdditionalAssemblies { get; set; } - public SortedSet EntryAssemblyAdditionalAssemblies { get; set; } - - public void Deconstruct( - out SortedSet additionalAssemblies, - out SortedSet entryAssemblyAdditionalAssemblies) - { - additionalAssemblies = AdditionalAssemblies; - entryAssemblyAdditionalAssemblies = EntryAssemblyAdditionalAssemblies; - } - } - - internal class FullNameAssemblyComparer : IComparer - { - public static IComparer Instance { get; } = new FullNameAssemblyComparer(); - - public int Compare(Assembly x, Assembly y) => - string.Compare(x?.FullName, y?.FullName, StringComparison.Ordinal); - } - - internal static IEnumerable GetCandidateAssemblies(Assembly entryAssembly, DependencyContext dependencyContext) - { - if (dependencyContext == null) - { - // Use the entry assembly as the sole candidate. - return new[] { entryAssembly }; - } - - return GetCandidateLibraries(dependencyContext) - .SelectMany(library => library.GetDefaultAssemblyNames(dependencyContext)) - .Select(Assembly.Load); - } - - // Returns a list of libraries that references the assemblies in . - // By default it returns all assemblies that reference any of the primary MVC assemblies - // while ignoring MVC assemblies. - // Internal for unit testing - internal static IEnumerable GetCandidateLibraries(DependencyContext dependencyContext) - { - if (ReferenceAssemblies == null) - { - return Enumerable.Empty(); - } - - var candidatesResolver = new CandidateResolver(dependencyContext.RuntimeLibraries, ReferenceAssemblies); - return candidatesResolver.GetCandidates(); - } - - private class CandidateResolver - { - private readonly IDictionary _runtimeDependencies; - - public CandidateResolver(IReadOnlyList runtimeDependencies, ISet referenceAssemblies) - { - var dependenciesWithNoDuplicates = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var dependency in runtimeDependencies) - { - if (dependenciesWithNoDuplicates.ContainsKey(dependency.Name)) - { - throw new InvalidOperationException(Resources.FormatCandidateResolver_DifferentCasedReference(dependency.Name)); - } - dependenciesWithNoDuplicates.Add(dependency.Name, CreateDependency(dependency, referenceAssemblies)); - } - - _runtimeDependencies = dependenciesWithNoDuplicates; - } - - private Dependency CreateDependency(RuntimeLibrary library, ISet referenceAssemblies) - { - var classification = DependencyClassification.Unknown; - if (referenceAssemblies.Contains(library.Name)) - { - classification = DependencyClassification.MvcReference; - } - - return new Dependency(library, classification); - } - - private DependencyClassification ComputeClassification(string dependency) - { - if (!_runtimeDependencies.ContainsKey(dependency)) - { - // Library does not have runtime dependency. Since we can't infer - // anything about it's references, we'll assume it does not have a reference to Mvc. - return DependencyClassification.DoesNotReferenceMvc; - } - - var candidateEntry = _runtimeDependencies[dependency]; - if (candidateEntry.Classification != DependencyClassification.Unknown) - { - return candidateEntry.Classification; - } - else - { - var classification = DependencyClassification.DoesNotReferenceMvc; - foreach (var candidateDependency in candidateEntry.Library.Dependencies) - { - var dependencyClassification = ComputeClassification(candidateDependency.Name); - if (dependencyClassification == DependencyClassification.ReferencesMvc || - dependencyClassification == DependencyClassification.MvcReference) - { - classification = DependencyClassification.ReferencesMvc; - break; - } - } - - candidateEntry.Classification = classification; - - return classification; - } - } - - public IEnumerable GetCandidates() - { - foreach (var dependency in _runtimeDependencies) - { - if (ComputeClassification(dependency.Key) == DependencyClassification.ReferencesMvc) - { - yield return dependency.Value.Library; - } - } - } - - private class Dependency - { - public Dependency(RuntimeLibrary library, DependencyClassification classification) - { - Library = library; - Classification = classification; - } - - public RuntimeLibrary Library { get; } - - public DependencyClassification Classification { get; set; } - - public override string ToString() - { - return $"Library: {Library.Name}, Classification: {Classification}"; - } - } - - private enum DependencyClassification - { - Unknown = 0, - - /// - /// References (directly or transitively) one of the Mvc packages listed in - /// . - /// - ReferencesMvc = 1, - - /// - /// Does not reference (directly or transitively) one of the Mvc packages listed by - /// . - /// - DoesNotReferenceMvc = 2, - - /// - /// One of the references listed in . - /// - MvcReference = 3, - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs index 93109738dd..8e42894851 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs @@ -1368,6 +1368,76 @@ namespace Microsoft.AspNetCore.Mvc.Core internal static string FormatVirtualFileResultExecutor_NoFileProviderConfigured() => GetString("VirtualFileResultExecutor_NoFileProviderConfigured"); + /// + /// Type {0} specified by {1} is invalid. Type specified by {1} must derive from {2}. + /// + internal static string ApplicationPartFactory_InvalidFactoryType + { + get => GetString("ApplicationPartFactory_InvalidFactoryType"); + } + + /// + /// Type {0} specified by {1} is invalid. Type specified by {1} must derive from {2}. + /// + internal static string FormatApplicationPartFactory_InvalidFactoryType(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("ApplicationPartFactory_InvalidFactoryType"), p0, p1, p2); + + /// + /// {0} specified on {1} cannot be self referential. + /// + internal static string RelatedAssemblyAttribute_AssemblyCannotReferenceSelf + { + get => GetString("RelatedAssemblyAttribute_AssemblyCannotReferenceSelf"); + } + + /// + /// {0} specified on {1} cannot be self referential. + /// + internal static string FormatRelatedAssemblyAttribute_AssemblyCannotReferenceSelf(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("RelatedAssemblyAttribute_AssemblyCannotReferenceSelf"), p0, p1); + + /// + /// Related assembly '{0}' specified by assembly '{1}' could not be found in the directory {2}. Related assemblies must be co-located with the specifying assemblies. + /// + internal static string RelatedAssemblyAttribute_CouldNotBeFound + { + get => GetString("RelatedAssemblyAttribute_CouldNotBeFound"); + } + + /// + /// Related assembly '{0}' specified by assembly '{1}' could not be found in the directory {2}. Related assemblies must be co-located with the specifying assemblies. + /// + internal static string FormatRelatedAssemblyAttribute_CouldNotBeFound(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("RelatedAssemblyAttribute_CouldNotBeFound"), p0, p1, p2); + + /// + /// Each related assembly must be declared by exactly one assembly. The assembly '{0}' was declared as related assembly by the following: + /// + internal static string ApplicationAssembliesProvider_DuplicateRelatedAssembly + { + get => GetString("ApplicationAssembliesProvider_DuplicateRelatedAssembly"); + } + + /// + /// Each related assembly must be declared by exactly one assembly. The assembly '{0}' was declared as related assembly by the following: + /// + internal static string FormatApplicationAssembliesProvider_DuplicateRelatedAssembly(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("ApplicationAssembliesProvider_DuplicateRelatedAssembly"), p0); + + /// + /// Assembly '{0}' declared as a related assembly by assembly '{1}' cannot define additional related assemblies. + /// + internal static string ApplicationAssembliesProvider_RelatedAssemblyCannotDefineAdditional + { + get => GetString("ApplicationAssembliesProvider_RelatedAssemblyCannotDefineAdditional"); + } + + /// + /// Assembly '{0}' declared as a related assembly by assembly '{1}' cannot define additional related assemblies. + /// + internal static string FormatApplicationAssembliesProvider_RelatedAssemblyCannotDefineAdditional(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("ApplicationAssembliesProvider_RelatedAssemblyCannotDefineAdditional"), p0, p1); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx index d43e07dae3..e4326aba1d 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx @@ -421,4 +421,19 @@ No file provider has been configured to process the supplied file. - + + Type {0} specified by {1} is invalid. Type specified by {1} must derive from {2}. + + + {0} specified on {1} cannot be self referential. + + + Related assembly '{0}' specified by assembly '{1}' could not be found in the directory {2}. Related assemblies must be co-located with the specifying assemblies. + + + Each related assembly must be declared by exactly one assembly. The assembly '{0}' was declared as related assembly by the following: + + + Assembly '{0}' declared as a related assembly by assembly '{1}' cannot define additional related assemblies. + + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/CompiledRazorAssemblyApplicationPartFactory.cs b/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/CompiledRazorAssemblyApplicationPartFactory.cs index 4da198233a..d4b4243592 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/CompiledRazorAssemblyApplicationPartFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/CompiledRazorAssemblyApplicationPartFactory.cs @@ -13,9 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts public class CompiledRazorAssemblyApplicationPartFactory : ApplicationPartFactory { /// - public override IEnumerable GetApplicationParts( - Assembly assembly, - string configuration) + public override IEnumerable GetApplicationParts(Assembly assembly, string configuration) { if (assembly == null) { diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/CompiledRazorAssemblyPart.cs b/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/CompiledRazorAssemblyPart.cs index bed15f201f..36291ec4e8 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/CompiledRazorAssemblyPart.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/CompiledRazorAssemblyPart.cs @@ -2,19 +2,41 @@ // 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.Reflection; +using Microsoft.AspNetCore.Razor.Hosting; namespace Microsoft.AspNetCore.Mvc.ApplicationParts { - public class CompiledRazorAssemblyPart : ApplicationPart + /// + /// An for compiled Razor assemblies. + /// + public class CompiledRazorAssemblyPart : ApplicationPart, IRazorCompiledItemProvider { + /// + /// Initializes a new instance of . + /// + /// The public CompiledRazorAssemblyPart(Assembly assembly) { Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); } + /// + /// Gets the . + /// public Assembly Assembly { get; } + /// public override string Name => Assembly.GetName().Name; + + IEnumerable IRazorCompiledItemProvider.CompiledItems + { + get + { + var loader = new RazorCompiledItemLoader(); + return loader.LoadItems(Assembly); + } + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/IRazorCompiledItemProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/IRazorCompiledItemProvider.cs new file mode 100644 index 0000000000..bdf11d6d59 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/IRazorCompiledItemProvider.cs @@ -0,0 +1,19 @@ +// 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 Microsoft.AspNetCore.Razor.Hosting; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + /// + /// Exposes one or more instances from an . + /// + public interface IRazorCompiledItemProvider + { + /// + /// Gets a sequence of instances. + /// + IEnumerable CompiledItems { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/RazorCompiledItemFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/RazorCompiledItemFeatureProvider.cs new file mode 100644 index 0000000000..5e39299d1d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Razor/ApplicationParts/RazorCompiledItemFeatureProvider.cs @@ -0,0 +1,53 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Razor.Hosting; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + internal class RazorCompiledItemFeatureProvider : IApplicationFeatureProvider + { + public void PopulateFeature(IEnumerable parts, ViewsFeature feature) + { + foreach (var provider in parts.OfType()) + { + // Ensure parts do not specify views with differing cases. This is not supported + // at runtime and we should flag at as such for precompiled views. + var duplicates = provider.CompiledItems + .GroupBy(i => i.Identifier, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(g => g.Count() > 1); + + if (duplicates != null) + { + var viewsDiffereningInCase = string.Join(Environment.NewLine, duplicates.Select(d => d.Identifier)); + + var message = string.Join( + Environment.NewLine, + Resources.RazorViewCompiler_ViewPathsDifferOnlyInCase, + viewsDiffereningInCase); + throw new InvalidOperationException(message); + } + + foreach (var item in provider.CompiledItems) + { + var descriptor = GetCompiledViewDescriptor(item); + feature.ViewDescriptors.Add(descriptor); + } + } + } + + private static CompiledViewDescriptor GetCompiledViewDescriptor(RazorCompiledItem item) + { + var itemAssembly = item.Type.Assembly; + var razorViewAttribute = itemAssembly.GetCustomAttributes() + .FirstOrDefault(attribute => attribute.ViewType == item.Type); + return new CompiledViewDescriptor(item, razorViewAttribute); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs index e1ccf3c3b6..4b4c1ec3e8 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/CompiledViewDescriptor.cs @@ -63,6 +63,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation /// /// Gets or sets the decorating the view. /// + /// + /// May be null. + /// public RazorViewAttribute ViewAttribute { get; set; } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs index ebbcc9239f..deb57205de 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs @@ -7,136 +7,71 @@ using System.IO; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.AspNetCore.Razor.Hosting; +using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.Razor.Compilation { /// /// An for . /// + [Obsolete("This type is obsolete and will be removed in a future version. See " + nameof(IRazorCompiledItemProvider) + " for alternatives.")] public class ViewsFeatureProvider : IApplicationFeatureProvider { public static readonly string PrecompiledViewsAssemblySuffix = ".PrecompiledViews"; - public static readonly IReadOnlyList ViewAssemblySuffixes = new string[] - { - PrecompiledViewsAssemblySuffix, - ".Views", - }; - /// public void PopulateFeature(IEnumerable parts, ViewsFeature feature) { - var knownIdentifiers = new HashSet(StringComparer.OrdinalIgnoreCase); - var descriptors = new List(); foreach (var assemblyPart in parts.OfType()) { - var attributes = GetViewAttributes(assemblyPart); - var items = LoadItems(assemblyPart); + var viewAttributes = GetViewAttributes(assemblyPart) + .Select(attribute => (Attribute: attribute, RelativePath: ViewPath.NormalizePath(attribute.Path))); - var merged = Merge(items, attributes); - foreach (var item in merged) + var duplicates = viewAttributes.GroupBy(a => a.RelativePath, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(g => g.Count() > 1); + + if (duplicates != null) { - var descriptor = new CompiledViewDescriptor(item.item, item.attribute); - // We iterate through ApplicationPart instances appear in precendence order. - // If a view path appears in multiple views, we'll use the order to break ties. - if (knownIdentifiers.Add(descriptor.RelativePath)) + // Ensure parts do not specify views with differing cases. This is not supported + // at runtime and we should flag at as such for precompiled views. + var viewsDiffereningInCase = string.Join(Environment.NewLine, duplicates.Select(d => d.RelativePath)); + + var message = string.Join( + Environment.NewLine, + Resources.RazorViewCompiler_ViewPathsDifferOnlyInCase, + viewsDiffereningInCase); + throw new InvalidOperationException(message); + } + + foreach (var (attribute, relativePath) in viewAttributes) + { + var viewDescriptor = new CompiledViewDescriptor { - feature.ViewDescriptors.Add(descriptor); - } + ExpirationTokens = Array.Empty(), + RelativePath = relativePath, + ViewAttribute = attribute, + IsPrecompiled = true, + }; + + feature.ViewDescriptors.Add(viewDescriptor); } } } - private ICollection<(RazorCompiledItem item, RazorViewAttribute attribute)> Merge( - IReadOnlyList items, - IEnumerable attributes) - { - // This code is a intentionally defensive. We assume that it's possible to have duplicates - // of attributes, and also items that have a single kind of metadata, but not the other. - var dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - for (var i = 0; i < items.Count; i++) - { - var item = items[i]; - if (!dictionary.TryGetValue(item.Identifier, out var entry)) - { - dictionary.Add(item.Identifier, (item, null)); - - } - else if (entry.item == null) - { - dictionary[item.Identifier] = (item, entry.attribute); - } - } - - foreach (var attribute in attributes) - { - if (!dictionary.TryGetValue(attribute.Path, out var entry)) - { - dictionary.Add(attribute.Path, (null, attribute)); - } - else if (entry.attribute == null) - { - dictionary[attribute.Path] = (entry.item, attribute); - } - } - - return dictionary.Values; - } - - internal virtual IReadOnlyList LoadItems(AssemblyPart assemblyPart) - { - if (assemblyPart == null) - { - throw new ArgumentNullException(nameof(assemblyPart)); - } - - var viewAssembly = assemblyPart.Assembly; - if (viewAssembly != null) - { - var loader = new RazorCompiledItemLoader(); - return loader.LoadItems(viewAssembly); - } - - return Array.Empty(); - } - /// /// Gets the sequence of instances associated with the specified . /// /// The . /// The sequence of instances. protected virtual IEnumerable GetViewAttributes(AssemblyPart assemblyPart) - { - // We check if the method was overriden by a subclass and preserve the old behavior in that case. - if (GetViewAttributesOverriden()) - { - return GetViewAttributesLegacy(assemblyPart); - } - else - { - // It is safe to call this method for additional assembly parts even if there is a feature provider - // present on the pipeline that overrides getviewattributes as dependent parts are later in the list - // of application parts. - return GetViewAttributesFromCurrentAssembly(assemblyPart); - } - - bool GetViewAttributesOverriden() - { - const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance; - return GetType() != typeof(ViewsFeatureProvider) && - GetType().GetMethod(nameof(GetViewAttributes), bindingFlags).DeclaringType != typeof(ViewsFeatureProvider); - } - } - - private IEnumerable GetViewAttributesLegacy(AssemblyPart assemblyPart) { if (assemblyPart == null) { throw new ArgumentNullException(nameof(assemblyPart)); } - var featureAssembly = GetViewAssembly(assemblyPart); + var featureAssembly = GetFeatureAssembly(assemblyPart); if (featureAssembly != null) { return featureAssembly.GetCustomAttributes(); @@ -145,48 +80,34 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation return Enumerable.Empty(); } - private Assembly GetViewAssembly(AssemblyPart assemblyPart) + private static Assembly GetFeatureAssembly(AssemblyPart assemblyPart) { - if (assemblyPart.Assembly.IsDynamic || string.IsNullOrEmpty(assemblyPart.Assembly.Location)) + if (assemblyPart.Assembly.IsDynamic || string.IsNullOrEmpty((string)assemblyPart.Assembly.Location)) { return null; } - for (var i = 0; i < ViewAssemblySuffixes.Count; i++) - { - var fileName = assemblyPart.Assembly.GetName().Name + ViewAssemblySuffixes[i] + ".dll"; - var filePath = Path.Combine(Path.GetDirectoryName(assemblyPart.Assembly.Location), fileName); + var precompiledAssemblyFileName = assemblyPart.Assembly.GetName().Name + + PrecompiledViewsAssemblySuffix + + ".dll"; - if (File.Exists(filePath)) + var precompiledAssemblyFilePath = Path.Combine( + Path.GetDirectoryName(assemblyPart.Assembly.Location), + precompiledAssemblyFileName); + + if (File.Exists(precompiledAssemblyFilePath)) + { + try { - try - { - return Assembly.LoadFile(filePath); - } - catch (FileLoadException) - { - // Don't throw if assembly cannot be loaded. This can happen if the file is not a managed assembly. - } + return Assembly.LoadFile(precompiledAssemblyFilePath); + } + catch (FileLoadException) + { + // Don't throw if assembly cannot be loaded. This can happen if the file is not a managed assembly. } } return null; } - - private static IEnumerable GetViewAttributesFromCurrentAssembly(AssemblyPart assemblyPart) - { - if (assemblyPart == null) - { - throw new ArgumentNullException(nameof(assemblyPart)); - } - - var featureAssembly = assemblyPart.Assembly; - if (featureAssembly != null) - { - return featureAssembly.GetCustomAttributes(); - } - - return Enumerable.Empty(); - } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs index 5f669304fa..2eb17bc862 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/DependencyInjection/MvcRazorMvcCoreBuilderExtensions.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Razor.Extensions; @@ -74,10 +75,21 @@ namespace Microsoft.Extensions.DependencyInjection builder.PartManager.FeatureProviders.Add(new TagHelperFeatureProvider()); } + // ViewFeature items have precedence semantics - when two views have the same path \ identifier, + // the one that appears earlier in the list wins. Therefore the ordering of + // RazorCompiledItemFeatureProvider and ViewsFeatureProvider is pertinent - any view compiled + // using the Sdk will be prefered to views compiled using MvcPrecompilation. + if (!builder.PartManager.FeatureProviders.OfType().Any()) + { + builder.PartManager.FeatureProviders.Add(new RazorCompiledItemFeatureProvider()); + } + +#pragma warning disable CS0618 // Type or member is obsolete if (!builder.PartManager.FeatureProviders.OfType().Any()) { builder.PartManager.FeatureProviders.Add(new ViewsFeatureProvider()); } +#pragma warning restore CS0618 // Type or member is obsolete } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs index fca0cb415d..93b871cc76 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/RazorViewCompiler.cs @@ -100,17 +100,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal { logger.ViewCompilerLocatedCompiledView(precompiledView.RelativePath); - if (_precompiledViews.TryGetValue(precompiledView.RelativePath, out var otherValue)) + if (!_precompiledViews.ContainsKey(precompiledView.RelativePath)) { - var message = string.Join( - Environment.NewLine, - Resources.RazorViewCompiler_ViewPathsDifferOnlyInCase, - otherValue.RelativePath, - precompiledView.RelativePath); - throw new InvalidOperationException(message); + // View ordering has precedence semantics, a view with a higher precedence was + // already added to the list. + _precompiledViews.Add(precompiledView.RelativePath, precompiledView); } - - _precompiledViews.Add(precompiledView.RelativePath, precompiledView); } if (_precompiledViews.Count == 0) diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/AssemblyInfo.cs index 023f4c1f8e..831fbe57e1 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/AssemblyInfo.cs @@ -3,4 +3,5 @@ using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Razor.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs index 23a57a786e..9bb9390bcc 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/DefaultPageLoader.cs @@ -45,9 +45,8 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal var compileTask = Compiler.CompileAsync(actionDescriptor.RelativePath); var viewDescriptor = compileTask.GetAwaiter().GetResult(); - var pageAttribute = (RazorPageAttribute)viewDescriptor.ViewAttribute; - var context = new PageApplicationModelProviderContext(actionDescriptor, pageAttribute.ViewType.GetTypeInfo()); + var context = new PageApplicationModelProviderContext(actionDescriptor, viewDescriptor.Type.GetTypeInfo()); for (var i = 0; i < _applicationModelProviders.Length; i++) { _applicationModelProviders[i].OnProvidersExecuting(context); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/ApplicationAssembliesProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/ApplicationAssembliesProviderTest.cs new file mode 100644 index 0000000000..47f8a92b1f --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/ApplicationAssembliesProviderTest.cs @@ -0,0 +1,436 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyModel; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + public class ApplicationAssembliesProviderTest + { + private static readonly Assembly ThisAssembly = typeof(ApplicationAssembliesProviderTest).Assembly; + + [Fact] + public void ResolveAssemblies_ReturnsCurrentAssembly_IfNoDepsFileIsPresent() + { + // Arrange + var provider = new TestApplicationAssembliesProvider(); + + // Act + var result = provider.ResolveAssemblies(ThisAssembly); + + // Assert + Assert.Equal(new[] { ThisAssembly }, result); + } + + [Fact] + public void ResolveAssemblies_ReturnsRelatedAssembliesOrderedByName() + { + // Arrange + var assembly1 = typeof(ApplicationAssembliesProvider).Assembly; + var assembly2 = typeof(IActionResult).Assembly; + var assembly3 = typeof(FactAttribute).Assembly; + + var relatedAssemblies = new[] { assembly1, assembly2, assembly3 }; + var provider = new TestApplicationAssembliesProvider + { + GetRelatedAssembliesDelegate = (assembly) => relatedAssemblies, + }; + + // Act + var result = provider.ResolveAssemblies(ThisAssembly); + + // Assert + Assert.Equal(new[] { ThisAssembly, assembly2, assembly1, assembly3 }, result); + } + + [Fact] + public void ResolveAssemblies_ReturnsLibrariesFromTheDepsFileThatReferenceMvc() + { + // Arrange + var mvcAssembly = typeof(IActionResult).Assembly; + var classLibrary = typeof(FactAttribute).Assembly; + + var dependencyContext = GetDependencyContext(new[] + { + GetLibrary(ThisAssembly.GetName().Name, new[] { mvcAssembly.GetName().Name, classLibrary.GetName().Name }), + GetLibrary(mvcAssembly.GetName().Name), + GetLibrary(classLibrary.GetName().Name, new[] { mvcAssembly.GetName().Name }), + }); + + var provider = new TestApplicationAssembliesProvider + { + DependencyContext = dependencyContext, + }; + + // Act + var result = provider.ResolveAssemblies(ThisAssembly); + + // Assert + Assert.Equal(new[] { ThisAssembly, classLibrary, }, result); + } + + [Fact] + public void ResolveAssemblies_ReturnsRelatedAssembliesForLibrariesFromDepsFile() + { + // Arrange + var mvcAssembly = typeof(IActionResult).Assembly; + var classLibrary = typeof(object).Assembly; + var relatedPart = typeof(FactAttribute).Assembly; + + var dependencyContext = GetDependencyContext(new[] + { + GetLibrary(ThisAssembly.GetName().Name, new[] { relatedPart.GetName().Name, classLibrary.GetName().Name }), + GetLibrary(classLibrary.GetName().Name, new[] { mvcAssembly.GetName().Name }), + GetLibrary(relatedPart.GetName().Name, new[] { mvcAssembly.GetName().Name }), + GetLibrary(mvcAssembly.GetName().Name), + }); + + var provider = new TestApplicationAssembliesProvider + { + DependencyContext = dependencyContext, + GetRelatedAssembliesDelegate = (assembly) => + { + if (assembly == classLibrary) + { + return new[] { relatedPart }; + } + + return Array.Empty(); + }, + }; + + // Act + var result = provider.ResolveAssemblies(ThisAssembly); + + // Assert + Assert.Equal(new[] { ThisAssembly, classLibrary, relatedPart, }, result); + } + + [Fact] + public void ResolveAssemblies_ThrowsIfRelatedAssemblyDefinesAdditionalRelatedAssemblies() + { + // Arrange + var expected = $"Assembly 'TestRelatedAssembly' declared as a related assembly by assembly '{ThisAssembly}' cannot define additional related assemblies."; + var assembly1 = typeof(ApplicationAssembliesProvider).Assembly; + var assembly2 = new TestAssembly(); + + var relatedAssemblies = new[] { assembly1, assembly2 }; + var provider = new TestApplicationAssembliesProvider + { + GetRelatedAssembliesDelegate = (assembly) => relatedAssemblies, + }; + + // Act & Assert + var ex = Assert.Throws(() => provider.ResolveAssemblies(ThisAssembly).ToArray()); + Assert.Equal(expected, ex.Message); + } + + [Fact] + public void ResolveAssemblies_ThrowsIfMultipleAssembliesDeclareTheSameRelatedPart() + { + // Arrange + var mvcAssembly = typeof(IActionResult).Assembly; + var libraryAssembly1 = typeof(object).Assembly; + var libraryAssembly2 = typeof(HttpContext).Assembly; + var relatedPart = typeof(FactAttribute).Assembly; + var expected = string.Join( + Environment.NewLine, + $"Each related assembly must be declared by exactly one assembly. The assembly '{relatedPart.FullName}' was declared as related assembly by the following:", + libraryAssembly1.FullName, + libraryAssembly2.FullName); + + var dependencyContext = GetDependencyContext(new[] + { + GetLibrary(ThisAssembly.GetName().Name, new[] { relatedPart.GetName().Name, libraryAssembly1.GetName().Name }), + GetLibrary(libraryAssembly1.GetName().Name, new[] { mvcAssembly.GetName().Name }), + GetLibrary(libraryAssembly2.GetName().Name, new[] { mvcAssembly.GetName().Name }), + GetLibrary(mvcAssembly.GetName().Name), + }); + + var provider = new TestApplicationAssembliesProvider + { + DependencyContext = dependencyContext, + GetRelatedAssembliesDelegate = (assembly) => + { + if (assembly == libraryAssembly1 || assembly == libraryAssembly2) + { + return new[] { relatedPart }; + } + + return Array.Empty(); + }, + }; + + // Act & Assert + var ex = Assert.Throws(() => provider.ResolveAssemblies(ThisAssembly).ToArray()); + Assert.Equal(expected, ex.Message); + } + + [Fact] + public void CandidateResolver_ThrowsIfDependencyContextContainsDuplicateRuntimeLibraryNames() + { + // Arrange + var upperCaseLibrary = "Microsoft.AspNetCore.Mvc"; + var mixedCaseLibrary = "microsoft.aspNetCore.mvc"; + + var dependencyContext = GetDependencyContext(new[] + { + GetLibrary(mixedCaseLibrary), + GetLibrary(upperCaseLibrary), + }); + + // Act + var exception = Assert.Throws(() => ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext)); + + // Assert + Assert.Equal($"A duplicate entry for library reference {upperCaseLibrary} was found. Please check that all package references in all projects use the same casing for the same package references.", exception.Message); + } + + [Fact] + public void GetCandidateLibraries_IgnoresMvcAssemblies() + { + // Arrange + var expected = GetLibrary("SomeRandomAssembly", "Microsoft.AspNetCore.Mvc.Abstractions"); + var dependencyContext = GetDependencyContext(new[] + { + GetLibrary("Microsoft.AspNetCore.Mvc.Core"), + GetLibrary("Microsoft.AspNetCore.Mvc"), + GetLibrary("Microsoft.AspNetCore.Mvc.Abstractions"), + expected, + }); + + // Act + var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext); + + // Assert + Assert.Equal(new[] { expected }, candidates); + } + + [Fact] + public void GetCandidateLibraries_DoesNotThrow_IfLibraryDoesNotHaveRuntimeComponent() + { + // Arrange + var expected = GetLibrary("MyApplication", "Microsoft.AspNetCore.Server.Kestrel", "Microsoft.AspNetCore.Mvc"); + var dependencyContext = GetDependencyContext(new[] + { + expected, + GetLibrary("Microsoft.AspNetCore.Server.Kestrel", "Libuv"), + GetLibrary("Microsoft.AspNetCore.Mvc"), + }); + + // Act + var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext).ToList(); + + // Assert + Assert.Equal(new[] { expected }, candidates); + } + + [Fact] + public void GetCandidateLibraries_ReturnsLibrariesReferencingAnyMvcAssembly() + { + // Arrange + var dependencyContext = GetDependencyContext(new[] + { + GetLibrary("Foo", "Microsoft.AspNetCore.Mvc.Core"), + GetLibrary("Bar", "Microsoft.AspNetCore.Mvc"), + GetLibrary("Qux", "Not.Mvc.Assembly", "Unofficial.Microsoft.AspNetCore.Mvc"), + GetLibrary("Baz", "Microsoft.AspNetCore.Mvc.Abstractions"), + GetLibrary("Microsoft.AspNetCore.Mvc.Core"), + GetLibrary("Microsoft.AspNetCore.Mvc"), + GetLibrary("Not.Mvc.Assembly"), + GetLibrary("Unofficial.Microsoft.AspNetCore.Mvc"), + GetLibrary("Microsoft.AspNetCore.Mvc.Abstractions"), + }); + + // Act + var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext); + + // Assert + Assert.Equal(new[] { "Foo", "Bar", "Baz" }, candidates.Select(a => a.Name)); + } + + [Fact] + public void GetCandidateLibraries_LibraryNameComparisonsAreCaseInsensitive() + { + // Arrange + var dependencyContext = GetDependencyContext(new[] + { + GetLibrary("Foo", "MICROSOFT.ASPNETCORE.MVC.CORE"), + GetLibrary("Bar", "microsoft.aspnetcore.mvc"), + GetLibrary("Qux", "Not.Mvc.Assembly", "Unofficial.Microsoft.AspNetCore.Mvc"), + GetLibrary("Baz", "mIcRoSoFt.AsPnEtCoRe.MvC.aBsTrAcTiOnS"), + GetLibrary("Microsoft.AspNetCore.Mvc.Core"), + GetLibrary("LibraryA", "LIBRARYB"), + GetLibrary("LibraryB", "microsoft.aspnetcore.mvc"), + GetLibrary("Microsoft.AspNetCore.Mvc"), + GetLibrary("Not.Mvc.Assembly"), + GetLibrary("Unofficial.Microsoft.AspNetCore.Mvc"), + GetLibrary("Microsoft.AspNetCore.Mvc.Abstractions"), + }); + + // Act + var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext); + + // Assert + Assert.Equal(new[] { "Foo", "Bar", "Baz", "LibraryA", "LibraryB" }, candidates.Select(a => a.Name)); + } + + [Fact] + public void GetCandidateLibraries_ReturnsLibrariesWithTransitiveReferencesToAnyMvcAssembly() + { + // Arrange + var expectedLibraries = new[] { "Foo", "Bar", "Baz", "LibraryA", "LibraryB", "LibraryC", "LibraryE", "LibraryG", "LibraryH" }; + + var dependencyContext = GetDependencyContext(new[] + { + GetLibrary("Foo", "Bar"), + GetLibrary("Bar", "Microsoft.AspNetCore.Mvc"), + GetLibrary("Qux", "Not.Mvc.Assembly", "Unofficial.Microsoft.AspNetCore.Mvc"), + GetLibrary("Baz", "Microsoft.AspNetCore.Mvc.Abstractions"), + GetLibrary("Microsoft.AspNetCore.Mvc"), + GetLibrary("Not.Mvc.Assembly"), + GetLibrary("Microsoft.AspNetCore.Mvc.Abstractions"), + GetLibrary("Unofficial.Microsoft.AspNetCore.Mvc"), + GetLibrary("LibraryA", "LibraryB"), + GetLibrary("LibraryB","LibraryC"), + GetLibrary("LibraryC", "LibraryD", "Microsoft.AspNetCore.Mvc.Abstractions"), + GetLibrary("LibraryD"), + GetLibrary("LibraryE","LibraryF","LibraryG"), + GetLibrary("LibraryF"), + GetLibrary("LibraryG", "LibraryH"), + GetLibrary("LibraryH", "LibraryI", "Microsoft.AspNetCore.Mvc"), + GetLibrary("LibraryI"), + }); + + // Act + var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext); + + // Assert + Assert.Equal(expectedLibraries, candidates.Select(a => a.Name)); + } + + [Fact] + public void GetCandidateLibraries_SkipsMvcAssemblies() + { + // Arrange + var dependencyContext = GetDependencyContext(new[] + { + GetLibrary("MvcSandbox", "Microsoft.AspNetCore.Mvc.Core", "Microsoft.AspNetCore.Mvc"), + GetLibrary("Microsoft.AspNetCore.Mvc.Core", "Microsoft.AspNetCore.HttpAbstractions"), + GetLibrary("Microsoft.AspNetCore.HttpAbstractions"), + GetLibrary("Microsoft.AspNetCore.Mvc", "Microsoft.AspNetCore.Mvc.Abstractions", "Microsoft.AspNetCore.Mvc.Core"), + GetLibrary("Microsoft.AspNetCore.Mvc.Abstractions"), + GetLibrary("Microsoft.AspNetCore.Mvc.TagHelpers", "Microsoft.AspNetCore.Mvc.Razor"), + GetLibrary("Microsoft.AspNetCore.Mvc.Razor"), + GetLibrary("ControllersAssembly", "Microsoft.AspNetCore.Mvc"), + }); + + // Act + var candidates = ApplicationAssembliesProvider.GetCandidateLibraries(dependencyContext); + + // Assert + Assert.Equal(new[] { "MvcSandbox", "ControllersAssembly" }, candidates.Select(a => a.Name)); + } + + // This test verifies DefaultAssemblyPartDiscoveryProvider.ReferenceAssemblies reflects the actual loadable assemblies + // of the libraries that Microsoft.AspNetCore.Mvc depends on. + // If we add or remove dependencies, this test should be changed together. + [Fact] + public void ReferenceAssemblies_ReturnsLoadableReferenceAssemblies() + { + // Arrange + var excludeAssemblies = new string[] + { + "Microsoft.AspNetCore.Mvc.Core.Test", + "Microsoft.AspNetCore.Mvc.TestCommon", + "Microsoft.AspNetCore.Mvc.TestDiagnosticListener", + "Microsoft.AspNetCore.Mvc.WebApiCompatShim", + }; + + var additionalAssemblies = new[] + { + // The following assemblies are not reachable from Microsoft.AspNetCore.Mvc + "Microsoft.AspNetCore.Mvc.Formatters.Xml", + }; + + var dependencyContextLibraries = DependencyContext.Load(ThisAssembly) + .CompileLibraries + .Where(r => r.Name.StartsWith("Microsoft.AspNetCore.Mvc", StringComparison.OrdinalIgnoreCase) && + !excludeAssemblies.Contains(r.Name, StringComparer.OrdinalIgnoreCase)) + .Select(r => r.Name); + + var expected = dependencyContextLibraries + .Concat(additionalAssemblies) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(p => p, StringComparer.OrdinalIgnoreCase); + + // Act + var referenceAssemblies = ApplicationAssembliesProvider + .ReferenceAssemblies + .OrderBy(p => p, StringComparer.OrdinalIgnoreCase); + + // Assert + Assert.Equal(expected, referenceAssemblies, StringComparer.OrdinalIgnoreCase); + } + + private class TestApplicationAssembliesProvider : ApplicationAssembliesProvider + { + public DependencyContext DependencyContext { get; set; } + + public Func> GetRelatedAssembliesDelegate { get; set; } = (assembly) => Array.Empty(); + + protected override DependencyContext LoadDependencyContext(Assembly assembly) => DependencyContext; + + protected override IReadOnlyList GetRelatedAssemblies(Assembly assembly) => GetRelatedAssembliesDelegate(assembly); + + protected override IEnumerable GetLibraryAssemblies(DependencyContext dependencyContext, RuntimeLibrary runtimeLibrary) + { + var assemblyName = new AssemblyName(runtimeLibrary.Name); + yield return Assembly.Load(assemblyName); + } + } + + private static DependencyContext GetDependencyContext(RuntimeLibrary[] libraries) + { + var dependencyContext = new DependencyContext( + new TargetInfo("framework", "runtime", "signature", isPortable: true), + CompilationOptions.Default, + new CompilationLibrary[0], + libraries, + Enumerable.Empty()); + return dependencyContext; + } + + private static RuntimeLibrary GetLibrary(string name, params string[] dependencyNames) + { + var dependencies = dependencyNames?.Select(d => new Dependency(d, "42.0.0")) ?? new Dependency[0]; + + return new RuntimeLibrary( + "package", + name, + "23.0.0", + "hash", + new RuntimeAssetGroup[0], + new RuntimeAssetGroup[0], + new ResourceAssembly[0], + dependencies: dependencies.ToArray(), + serviceable: true); + } + + private class TestAssembly : Assembly + { + public override string FullName => "TestRelatedAssembly"; + + public override bool IsDefined(Type attributeType, bool inherit) + { + return attributeType == typeof(RelatedAssemblyAttribute); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/ApplicationPartManagerTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/ApplicationPartManagerTest.cs index 27735d2693..8b841f02ab 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/ApplicationPartManagerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/ApplicationPartManagerTest.cs @@ -24,7 +24,8 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts new ControllersFeatureProvider((f, v) => f.Values.Add($"ControllersFeatureProvider2{v}"))); var feature = new ControllersFeature(); - var expectedResults = new[] { + var expectedResults = new[] + { "ControllersFeatureProvider1ControllersPartA", "ControllersFeatureProvider1ControllersPartC", "ControllersFeatureProvider2ControllersPartA", diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/RelatedAssemblyPartTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/RelatedAssemblyPartTest.cs new file mode 100644 index 0000000000..8f3bbdb47c --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationParts/RelatedAssemblyPartTest.cs @@ -0,0 +1,104 @@ +// 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.Reflection; +using System.Reflection.Emit; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + public class RelatedAssemblyPartTest + { + private static readonly string AssemblyDirectory = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar); + + [Fact] + public void GetRelatedAssemblies_Noops_ForDynamicAssemblies() + { + // Arrange + var name = new AssemblyName($"DynamicAssembly-{Guid.NewGuid()}"); + var assembly = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndCollect); + + // Act + var result = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true); + + // Assert + Assert.Empty(result); + } + + [Fact] + public void GetRelatedAssemblies_ThrowsIfRelatedAttributeReferencesSelf() + { + // Arrange + var expected = "RelatedAssemblyAttribute specified on MyAssembly cannot be self referential."; + var assembly = new TestAssembly { AttributeAssembly = "MyAssembly" }; + + // Act & Assert + var ex = Assert.Throws(() => RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true)); + Assert.Equal(expected, ex.Message); + } + + [Fact] + public void GetRelatedAssemblies_ThrowsIfAssemblyCannotBeFound() + { + // Arrange + var expected = $"Related assembly 'DoesNotExist' specified by assembly 'MyAssembly' could not be found in the directory {AssemblyDirectory}. Related assemblies must be co-located with the specifying assemblies."; + var assemblyPath = Path.Combine(AssemblyDirectory, "MyAssembly.dll"); + var assembly = new TestAssembly + { + AttributeAssembly = "DoesNotExist" + }; + + // Act & Assert + var ex = Assert.Throws(() => RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true)); + Assert.Equal(expected, ex.Message); + Assert.Equal(Path.Combine(AssemblyDirectory, "DoesNotExist.dll"), ex.FileName); + } + + [Fact] + public void GetRelatedAssemblies_LoadsRelatedAssembly() + { + // Arrange + var destination = Path.Combine(AssemblyDirectory, "RelatedAssembly.dll"); + var assembly = new TestAssembly + { + AttributeAssembly = "RelatedAssembly", + }; + var relatedAssembly = typeof(RelatedAssemblyPartTest).Assembly; + + try + { + File.WriteAllBytes(destination, new byte[0]); + var result = RelatedAssemblyAttribute.GetRelatedAssemblies(assembly, throwOnError: true, file => + { + Assert.Equal(file, destination); + return relatedAssembly; + }); + Assert.Equal(new[] { relatedAssembly }, result); + } + finally + { + File.Delete(destination); + } + } + + private class TestAssembly : Assembly + { + public override AssemblyName GetName() + { + return new AssemblyName("MyAssembly"); + } + + public string AttributeAssembly { get; set; } + + public override string CodeBase => Path.Combine(AssemblyDirectory, "MyAssembly.dll"); + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + var attribute = new RelatedAssemblyAttribute(AttributeAssembly); + return new[] { attribute }; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultAssemblyPartDiscoveryProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultAssemblyPartDiscoveryProviderTest.cs deleted file mode 100644 index ee5a6a5c69..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultAssemblyPartDiscoveryProviderTest.cs +++ /dev/null @@ -1,501 +0,0 @@ -// 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.Runtime.InteropServices; -using Microsoft.Extensions.DependencyModel; -using Xunit; - -namespace Microsoft.AspNetCore.Mvc.Internal -{ - public class DefaultAssemblyPartDiscoveryProviderTests - { - private static readonly Assembly CurrentAssembly = - typeof(DefaultAssemblyPartDiscoveryProviderTests).GetTypeInfo().Assembly; - - [Fact] - public void CandidateResolver_ThrowsIfDependencyContextContainsDuplicateRuntimeLibraryNames() - { - // Arrange - var upperCaseLibrary = "Microsoft.AspNetCore.Mvc"; - var mixedCaseLibrary = "microsoft.aspNetCore.mvc"; - - var dependencyContext = new DependencyContext( - new TargetInfo("framework", "runtime", "signature", isPortable: true), - CompilationOptions.Default, - new CompilationLibrary[0], - new[] - { - GetLibrary(mixedCaseLibrary), - GetLibrary(upperCaseLibrary), - }, - Enumerable.Empty()); - - // Act - var exception = Assert.Throws(() => DefaultAssemblyPartDiscoveryProvider.GetCandidateLibraries(dependencyContext)); - - // Assert - Assert.Equal($"A duplicate entry for library reference {upperCaseLibrary} was found. Please check that all package references in all projects use the same casing for the same package references.", exception.Message); - } - - [Fact] - public void GetCandidateLibraries_IgnoresMvcAssemblies() - { - // Arrange - var expected = GetLibrary("SomeRandomAssembly", "Microsoft.AspNetCore.Mvc.Abstractions"); - var dependencyContext = new DependencyContext( - new TargetInfo("framework", "runtime", "signature", isPortable: true), - CompilationOptions.Default, - new CompilationLibrary[0], - new[] - { - GetLibrary("Microsoft.AspNetCore.Mvc.Core"), - GetLibrary("Microsoft.AspNetCore.Mvc"), - GetLibrary("Microsoft.AspNetCore.Mvc.Abstractions"), - expected, - }, - Enumerable.Empty()); - - // Act - var candidates = DefaultAssemblyPartDiscoveryProvider.GetCandidateLibraries(dependencyContext); - - // Assert - Assert.Equal(new[] { expected }, candidates); - } - - [Theory] - [MemberData(nameof(ResolveAdditionalReferencesData))] - public void ResolveAdditionalReferences_DiscoversAdditionalReferences(ResolveAdditionalReferencesTestData testData) - { - // Arrange - var resolver = testData.AssemblyResolver; - DefaultAssemblyPartDiscoveryProvider.AssemblyResolver = path => resolver.ContainsKey(path); - DefaultAssemblyPartDiscoveryProvider.AssemblyLoader = path => resolver.TryGetValue(path, out var result) ? result : null; - - // Arrange & Act - var (additionalReferences, entryAssemblyAdditionalReferences) = - DefaultAssemblyPartDiscoveryProvider.ResolveAdditionalReferences(testData.EntryAssembly, testData.CandidateAssemblies); - - var additionalRefs = additionalReferences.Select(a => a.FullName).OrderBy(id => id).ToArray(); - var entryAssemblyAdditionalRefs = entryAssemblyAdditionalReferences.Select(a => a.FullName).OrderBy(id => id).ToArray(); - - // Assert - Assert.Equal(testData.ExpectedAdditionalReferences, additionalRefs); - Assert.Equal(testData.ExpectedEntryAssemblyAdditionalReferences, entryAssemblyAdditionalRefs); - } - - public class ResolveAdditionalReferencesTestData - { - public Assembly EntryAssembly { get; set; } - public SortedSet CandidateAssemblies { get; set; } - public IDictionary AssemblyResolver { get; set; } - public string[] ExpectedAdditionalReferences { get; set; } - public string[] ExpectedEntryAssemblyAdditionalReferences { get; set; } - } - - public static TheoryData ResolveAdditionalReferencesData - { - get - { - var data = new TheoryData(); - var noCandidates = Array.Empty(); - var noResolvable = new Dictionary(); - var noAdditionalReferences = new string[] { }; - - // Single assembly app no precompilation - var aAssembly = new DiscoveryTestAssembly("A"); - var singleAppNoPrecompilation = new ResolveAdditionalReferencesTestData - { - EntryAssembly = aAssembly, - CandidateAssemblies = CreateCandidates(aAssembly), - AssemblyResolver = noResolvable, - ExpectedAdditionalReferences = Array.Empty(), - ExpectedEntryAssemblyAdditionalReferences = Array.Empty() - }; - data.Add(singleAppNoPrecompilation); - - // Single assembly app with old precompilation not included in the graph - var bAssembly = new DiscoveryTestAssembly("B"); - var (bPath, bPrecompiledViews) = CreateResolvablePrecompiledViewsAssembly("B"); - var singleAssemblyPrecompilationNotInGraph = new ResolveAdditionalReferencesTestData - { - EntryAssembly = bAssembly, - CandidateAssemblies = CreateCandidates(bAssembly), - AssemblyResolver = new Dictionary { [bPath] = bPrecompiledViews }, - ExpectedAdditionalReferences = noAdditionalReferences, - ExpectedEntryAssemblyAdditionalReferences = new[] { bPrecompiledViews.FullName } - }; - data.Add(singleAssemblyPrecompilationNotInGraph); - - //// Single assembly app with new precompilation not included in the graph - var cAssembly = new DiscoveryTestAssembly( - "C", - DiscoveryTestAssembly.DefaultLocationBase, - new[] { ("C.PrecompiledViews.dll", true) }); - var (cPath, cPrecompiledViews) = CreateResolvablePrecompiledViewsAssembly("C"); - var singleAssemblyNewPrecompilationNotInGraph = new ResolveAdditionalReferencesTestData - { - EntryAssembly = cAssembly, - CandidateAssemblies = CreateCandidates(cAssembly), - AssemblyResolver = new Dictionary { [cPath] = cPrecompiledViews }, - ExpectedAdditionalReferences = noAdditionalReferences, - ExpectedEntryAssemblyAdditionalReferences = new[] { cPrecompiledViews.FullName } - }; - data.Add(singleAssemblyNewPrecompilationNotInGraph); - - //// Single assembly app with new precompilation included in the graph - var dAssembly = new DiscoveryTestAssembly( - "D", - DiscoveryTestAssembly.DefaultLocationBase, - new[] { (Path.Combine(DiscoveryTestAssembly.DefaultLocationBase, "subfolder", "D.PrecompiledViews.dll"), true) }); - var (dPath, dPrecompiledViews) = CreateResolvablePrecompiledViewsAssembly("D"); - var singleAssemblyNewPrecompilationInGraph = new ResolveAdditionalReferencesTestData - { - EntryAssembly = dAssembly, - CandidateAssemblies = CreateCandidates(dAssembly, dPrecompiledViews), - AssemblyResolver = new Dictionary { [dPath] = dPrecompiledViews }, - ExpectedAdditionalReferences = noAdditionalReferences, - ExpectedEntryAssemblyAdditionalReferences = new[] { dPrecompiledViews.FullName } - }; - data.Add(singleAssemblyNewPrecompilationInGraph); - - //// Single assembly app with new precompilation included in the graph optional part - var hAssembly = new DiscoveryTestAssembly( - "h", - DiscoveryTestAssembly.DefaultLocationBase, - new[] { ("H.PrecompiledViews.dll", false) }); - var (hPath, hPrecompiledViews) = CreateResolvablePrecompiledViewsAssembly("H"); - var singleAssemblyNewPrecompilationInGraphOptionalDependency = new ResolveAdditionalReferencesTestData - { - EntryAssembly = hAssembly, - CandidateAssemblies = CreateCandidates(hAssembly, hPrecompiledViews), - AssemblyResolver = new Dictionary { [hPath] = hPrecompiledViews }, - ExpectedAdditionalReferences = noAdditionalReferences, - ExpectedEntryAssemblyAdditionalReferences = noAdditionalReferences - }; - data.Add(singleAssemblyNewPrecompilationInGraphOptionalDependency); - - //// Entry assembly with two dependencies app with new precompilation included in the graph - var eAssembly = new DiscoveryTestAssembly( - "E", - DiscoveryTestAssembly.DefaultLocationBase, - new[] { ("E.PrecompiledViews.dll", true) }); - var (ePath, ePrecompiledViews) = CreateResolvablePrecompiledViewsAssembly("E"); - - var fAssembly = new DiscoveryTestAssembly( - "F", - DiscoveryTestAssembly.DefaultLocationBase, - new[] { ("F.PrecompiledViews.dll", true) }); - var (fPath, fPrecompiledViews) = CreateResolvablePrecompiledViewsAssembly("F"); - - var gAssembly = new DiscoveryTestAssembly( - "G", - DiscoveryTestAssembly.DefaultLocationBase, - new[] { (Path.Combine(DiscoveryTestAssembly.DefaultLocationBase, "subfolder", "G.PrecompiledViews.dll"), true) }); - - var (gPath, gPrecompiledViews) = CreateResolvablePrecompiledViewsAssembly("G"); - var multipleAssembliesNewPrecompilationInGraph = new ResolveAdditionalReferencesTestData - { - EntryAssembly = gAssembly, - CandidateAssemblies = CreateCandidates( - fAssembly, - fPrecompiledViews, - gAssembly, - gPrecompiledViews, - eAssembly, - ePrecompiledViews), - AssemblyResolver = new Dictionary { - [ePath] = ePrecompiledViews, - [fPath] = fPrecompiledViews, - [gPath] = gPrecompiledViews - }, - ExpectedAdditionalReferences = new[] { ePrecompiledViews.FullName, fPrecompiledViews.FullName }, - ExpectedEntryAssemblyAdditionalReferences = new[] { - gPrecompiledViews.FullName - } - }; - data.Add(multipleAssembliesNewPrecompilationInGraph); - - return data; - } - } - - private static SortedSet CreateCandidates(params Assembly[] assemblies) => - new SortedSet(assemblies, DefaultAssemblyPartDiscoveryProvider.FullNameAssemblyComparer.Instance); - - private static (string, Assembly) CreateResolvablePrecompiledViewsAssembly(string name, string path = null) => - (path ?? Path.Combine(DiscoveryTestAssembly.DefaultLocationBase, $"{name}.PrecompiledViews.dll"), - new DiscoveryTestAssembly($"{name}.PrecompiledViews")); - - [Fact] - public void GetCandidateLibraries_DoesNotThrow_IfLibraryDoesNotHaveRuntimeComponent() - { - // Arrange - var expected = GetLibrary("MyApplication", "Microsoft.AspNetCore.Server.Kestrel", "Microsoft.AspNetCore.Mvc"); - var deps = new DependencyContext( - new TargetInfo("netcoreapp2.0", "rurntime", "signature", isPortable: true), - CompilationOptions.Default, - Enumerable.Empty(), - new[] - { - expected, - GetLibrary("Microsoft.AspNetCore.Server.Kestrel", "Libuv"), - GetLibrary("Microsoft.AspNetCore.Mvc"), - }, - Enumerable.Empty()); - - // Act - var candidates = DefaultAssemblyPartDiscoveryProvider.GetCandidateLibraries(deps).ToList(); - - // Assert - Assert.Equal(new[] { expected }, candidates); - } - - [Fact] - public void CandidateAssemblies_ReturnsEntryAssemblyIfDependencyContextIsNull() - { - // Arrange & Act - var candidates = DefaultAssemblyPartDiscoveryProvider.GetCandidateAssemblies(CurrentAssembly, dependencyContext: null); - - // Assert - Assert.Equal(new[] { CurrentAssembly }, candidates); - } - - [Fact] - public void GetCandidateLibraries_ReturnsLibrariesReferencingAnyMvcAssembly() - { - // Arrange - var dependencyContext = new DependencyContext( - new TargetInfo("framework", "runtime", "signature", isPortable: true), - CompilationOptions.Default, - new CompilationLibrary[0], - new[] - { - GetLibrary("Foo", "Microsoft.AspNetCore.Mvc.Core"), - GetLibrary("Bar", "Microsoft.AspNetCore.Mvc"), - GetLibrary("Qux", "Not.Mvc.Assembly", "Unofficial.Microsoft.AspNetCore.Mvc"), - GetLibrary("Baz", "Microsoft.AspNetCore.Mvc.Abstractions"), - GetLibrary("Microsoft.AspNetCore.Mvc.Core"), - GetLibrary("Microsoft.AspNetCore.Mvc"), - GetLibrary("Not.Mvc.Assembly"), - GetLibrary("Unofficial.Microsoft.AspNetCore.Mvc"), - GetLibrary("Microsoft.AspNetCore.Mvc.Abstractions"), - - }, - Enumerable.Empty()); - - // Act - var candidates = DefaultAssemblyPartDiscoveryProvider.GetCandidateLibraries(dependencyContext); - - // Assert - Assert.Equal(new[] { "Foo", "Bar", "Baz" }, candidates.Select(a => a.Name)); - } - - [Fact] - public void GetCandidateLibraries_LibraryNameComparisonsAreCaseInsensitive() - { - // Arrange - var dependencyContext = new DependencyContext( - new TargetInfo("framework", "runtime", "signature", isPortable: true), - CompilationOptions.Default, - new CompilationLibrary[0], - new[] - { - GetLibrary("Foo", "MICROSOFT.ASPNETCORE.MVC.CORE"), - GetLibrary("Bar", "microsoft.aspnetcore.mvc"), - GetLibrary("Qux", "Not.Mvc.Assembly", "Unofficial.Microsoft.AspNetCore.Mvc"), - GetLibrary("Baz", "mIcRoSoFt.AsPnEtCoRe.MvC.aBsTrAcTiOnS"), - GetLibrary("Microsoft.AspNetCore.Mvc.Core"), - GetLibrary("LibraryA", "LIBRARYB"), - GetLibrary("LibraryB", "microsoft.aspnetcore.mvc"), - GetLibrary("Microsoft.AspNetCore.Mvc"), - GetLibrary("Not.Mvc.Assembly"), - GetLibrary("Unofficial.Microsoft.AspNetCore.Mvc"), - GetLibrary("Microsoft.AspNetCore.Mvc.Abstractions"), - }, - Enumerable.Empty()); - - // Act - var candidates = DefaultAssemblyPartDiscoveryProvider.GetCandidateLibraries(dependencyContext); - - // Assert - Assert.Equal(new[] { "Foo", "Bar", "Baz", "LibraryA", "LibraryB" }, candidates.Select(a => a.Name)); - } - - [Fact] - public void GetCandidateLibraries_ReturnsLibrariesWithTransitiveReferencesToAnyMvcAssembly() - { - // Arrange - var expectedLibraries = new[] { "Foo", "Bar", "Baz", "LibraryA", "LibraryB", "LibraryC", "LibraryE", "LibraryG", "LibraryH" }; - - var dependencyContext = new DependencyContext( - new TargetInfo("framework", "runtime", "signature", isPortable: true), - CompilationOptions.Default, - new CompilationLibrary[0], - new[] - { - GetLibrary("Foo", "Bar"), - GetLibrary("Bar", "Microsoft.AspNetCore.Mvc"), - GetLibrary("Qux", "Not.Mvc.Assembly", "Unofficial.Microsoft.AspNetCore.Mvc"), - GetLibrary("Baz", "Microsoft.AspNetCore.Mvc.Abstractions"), - GetLibrary("Microsoft.AspNetCore.Mvc"), - GetLibrary("Not.Mvc.Assembly"), - GetLibrary("Microsoft.AspNetCore.Mvc.Abstractions"), - GetLibrary("Unofficial.Microsoft.AspNetCore.Mvc"), - GetLibrary("LibraryA", "LibraryB"), - GetLibrary("LibraryB","LibraryC"), - GetLibrary("LibraryC", "LibraryD", "Microsoft.AspNetCore.Mvc.Abstractions"), - GetLibrary("LibraryD"), - GetLibrary("LibraryE","LibraryF","LibraryG"), - GetLibrary("LibraryF"), - GetLibrary("LibraryG", "LibraryH"), - GetLibrary("LibraryH", "LibraryI", "Microsoft.AspNetCore.Mvc"), - GetLibrary("LibraryI") - }, - Enumerable.Empty()); - - // Act - var candidates = DefaultAssemblyPartDiscoveryProvider.GetCandidateLibraries(dependencyContext); - - // Assert - Assert.Equal(expectedLibraries, candidates.Select(a => a.Name)); - } - - [Fact] - public void GetCandidateLibraries_SkipsMvcAssemblies() - { - // Arrange - var dependencyContext = new DependencyContext( - new TargetInfo("framework", "runtime", "signature", isPortable: true), - CompilationOptions.Default, - new CompilationLibrary[0], - new[] - { - GetLibrary("MvcSandbox", "Microsoft.AspNetCore.Mvc.Core", "Microsoft.AspNetCore.Mvc"), - GetLibrary("Microsoft.AspNetCore.Mvc.Core", "Microsoft.AspNetCore.HttpAbstractions"), - GetLibrary("Microsoft.AspNetCore.HttpAbstractions"), - GetLibrary("Microsoft.AspNetCore.Mvc", "Microsoft.AspNetCore.Mvc.Abstractions", "Microsoft.AspNetCore.Mvc.Core"), - GetLibrary("Microsoft.AspNetCore.Mvc.Abstractions"), - GetLibrary("Microsoft.AspNetCore.Mvc.TagHelpers", "Microsoft.AspNetCore.Mvc.Razor"), - GetLibrary("Microsoft.AspNetCore.Mvc.Razor"), - GetLibrary("ControllersAssembly", "Microsoft.AspNetCore.Mvc"), - }, - Enumerable.Empty()); - - // Act - var candidates = DefaultAssemblyPartDiscoveryProvider.GetCandidateLibraries(dependencyContext); - - // Assert - Assert.Equal(new[] { "MvcSandbox", "ControllersAssembly" }, candidates.Select(a => a.Name)); - } - - // This test verifies DefaultAssemblyPartDiscoveryProvider.ReferenceAssemblies reflects the actual loadable assemblies - // of the libraries that Microsoft.AspNetCore.Mvc depends on. - // If we add or remove dependencies, this test should be changed together. - [Fact] - public void ReferenceAssemblies_ReturnsLoadableReferenceAssemblies() - { - // Arrange - var excludeAssemblies = new string[] - { - "Microsoft.AspNetCore.Mvc.Core.Test", - "Microsoft.AspNetCore.Mvc.TestCommon", - "Microsoft.AspNetCore.Mvc.TestDiagnosticListener", - "Microsoft.AspNetCore.Mvc.WebApiCompatShim", - }; - - var additionalAssemblies = new[] - { - // The following assemblies are not reachable from Microsoft.AspNetCore.Mvc - "Microsoft.AspNetCore.Mvc.Formatters.Xml", - "Microsoft.AspnetCore.All", - }; - - var dependencyContextLibraries = DependencyContext.Load(CurrentAssembly) - .RuntimeLibraries - .Where(r => r.Name.StartsWith("Microsoft.AspNetCore.Mvc", StringComparison.OrdinalIgnoreCase) && - !excludeAssemblies.Contains(r.Name, StringComparer.OrdinalIgnoreCase)) - .Select(r => r.Name); - - var expected = dependencyContextLibraries - .Concat(additionalAssemblies) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(p => p, StringComparer.OrdinalIgnoreCase); - - // Act - var referenceAssemblies = DefaultAssemblyPartDiscoveryProvider - .ReferenceAssemblies - .OrderBy(p => p, StringComparer.OrdinalIgnoreCase); - - // Assert - Assert.Equal(expected, referenceAssemblies, StringComparer.OrdinalIgnoreCase); - } - - private static RuntimeLibrary GetLibrary(string name, params string[] dependencyNames) - { - var dependencies = dependencyNames?.Select(d => new Dependency(d, "42.0.0")) ?? new Dependency[0]; - - return new RuntimeLibrary( - "package", - name, - "23.0.0", - "hash", - new RuntimeAssetGroup[0], - new RuntimeAssetGroup[0], - new ResourceAssembly[0], - dependencies: dependencies.ToArray(), - serviceable: true); - } - - private class DiscoveryTestAssembly : Assembly - { - private readonly string _fullName; - private readonly string _location; - private readonly Attribute[] _additionalDependencies; - public static readonly string DefaultLocationBase = - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"c:\app\" : "/app/"; - - public DiscoveryTestAssembly(string fullName, string location = null) - : this( - fullName, - location ?? Path.Combine(DefaultLocationBase, new AssemblyName(fullName).Name + ".dll"), - Array.Empty<(string, bool)>()) - { } - - public DiscoveryTestAssembly(string fullName, string location, IEnumerable<(string, bool)> additionalDependencies) - { - _fullName = fullName; - _location = location; - _additionalDependencies = additionalDependencies - .Select(ad => new AssemblyMetadataAttribute( - "Microsoft.AspNetCore.Mvc.AdditionalReference", - $"{ad.Item1},{ad.Item2}")).ToArray(); - } - - public override string FullName => _fullName; - - public override string Location => _location; - - public override object[] GetCustomAttributes(bool inherit) => _additionalDependencies; - - public override object[] GetCustomAttributes(Type attributeType, bool inherit) - { - var attributes = _additionalDependencies - .Where(t => t.GetType().IsAssignableFrom(attributeType)) - .ToArray(); - - var result = Array.CreateInstance(attributeType, attributes.Length); - attributes.CopyTo(result, 0); - return (object[])result; - } - - public override AssemblyName GetName(bool copiedName) => new AssemblyName(FullName); - - public override AssemblyName GetName() => new AssemblyName(FullName); - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorBuildTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorBuildTest.cs index 8be08f4d48..187e9d9ab3 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorBuildTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorBuildTest.cs @@ -64,5 +64,18 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal("Hello from runtime-compiled rzc view!", responseBody.Trim()); } + + [Fact] + public async Task RzcViewsArePreferredToPrecompiledViews() + { + // Verifies that when two views have the same paths, the one compiled using rzc is preferred to the one from Precompilation. + // Act + var response = await Client.GetAsync("http://localhost/Common/View"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Hello from buildtime-compiled rzc view!", responseBody.Trim()); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/ApplicationParts/RazorCompiledItemFeatureProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/ApplicationParts/RazorCompiledItemFeatureProviderTest.cs new file mode 100644 index 0000000000..ffcc6ad8cf --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/ApplicationParts/RazorCompiledItemFeatureProviderTest.cs @@ -0,0 +1,131 @@ +// 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.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.Razor.Compilation; +using Microsoft.AspNetCore.Razor.Hosting; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.ApplicationParts +{ + public class RazorCompiledItemFeatureProviderTest + { + [Fact] + public void PopulateFeature_AddsItemsFromProviderTypes() + { + // Arrange + var item1 = Mock.Of(i => i.Identifier == "Item1" && i.Type == typeof(TestView)); + var item2 = Mock.Of(i => i.Identifier == "Item2" && i.Type == typeof(TestPage)); + var part1 = new AssemblyPart(typeof(RazorCompiledItemFeatureProviderTest).Assembly); + var part2 = new Mock(); + part2 + .As() + .Setup(p => p.CompiledItems).Returns(new[] { item1, item2, }); + var featureProvider = new RazorCompiledItemFeatureProvider(); + var feature = new ViewsFeature(); + + // Act + featureProvider.PopulateFeature(new[] { part1, part2.Object }, feature); + + // Assert + Assert.Equal(new[] { item1, item2 }, feature.ViewDescriptors.Select(d => d.Item)); + } + + [Fact] + public void PopulateFeature_PopulatesRazorViewAttributeFromTypeAssembly() + { + // Arrange + var item1 = Mock.Of(i => i.Identifier == "Item1" && i.Type == typeof(TestView)); + var item2 = Mock.Of(i => i.Identifier == "Item2" && i.Type == typeof(TestPage)); + + var attribute1 = new RazorViewAttribute("Item1", typeof(TestView)); + var attribute2 = new RazorViewAttribute("Item2", typeof(TestPage)); + + var assembly = new TestAssembly(new[] { attribute1, attribute2 }); + + var part1 = new AssemblyPart(assembly); + var part2 = new Mock(); + part2 + .As() + .Setup(p => p.CompiledItems).Returns(new[] { item1, item2, }); + var featureProvider = new RazorCompiledItemFeatureProvider(); + var feature = new ViewsFeature(); + + // Act + featureProvider.PopulateFeature(new[] { part1, part2.Object }, feature); + + // Assert + Assert.Equal(new[] { item1, item2 }, feature.ViewDescriptors.Select(d => d.Item)); + } + + [Fact] + public void PopulateFeature_AllowsDuplicateItemsFromMultipleParts() + { + // Arrange + var item1 = Mock.Of(i => i.Identifier == "Item" && i.Type == typeof(TestView)); + var item2 = Mock.Of(i => i.Identifier == "Item" && i.Type == typeof(TestPage)); + var part1 = new Mock(); + part1 + .As() + .Setup(p => p.CompiledItems).Returns(new[] { item1, }); + var part2 = new Mock(); + part2 + .As() + .Setup(p => p.CompiledItems).Returns(new[] { item2, }); + var featureProvider = new RazorCompiledItemFeatureProvider(); + var feature = new ViewsFeature(); + + // Act + featureProvider.PopulateFeature(new[] { part1.Object, part2.Object }, feature); + + // Assert + Assert.Equal(new[] { item1, item2 }, feature.ViewDescriptors.Select(d => d.Item)); + } + + [Fact] + public void PopulateFeature_ThrowsIfTwoItemsFromSamePart_OnlyDifferInCase() + { + // Arrange + var item1 = Mock.Of(i => i.Identifier == "Item"); + var item2 = Mock.Of(i => i.Identifier == "item"); + var expected = string.Join( + Environment.NewLine, + "The following precompiled view paths differ only in case, which is not supported:", + "Item", + "item"); + var part1 = new AssemblyPart(typeof(RazorCompiledItemFeatureProviderTest).Assembly); + var part2 = new Mock(); + part2 + .As() + .Setup(p => p.CompiledItems).Returns(new[] { item1, item2, }); + var featureProvider = new RazorCompiledItemFeatureProvider(); + var feature = new ViewsFeature(); + + // Act & Assert + var ex = Assert.Throws(() => featureProvider.PopulateFeature(new[] { part1, part2.Object }, feature)); + Assert.Equal(expected, ex.Message); + } + + private class TestAssembly : Assembly + { + private readonly object[] _attributes; + + public TestAssembly(object[] attributes) + { + _attributes = attributes; + } + + public override object[] GetCustomAttributes(Type attributeType, bool inherit) + { + return _attributes; + } + } + + private class TestView { } + + private class TestPage { } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs index 02b64f977c..4b76f0c8d9 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs @@ -6,27 +6,26 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; -using System.Text; using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.AspNetCore.Razor.Hosting; -using Moq; using Xunit; namespace Microsoft.AspNetCore.Mvc.Razor.Compilation { +#pragma warning disable CS0618 // Type or member is obsolete public class ViewsFeatureProviderTest { [Fact] public void PopulateFeature_ReturnsEmptySequenceIfNoAssemblyPartHasViewAssembly() { // Arrange - var partManager = new ApplicationPartManager(); - partManager.ApplicationParts.Add(new AssemblyPart(typeof(ViewsFeatureProviderTest).Assembly)); - partManager.FeatureProviders.Add(new ViewsFeatureProvider()); + var applicationPartManager = new ApplicationPartManager(); + applicationPartManager.ApplicationParts.Add( + new AssemblyPart(typeof(ViewsFeatureProviderTest).GetTypeInfo().Assembly)); + applicationPartManager.FeatureProviders.Add(new ViewsFeatureProvider()); var feature = new ViewsFeature(); // Act - partManager.PopulateFeature(feature); + applicationPartManager.PopulateFeature(feature); // Assert Assert.Empty(feature.ViewDescriptors); @@ -36,31 +35,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation public void PopulateFeature_ReturnsViewsFromAllAvailableApplicationParts() { // Arrange - var part1 = new AssemblyPart(typeof(object).Assembly); - var part2 = new AssemblyPart(GetType().Assembly); - - var items = new Dictionary> - { - { - part1, - new[] - { - new TestRazorCompiledItem(typeof(object), "mvc.1.0.view", "/Views/test/Index.cshtml", new object[]{ }), - - // This one doesn't have a RazorViewAttribute - new TestRazorCompiledItem(typeof(StringBuilder), "mvc.1.0.view", "/Views/test/About.cshtml", new object[]{ }), - } - }, - { - part2, - new[] - { - new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Areas/Admin/Views/Index.cshtml", new object[]{ }), - } - }, - }; - - var attributes = new Dictionary> + var part1 = new AssemblyPart(typeof(object).GetTypeInfo().Assembly); + var part2 = new AssemblyPart(GetType().GetTypeInfo().Assembly); + var featureProvider = new TestableViewsFeatureProvider(new Dictionary> { { part1, @@ -74,105 +51,71 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation new[] { new RazorViewAttribute("/Areas/Admin/Views/Index.cshtml", typeof(string)), - - // This one doesn't have a RazorCompiledItem new RazorViewAttribute("/Areas/Admin/Views/About.cshtml", typeof(int)), } }, - }; + }); - var featureProvider = new TestableViewsFeatureProvider(items, attributes); - var partManager = new ApplicationPartManager(); - partManager.ApplicationParts.Add(part1); - partManager.ApplicationParts.Add(part2); - partManager.FeatureProviders.Add(featureProvider); + var applicationPartManager = new ApplicationPartManager(); + applicationPartManager.ApplicationParts.Add(part1); + applicationPartManager.ApplicationParts.Add(part2); + applicationPartManager.FeatureProviders.Add(featureProvider); var feature = new ViewsFeature(); // Act - partManager.PopulateFeature(feature); + applicationPartManager.PopulateFeature(feature); // Assert Assert.Collection(feature.ViewDescriptors.OrderBy(f => f.RelativePath, StringComparer.Ordinal), view => { - Assert.Empty(view.ExpirationTokens); - Assert.True(view.IsPrecompiled); - Assert.Null(view.Item); Assert.Equal("/Areas/Admin/Views/About.cshtml", view.RelativePath); - Assert.Equal(typeof(int), view.Type); - Assert.Equal("/Areas/Admin/Views/About.cshtml", view.ViewAttribute.Path); Assert.Equal(typeof(int), view.ViewAttribute.ViewType); }, view => { - // This one doesn't have a RazorCompiledItem - Assert.Empty(view.ExpirationTokens); - Assert.True(view.IsPrecompiled); - Assert.Equal("/Areas/Admin/Views/Index.cshtml", view.Item.Identifier); - Assert.Equal("mvc.1.0.view", view.Item.Kind); - Assert.Equal(typeof(string), view.Item.Type); Assert.Equal("/Areas/Admin/Views/Index.cshtml", view.RelativePath); - Assert.Equal(typeof(string), view.Type); - Assert.Equal("/Areas/Admin/Views/Index.cshtml", view.ViewAttribute.Path); Assert.Equal(typeof(string), view.ViewAttribute.ViewType); }, view => { - // This one doesn't have a RazorViewAttribute - Assert.Empty(view.ExpirationTokens); - Assert.True(view.IsPrecompiled); - Assert.Equal("/Views/test/About.cshtml", view.Item.Identifier); - Assert.Equal("mvc.1.0.view", view.Item.Kind); - Assert.Equal(typeof(StringBuilder), view.Item.Type); - Assert.Equal("/Views/test/About.cshtml", view.RelativePath); - Assert.Equal(typeof(StringBuilder), view.Type); - Assert.Null(view.ViewAttribute); - }, - view => - { - Assert.Empty(view.ExpirationTokens); - Assert.True(view.IsPrecompiled); - Assert.Equal("/Views/test/Index.cshtml", view.Item.Identifier); - Assert.Equal("mvc.1.0.view", view.Item.Kind); - Assert.Equal(typeof(object), view.Item.Type); Assert.Equal("/Views/test/Index.cshtml", view.RelativePath); - Assert.Equal(typeof(object), view.Type); - Assert.Equal("/Views/test/Index.cshtml", view.ViewAttribute.Path); Assert.Equal(typeof(object), view.ViewAttribute.ViewType); }); } [Fact] - public void PopulateFeature_PrefersViewsFromPartsWithHigherPrecedence() + public void PopulateFeature_ThrowsIfSingleAssemblyContainsMultipleAttributesWithTheSamePath() { // Arrange - var part1 = new AssemblyPart(typeof(ViewsFeatureProvider).Assembly); - var item1 = new TestRazorCompiledItem(typeof(StringBuilder), "mvc.1.0.view", "/Areas/Admin/Views/Shared/_Layout.cshtml", new object[] { }); - - var part2 = new AssemblyPart(GetType().Assembly); - var item2 = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Areas/Admin/Views/Shared/_Layout.cshtml", new object[] { }); - var item3 = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Areas/Admin/Views/Shared/_Partial.cshtml", new object[] { }); - - var items = new Dictionary> + var path1 = "/Views/test/Index.cshtml"; + var path2 = "/views/test/index.cshtml"; + var expected = string.Join( + Environment.NewLine, + "The following precompiled view paths differ only in case, which is not supported:", + path1, + path2); + var part = new AssemblyPart(typeof(object).GetTypeInfo().Assembly); + var featureProvider = new TestableViewsFeatureProvider(new Dictionary> { - { part1, new[] { item1 } }, - { part2, new[] { item2, item3, } }, - }; + { + part, + new[] + { + new RazorViewAttribute(path1, typeof(object)), + new RazorViewAttribute(path2, typeof(object)), + } + }, + }); - var featureProvider = new TestableViewsFeatureProvider(items, attributes: new Dictionary>()); - var partManager = new ApplicationPartManager(); - partManager.ApplicationParts.Add(part1); - partManager.ApplicationParts.Add(part2); - partManager.FeatureProviders.Add(featureProvider); + var applicationPartManager = new ApplicationPartManager(); + applicationPartManager.ApplicationParts.Add(part); + applicationPartManager.FeatureProviders.Add(featureProvider); var feature = new ViewsFeature(); - // Act - partManager.PopulateFeature(feature); - - // Assert - Assert.Collection(feature.ViewDescriptors.OrderBy(f => f.RelativePath, StringComparer.Ordinal), - view => Assert.Same(item1, view.Item), - view => Assert.Same(item3, view.Item)); + // Act & Assert + var ex = Assert.Throws(() => applicationPartManager.PopulateFeature(feature)); + Assert.Equal(expected, ex.Message); } [Fact] @@ -180,168 +123,58 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation { // Arrange var name = new AssemblyName($"DynamicAssembly-{Guid.NewGuid()}"); - var assembly = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndCollect); + var assembly = AssemblyBuilder.DefineDynamicAssembly(name, + AssemblyBuilderAccess.RunAndCollect); - var partManager = new ApplicationPartManager(); - partManager.ApplicationParts.Add(new AssemblyPart(assembly)); - partManager.FeatureProviders.Add(new ViewsFeatureProvider()); + var applicationPartManager = new ApplicationPartManager(); + applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly)); + applicationPartManager.FeatureProviders.Add(new ViewsFeatureProvider()); var feature = new ViewsFeature(); // Act - partManager.PopulateFeature(feature); + applicationPartManager.PopulateFeature(feature); // Assert Assert.Empty(feature.ViewDescriptors); } [Fact] - public void PopulateFeature_ReadsAttributesFromTheCurrentAssembly() - { - // Arrange - var item1 = new RazorCompiledItemAttribute(typeof(string), "mvc.1.0.view", "view"); - var assembly = new AssemblyWithEmptyLocation( - new RazorViewAttribute[] { new RazorViewAttribute("view", typeof(string)) }, - new RazorCompiledItemAttribute[] { item1 }); - - var partManager = new ApplicationPartManager(); - partManager.ApplicationParts.Add(new AssemblyPart(assembly)); - partManager.FeatureProviders.Add(new ViewsFeatureProvider()); - var feature = new ViewsFeature(); - - // Act - partManager.PopulateFeature(feature); - - // Assert - var descriptor = Assert.Single(feature.ViewDescriptors); - Assert.Equal(typeof(string), descriptor.Item.Type); - Assert.Equal("mvc.1.0.view", descriptor.Item.Kind); - Assert.Equal("view", descriptor.Item.Identifier); - } - - [Fact] - public void PopulateFeature_LegacyBehaviorDoesNotFail_IfAssemblyHasEmptyLocation() + public void PopulateFeature_DoesNotFail_IfAssemblyHasEmptyLocation() { // Arrange var assembly = new AssemblyWithEmptyLocation(); - var partManager = new ApplicationPartManager(); - partManager.ApplicationParts.Add(new AssemblyPart(assembly)); - partManager.FeatureProviders.Add(new OverrideViewsFeatureProvider()); + var applicationPartManager = new ApplicationPartManager(); + applicationPartManager.ApplicationParts.Add(new AssemblyPart(assembly)); + applicationPartManager.FeatureProviders.Add(new ViewsFeatureProvider()); var feature = new ViewsFeature(); // Act - partManager.PopulateFeature(feature); + applicationPartManager.PopulateFeature(feature); // Assert Assert.Empty(feature.ViewDescriptors); } - [Fact] - public void PopulateFeature_PreservesOldBehavior_IfGetViewAttributesWasOverriden() - { - // Arrange - var assembly = new AssemblyWithEmptyLocation( - new RazorViewAttribute[] { new RazorViewAttribute("view", typeof(string)) }, - new RazorCompiledItemAttribute[] { }); - - var partManager = new ApplicationPartManager(); - partManager.ApplicationParts.Add(new AssemblyPart(assembly)); - partManager.FeatureProviders.Add(new OverrideViewsFeatureProvider()); - var feature = new ViewsFeature(); - - // Act - partManager.PopulateFeature(feature); - - // Assert - Assert.Empty(feature.ViewDescriptors); - } - - internal class OverrideViewsFeatureProvider : ViewsFeatureProvider - { - protected override IEnumerable GetViewAttributes(AssemblyPart assemblyPart) - => base.GetViewAttributes(assemblyPart); - } - - private class TestRazorCompiledItem : RazorCompiledItem - { - public TestRazorCompiledItem(Type type, string kind, string identifier, object[] metadata) - { - Type = type; - Kind = kind; - Identifier = identifier; - Metadata = metadata; - } - - public override string Identifier { get; } - - public override string Kind { get; } - - public override IReadOnlyList Metadata { get; } - - public override Type Type { get; } - } - private class TestableViewsFeatureProvider : ViewsFeatureProvider { - private readonly Dictionary> _attributes; - private readonly Dictionary> _items; + private readonly Dictionary> _attributeLookup; - public TestableViewsFeatureProvider( - Dictionary> items, - Dictionary> attributes) + public TestableViewsFeatureProvider(Dictionary> attributeLookup) { - _items = items; - _attributes = attributes; + _attributeLookup = attributeLookup; } protected override IEnumerable GetViewAttributes(AssemblyPart assemblyPart) { - if (_attributes.TryGetValue(assemblyPart, out var attributes)) - { - return attributes; - } - - return Enumerable.Empty(); - } - - internal override IReadOnlyList LoadItems(AssemblyPart assemblyPart) - { - return _items[assemblyPart]; + return _attributeLookup[assemblyPart]; } } private class AssemblyWithEmptyLocation : Assembly { - private readonly RazorViewAttribute[] _razorViewAttributes; - private readonly RazorCompiledItemAttribute[] _razorCompiledItemAttributes; - - public AssemblyWithEmptyLocation() - : this(Array.Empty(), Array.Empty()) - { - } - - public AssemblyWithEmptyLocation( - RazorViewAttribute[] razorViewAttributes, - RazorCompiledItemAttribute[] razorCompiledItemAttributes) - { - _razorViewAttributes = razorViewAttributes; - _razorCompiledItemAttributes = razorCompiledItemAttributes; - } - public override string Location => string.Empty; - public override string FullName => typeof(ViewsFeatureProviderTest).Assembly.FullName; - - public override object[] GetCustomAttributes(Type attributeType, bool inherit) - { - if (attributeType == typeof(RazorViewAttribute)) - { - return _razorViewAttributes; - } - else - { - return _razorCompiledItemAttributes; - } - } + public override string FullName => typeof(ViewsFeatureProviderTest).GetTypeInfo().Assembly.FullName; public override IEnumerable DefinedTypes { @@ -360,4 +193,5 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation } } } +#pragma warning restore CS0618 // Type or member is obsolete } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs index 36dc4530ad..c4df0280aa 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/RazorViewCompilerTest.cs @@ -23,27 +23,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal { public class RazorViewCompilerTest { - [Fact] - public void Constructor_ThrowsIfMultiplePrecompiledViewsHavePathsDifferingOnlyInCase() - { - // Arrange - var fileProvider = new TestFileProvider(); - var precompiledViews = new[] - { - new CompiledViewDescriptor { RelativePath = "/Views/Home/About.cshtml" }, - new CompiledViewDescriptor { RelativePath = "/Views/home/About.cshtml" }, - }; - var message = string.Join( - Environment.NewLine, - "The following precompiled view paths differ only in case, which is not supported:", - precompiledViews[0].RelativePath, - precompiledViews[1].RelativePath); - - // Act & Assert - var ex = Assert.Throws(() => GetViewCompiler(fileProvider, precompiledViews: precompiledViews)); - Assert.Equal(message, ex.Message); - } - [Fact] public async Task CompileAsync_ReturnsResultWithNullAttribute_IfFileIsNotFoundInFileSystem() { diff --git a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs index 61ce320702..d17a0c701f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Test/MvcServiceCollectionExtensionsTest.cs @@ -216,7 +216,10 @@ namespace Microsoft.AspNetCore.Mvc feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), + feature => Assert.IsType(feature), +#pragma warning disable CS0618 // Type or member is obsolete feature => Assert.IsType(feature)); +#pragma warning restore CS0618 // Type or member is obsolete } [Fact] diff --git a/test/WebSites/RazorBuildWebSite.PrecompiledViews/Views/Common/CommonView.cs b/test/WebSites/RazorBuildWebSite.PrecompiledViews/Views/Common/CommonView.cs new file mode 100644 index 0000000000..429418e2d5 --- /dev/null +++ b/test/WebSites/RazorBuildWebSite.PrecompiledViews/Views/Common/CommonView.cs @@ -0,0 +1,37 @@ +#pragma checksum "D:\k\Mvc\test\WebSites\RazorBuildWebSite\Views\Common\CommonView.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "a09a0106df2e63aecf6fc6ddf30df39b489d9783" +// +#pragma warning disable 1591 +[assembly: global::Microsoft.AspNetCore.Mvc.Razor.Compilation.RazorViewAttribute(@"/Views/Common/CommonView.cshtml", typeof(RazorBuildWebSite.Views.Precompilation._Views_Precompilation_CommonView))] +namespace RazorBuildWebSite.Views.Precompilation +{ +#line hidden + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Rendering; + using Microsoft.AspNetCore.Mvc.ViewFeatures; + public class _Views_Precompilation_CommonView : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage + { +#pragma warning disable 1998 + public async override global::System.Threading.Tasks.Task ExecuteAsync() + { + BeginContext(0, 48, true); + WriteLiteral("Hello from buildtime-compiled precompilation view!"); + EndContext(); + } +#pragma warning restore 1998 + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } + } +} +#pragma warning restore 1591 diff --git a/test/WebSites/RazorBuildWebSite.Views/AssemblyInfo.cs b/test/WebSites/RazorBuildWebSite.Views/AssemblyInfo.cs new file mode 100644 index 0000000000..f5a842f0db --- /dev/null +++ b/test/WebSites/RazorBuildWebSite.Views/AssemblyInfo.cs @@ -0,0 +1,11 @@ +//------------------------------------------------------------------------------ +// +// Generated by the MSBuild WriteCodeFragment class. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: Microsoft.AspNetCore.Mvc.ApplicationParts.ProvideApplicationPartFactoryAttribute("Microsoft.AspNetCore.Mvc.ApplicationParts.CompiledRazorAssemblyApplicationPartFac" + +"tory, Microsoft.AspNetCore.Mvc.Razor")] diff --git a/test/WebSites/RazorBuildWebSite.Views/Views/Common/CommonView.cs b/test/WebSites/RazorBuildWebSite.Views/Views/Common/CommonView.cs new file mode 100644 index 0000000000..c94a379958 --- /dev/null +++ b/test/WebSites/RazorBuildWebSite.Views/Views/Common/CommonView.cs @@ -0,0 +1,40 @@ +#pragma checksum "D:\k\Mvc\test\WebSites\RazorBuildWebSite\Views\Common\CommonView.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "56260f24a80ae2b8854fd2c9a1005bae485882cc" +// +#pragma warning disable 1591 +[assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(RazorBuildWebSite.Views.Rzc.Views_Rzc_View), @"mvc.1.0.view", @"/Views/Common/CommonView.cshtml")] +[assembly:global::Microsoft.AspNetCore.Mvc.Razor.Compilation.RazorViewAttribute(@"/Views/CommonView.cshtml", typeof(RazorBuildWebSite.Views.Rzc.Views_Rzc_View))] +namespace RazorBuildWebSite.Views.Rzc +{ + #line hidden + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Mvc; + using Microsoft.AspNetCore.Mvc.Rendering; + using Microsoft.AspNetCore.Mvc.ViewFeatures; + [global::Microsoft.AspNetCore.Razor.Hosting.RazorSourceChecksumAttribute(@"SHA1", @"56260f24a80ae2b8854fd2c9a1005bae485882cc", @"/Views/Common/CommonView.cshtml")] + [global::Microsoft.AspNetCore.Razor.Hosting.RazorSourceChecksumAttribute(@"SHA1", @"f265f06036e4378eada2a78f5366ad0e13e1d8af", @"/_ViewImports.cshtml")] + internal class Views_Rzc_CommonView : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage + { + #pragma warning disable 1998 + public async override global::System.Threading.Tasks.Task ExecuteAsync() + { + BeginContext(0, 39, true); + WriteLiteral("Hello from buildtime-compiled rzc view!"); + EndContext(); + } + #pragma warning restore 1998 + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } + } +} +#pragma warning restore 1591 diff --git a/test/WebSites/RazorBuildWebSite/Controllers/CommonController.cs b/test/WebSites/RazorBuildWebSite/Controllers/CommonController.cs new file mode 100644 index 0000000000..4659d40966 --- /dev/null +++ b/test/WebSites/RazorBuildWebSite/Controllers/CommonController.cs @@ -0,0 +1,13 @@ + +using Microsoft.AspNetCore.Mvc; + +namespace RazorBuildWebSite.Controllers +{ + public class CommonController : Controller + { + public new ActionResult View() + { + return base.View("CommonView"); + } + } +}