From 1d6b02c1f56ef46bc4c8568e748747c82bbf1bb8 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Wed, 17 Jan 2018 14:27:45 -0800 Subject: [PATCH] [Fixes #7239] Add support for loading additional parts. * Support loading parts through an assembly metadata attribute with a key of Microsoft.AspNetCore.Mvc.AdditionalReference and a value that describes the additional assembly to add to the list of parts and whether or not it should be added by default. The additional reference can only contain the file name of the assembly and it must be located side by side with the assembly where the additional reference is defined. * Add an AdditionalAssemblyPart application parts to represent parts that are not part of the original application per se, like precompiled views. * Update the ViewsFeatureProvider to search for razor views in the application part directly instead of trying to load the precompiled views assembly part. --- .../AdditionalAssemblyPart.cs | 23 ++ .../MvcCoreServiceCollectionExtensions.cs | 6 +- .../DefaultAssemblyPartDiscoveryProvider.cs | 220 +++++++++++++++++- .../Compilation/ViewsFeatureProvider.cs | 43 +++- ...efaultAssemblyPartDiscoveryProviderTest.cs | 214 +++++++++++++++++ .../Compilation/ViewsFeatureProviderTest.cs | 85 ++++++- 6 files changed, 580 insertions(+), 11 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AdditionalAssemblyPart.cs 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