diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AdditionalAssemblyPart.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AdditionalAssemblyPart.cs new file mode 100644 index 0000000000..a900e1b042 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AdditionalAssemblyPart.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +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 + { + /// + public AdditionalAssemblyPart(Assembly assembly) : base(assembly) + { + } + + IEnumerable ICompilationReferencesProvider.GetReferencePaths() => Array.Empty(); + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index b5a4dc8682..aed047c9ea 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; } - // Parts appear in the ApplicationParts collection in precedence order. The part that represents the - // current application appears first, followed by all other parts sorted by name. - var parts = DefaultAssemblyPartDiscoveryProvider.DiscoverAssemblyParts(entryAssemblyName) - .OrderBy(part => string.Equals(entryAssemblyName, part.Name, StringComparison.Ordinal) ? 0 : 1) - .ThenBy(part => part.Name, StringComparer.Ordinal); + var parts = DefaultAssemblyPartDiscoveryProvider.DiscoverAssemblyParts(entryAssemblyName); foreach (var part in parts) { manager.ApplicationParts.Add(part); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultAssemblyPartDiscoveryProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultAssemblyPartDiscoveryProvider.cs index c7cdeb3d68..645f1d6731 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultAssemblyPartDiscoveryProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultAssemblyPartDiscoveryProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc.ApplicationParts; @@ -14,6 +15,16 @@ 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", @@ -33,12 +44,219 @@ namespace Microsoft.AspNetCore.Mvc.Internal "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); - return GetCandidateAssemblies(entryAssembly, context).Select(p => new AssemblyPart(p)); + 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) diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs index 439bee7f2b..ebbcc9239f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs @@ -84,14 +84,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation return dictionary.Values; } - protected virtual IReadOnlyList LoadItems(AssemblyPart assemblyPart) + internal virtual IReadOnlyList LoadItems(AssemblyPart assemblyPart) { if (assemblyPart == null) { throw new ArgumentNullException(nameof(assemblyPart)); } - var viewAssembly = GetViewAssembly(assemblyPart); + var viewAssembly = assemblyPart.Assembly; if (viewAssembly != null) { var loader = new RazorCompiledItemLoader(); @@ -107,6 +107,29 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation /// 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) { @@ -149,5 +172,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation 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(); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultAssemblyPartDiscoveryProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultAssemblyPartDiscoveryProviderTest.cs index f9603dc03f..ee5a6a5c69 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultAssemblyPartDiscoveryProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultAssemblyPartDiscoveryProviderTest.cs @@ -2,8 +2,11 @@ // 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; @@ -64,6 +67,170 @@ namespace Microsoft.AspNetCore.Mvc.Internal 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() { @@ -283,5 +450,52 @@ namespace Microsoft.AspNetCore.Mvc.Internal 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.Razor.Test/Compilation/ViewsFeatureProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs index 0430653869..02b64f977c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs @@ -9,6 +9,7 @@ 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 @@ -194,10 +195,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation } [Fact] - public void PopulateFeature_DoesNotFail_IfAssemblyHasEmptyLocation() + public void PopulateFeature_ReadsAttributesFromTheCurrentAssembly() { // Arrange - var assembly = new AssemblyWithEmptyLocation(); + 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()); @@ -206,10 +211,56 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation // 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() + { + // Arrange + var assembly = new AssemblyWithEmptyLocation(); + 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); } + [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) @@ -252,7 +303,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation return Enumerable.Empty(); } - protected override IReadOnlyList LoadItems(AssemblyPart assemblyPart) + internal override IReadOnlyList LoadItems(AssemblyPart assemblyPart) { return _items[assemblyPart]; } @@ -260,10 +311,38 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation 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 IEnumerable DefinedTypes { get