[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.
This commit is contained in:
Javier Calvarro Nelson 2018-01-17 14:27:45 -08:00
parent c6c77dd4d3
commit 1d6b02c1f5
6 changed files with 580 additions and 11 deletions

View File

@ -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
{
/// <summary>
/// An <see cref="AssemblyPart"/> that was added by an assembly that referenced it through the use
/// of an assembly metadata attribute.
/// </summary>
public class AdditionalAssemblyPart : AssemblyPart, ICompilationReferencesProvider
{
/// <inheritdoc />
public AdditionalAssemblyPart(Assembly assembly) : base(assembly)
{
}
IEnumerable<string> ICompilationReferencesProvider.GetReferencePaths() => Array.Empty<string>();
}
}

View File

@ -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);

View File

@ -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<string> ViewAssemblySuffixes = new string[]
{
PrecompiledViewsAssemblySuffix,
".Views",
};
private const string AdditionalReferenceKey = "Microsoft.AspNetCore.Mvc.AdditionalReference";
private static readonly char[] MetadataSeparators = new[] { ',' };
internal static HashSet<string> ReferenceAssemblies { get; } = new HashSet<string>(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<string, Assembly> AssemblyLoader { get; set; } = Assembly.LoadFile;
internal static Func<string, bool> AssemblyResolver { get; set; } = File.Exists;
public static IEnumerable<ApplicationPart> 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<Assembly>(
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<AssemblyPart> 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<Assembly> candidateAssemblies)
{
var additionalAssemblyReferences = candidateAssemblies
.Select(ca =>
(assembly: ca,
metadata: ca.GetCustomAttributes<AssemblyMetadataAttribute>()
.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<Assembly>(FullNameAssemblyComparer.Instance);
var entryAssemblyAdditionalReferences = new SortedSet<Assembly>(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<Assembly> additionalReferences,
SortedSet<Assembly> 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<Assembly> AdditionalAssemblies { get; set; }
public SortedSet<Assembly> EntryAssemblyAdditionalAssemblies { get; set; }
public void Deconstruct(
out SortedSet<Assembly> additionalAssemblies,
out SortedSet<Assembly> entryAssemblyAdditionalAssemblies)
{
additionalAssemblies = AdditionalAssemblies;
entryAssemblyAdditionalAssemblies = EntryAssemblyAdditionalAssemblies;
}
}
internal class FullNameAssemblyComparer : IComparer<Assembly>
{
public static IComparer<Assembly> Instance { get; } = new FullNameAssemblyComparer();
public int Compare(Assembly x, Assembly y) =>
string.Compare(x?.FullName, y?.FullName, StringComparison.Ordinal);
}
internal static IEnumerable<Assembly> GetCandidateAssemblies(Assembly entryAssembly, DependencyContext dependencyContext)

View File

@ -84,14 +84,14 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
return dictionary.Values;
}
protected virtual IReadOnlyList<RazorCompiledItem> LoadItems(AssemblyPart assemblyPart)
internal virtual IReadOnlyList<RazorCompiledItem> 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
/// <param name="assemblyPart">The <see cref="AssemblyPart"/>.</param>
/// <returns>The sequence of <see cref="RazorViewAttribute"/> instances.</returns>
protected virtual IEnumerable<RazorViewAttribute> 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<RazorViewAttribute> GetViewAttributesLegacy(AssemblyPart assemblyPart)
{
if (assemblyPart == null)
{
@ -149,5 +172,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
return null;
}
private static IEnumerable<RazorViewAttribute> GetViewAttributesFromCurrentAssembly(AssemblyPart assemblyPart)
{
if (assemblyPart == null)
{
throw new ArgumentNullException(nameof(assemblyPart));
}
var featureAssembly = assemblyPart.Assembly;
if (featureAssembly != null)
{
return featureAssembly.GetCustomAttributes<RazorViewAttribute>();
}
return Enumerable.Empty<RazorViewAttribute>();
}
}
}

View File

@ -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<Assembly> CandidateAssemblies { get; set; }
public IDictionary<string, Assembly> AssemblyResolver { get; set; }
public string[] ExpectedAdditionalReferences { get; set; }
public string[] ExpectedEntryAssemblyAdditionalReferences { get; set; }
}
public static TheoryData<ResolveAdditionalReferencesTestData> ResolveAdditionalReferencesData
{
get
{
var data = new TheoryData<ResolveAdditionalReferencesTestData>();
var noCandidates = Array.Empty<Assembly>();
var noResolvable = new Dictionary<string, Assembly>();
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<string>(),
ExpectedEntryAssemblyAdditionalReferences = Array.Empty<string>()
};
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<string, Assembly> { [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<string, Assembly> { [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<string, Assembly> { [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<string, Assembly> { [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<string, Assembly> {
[ePath] = ePrecompiledViews,
[fPath] = fPrecompiledViews,
[gPath] = gPrecompiledViews
},
ExpectedAdditionalReferences = new[] { ePrecompiledViews.FullName, fPrecompiledViews.FullName },
ExpectedEntryAssemblyAdditionalReferences = new[] {
gPrecompiledViews.FullName
}
};
data.Add(multipleAssembliesNewPrecompilationInGraph);
return data;
}
}
private static SortedSet<Assembly> CreateCandidates(params Assembly[] assemblies) =>
new SortedSet<Assembly>(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);
}
}
}

View File

@ -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<RazorViewAttribute> 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<RazorViewAttribute>();
}
protected override IReadOnlyList<RazorCompiledItem> LoadItems(AssemblyPart assemblyPart)
internal override IReadOnlyList<RazorCompiledItem> 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<RazorViewAttribute>(), Array.Empty<RazorCompiledItemAttribute>())
{
}
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<TypeInfo> DefinedTypes
{
get