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