[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:
parent
c6c77dd4d3
commit
1d6b02c1f5
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue