Support conflict resolution when multiple precompiled views have the same path

Fixes #7223
This commit is contained in:
Pranav K 2018-01-10 09:25:38 -08:00
parent 946b64143e
commit ab3134e373
8 changed files with 109 additions and 24 deletions

View File

@ -19,10 +19,15 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts
new List<IApplicationFeatureProvider>(); new List<IApplicationFeatureProvider>();
/// <summary> /// <summary>
/// Gets the list of <see cref="ApplicationPart"/>s. /// Gets the list of <see cref="ApplicationPart"/> instances.
/// <para>
/// Instances in this collection are stored in precedence order. An <see cref="ApplicationPart"/> that appears
/// earlier in the list has a higher precendence.
/// An <see cref="IApplicationFeatureProvider"/> may choose to use this an interface as a way to resolve conflicts when
/// multiple <see cref="ApplicationPart"/> instances resolve equivalent feature values.
/// </para>
/// </summary> /// </summary>
public IList<ApplicationPart> ApplicationParts { get; } = public IList<ApplicationPart> ApplicationParts { get; } = new List<ApplicationPart>();
new List<ApplicationPart>();
/// <summary> /// <summary>
/// Populates the given <paramref name="feature"/> using the list of /// Populates the given <paramref name="feature"/> using the list of

View File

@ -10,7 +10,7 @@ using Microsoft.Extensions.DependencyModel;
namespace Microsoft.AspNetCore.Mvc.ApplicationParts namespace Microsoft.AspNetCore.Mvc.ApplicationParts
{ {
/// <summary> /// <summary>
/// An <see cref="ApplicationPart"/> backed by an <see cref="Assembly"/>. /// An <see cref="ApplicationPart"/> backed by an <see cref="System.Reflection.Assembly"/>.
/// </summary> /// </summary>
public class AssemblyPart : public class AssemblyPart :
ApplicationPart, ApplicationPart,
@ -20,15 +20,10 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts
/// <summary> /// <summary>
/// Initializes a new <see cref="AssemblyPart"/> instance. /// Initializes a new <see cref="AssemblyPart"/> instance.
/// </summary> /// </summary>
/// <param name="assembly"></param> /// <param name="assembly">The backing <see cref="System.Reflection.Assembly"/>.</param>
public AssemblyPart(Assembly assembly) public AssemblyPart(Assembly assembly)
{ {
if (assembly == null) Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
{
throw new ArgumentNullException(nameof(assembly));
}
Assembly = assembly;
} }
/// <summary> /// <summary>

View File

@ -14,10 +14,14 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts
/// <summary> /// <summary>
/// Updates the <paramref name="feature"/> instance. /// Updates the <paramref name="feature"/> instance.
/// </summary> /// </summary>
/// <param name="parts">The list of <see cref="ApplicationPart"/>s of the /// <param name="parts">The list of <see cref="ApplicationPart"/> instances in the application.
/// application.
/// </param> /// </param>
/// <param name="feature">The feature instance to populate.</param> /// <param name="feature">The feature instance to populate.</param>
/// <remarks>
/// <see cref="ApplicationPart"/> instances in <paramref name="parts"/> appear in the same ordered sequence they
/// are stored in <see cref="ApplicationPartManager.ApplicationParts"/>. This ordering may be used by the feature
/// provider to make precedence decisions.
/// </remarks>
void PopulateFeature(IEnumerable<ApplicationPart> parts, TFeature feature); void PopulateFeature(IEnumerable<ApplicationPart> parts, TFeature feature);
} }
} }

View File

@ -81,12 +81,17 @@ namespace Microsoft.Extensions.DependencyInjection
manager = new ApplicationPartManager(); manager = new ApplicationPartManager();
var environment = GetServiceFromCollection<IHostingEnvironment>(services); var environment = GetServiceFromCollection<IHostingEnvironment>(services);
if (string.IsNullOrEmpty(environment?.ApplicationName)) var entryAssemblyName = environment?.ApplicationName;
if (string.IsNullOrEmpty(entryAssemblyName))
{ {
return manager; return manager;
} }
var parts = DefaultAssemblyPartDiscoveryProvider.DiscoverAssemblyParts(environment.ApplicationName); // 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);
foreach (var part in parts) foreach (var part in parts)
{ {
manager.ApplicationParts.Add(part); manager.ApplicationParts.Add(part);

View File

@ -27,15 +27,23 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
/// <inheritdoc /> /// <inheritdoc />
public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewsFeature feature) public void PopulateFeature(IEnumerable<ApplicationPart> parts, ViewsFeature feature)
{ {
var knownIdentifiers = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var descriptors = new List<CompiledViewDescriptor>();
foreach (var assemblyPart in parts.OfType<AssemblyPart>()) foreach (var assemblyPart in parts.OfType<AssemblyPart>())
{ {
var attributes = GetViewAttributes(assemblyPart); var attributes = GetViewAttributes(assemblyPart);
var items = LoadItems(assemblyPart); var items = LoadItems(assemblyPart);
var merged = Merge(items, attributes); var merged = Merge(items, attributes);
foreach (var entry in merged) foreach (var item in merged)
{ {
feature.ViewDescriptors.Add(new CompiledViewDescriptor(entry.item, entry.attribute)); var descriptor = new CompiledViewDescriptor(item.item, item.attribute);
// We iterate through ApplicationPart instances appear in precendence order.
// If a view path appears in multiple views, we'll use the order to break ties.
if (knownIdentifiers.Add(descriptor.RelativePath))
{
feature.ViewDescriptors.Add(descriptor);
}
} }
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
@ -489,5 +490,22 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(422, (int)response.StatusCode); Assert.Equal(422, (int)response.StatusCode);
Assert.Equal("Can't process this!", await response.Content.ReadAsStringAsync()); Assert.Equal("Can't process this!", await response.Content.ReadAsStringAsync());
} }
[Fact]
public async Task ApplicationAssemblyPartIsListedAsFirstAssembly()
{
// Act
var response = await Client.GetStringAsync("Home/GetAssemblyPartData");
var assemblyParts = JsonConvert.DeserializeObject<IList<string>>(response);
var expected = new[]
{
"BasicWebSite",
"Microsoft.AspNetCore.Mvc.TagHelpers",
"Microsoft.AspNetCore.Mvc.Razor",
};
// Assert
Assert.Equal(expected, assemblyParts);
}
} }
} }

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
{ {
// Arrange // Arrange
var partManager = new ApplicationPartManager(); var partManager = new ApplicationPartManager();
partManager.ApplicationParts.Add(new AssemblyPart(typeof(ViewsFeatureProviderTest).GetTypeInfo().Assembly)); partManager.ApplicationParts.Add(new AssemblyPart(typeof(ViewsFeatureProviderTest).Assembly));
partManager.FeatureProviders.Add(new ViewsFeatureProvider()); partManager.FeatureProviders.Add(new ViewsFeatureProvider());
var feature = new ViewsFeature(); var feature = new ViewsFeature();
@ -35,8 +35,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
public void PopulateFeature_ReturnsViewsFromAllAvailableApplicationParts() public void PopulateFeature_ReturnsViewsFromAllAvailableApplicationParts()
{ {
// Arrange // Arrange
var part1 = new AssemblyPart(typeof(object).GetTypeInfo().Assembly); var part1 = new AssemblyPart(typeof(object).Assembly);
var part2 = new AssemblyPart(GetType().GetTypeInfo().Assembly); var part2 = new AssemblyPart(GetType().Assembly);
var items = new Dictionary<AssemblyPart, IReadOnlyList<RazorCompiledItem>> var items = new Dictionary<AssemblyPart, IReadOnlyList<RazorCompiledItem>>
{ {
@ -141,6 +141,39 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
}); });
} }
[Fact]
public void PopulateFeature_PrefersViewsFromPartsWithHigherPrecedence()
{
// Arrange
var part1 = new AssemblyPart(typeof(ViewsFeatureProvider).Assembly);
var item1 = new TestRazorCompiledItem(typeof(StringBuilder), "mvc.1.0.view", "/Areas/Admin/Views/Shared/_Layout.cshtml", new object[] { });
var part2 = new AssemblyPart(GetType().Assembly);
var item2 = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Areas/Admin/Views/Shared/_Layout.cshtml", new object[] { });
var item3 = new TestRazorCompiledItem(typeof(string), "mvc.1.0.view", "/Areas/Admin/Views/Shared/_Partial.cshtml", new object[] { });
var items = new Dictionary<AssemblyPart, IReadOnlyList<RazorCompiledItem>>
{
{ part1, new[] { item1 } },
{ part2, new[] { item2, item3, } },
};
var featureProvider = new TestableViewsFeatureProvider(items, attributes: new Dictionary<AssemblyPart, IEnumerable<RazorViewAttribute>>());
var partManager = new ApplicationPartManager();
partManager.ApplicationParts.Add(part1);
partManager.ApplicationParts.Add(part2);
partManager.FeatureProviders.Add(featureProvider);
var feature = new ViewsFeature();
// Act
partManager.PopulateFeature(feature);
// Assert
Assert.Collection(feature.ViewDescriptors.OrderBy(f => f.RelativePath, StringComparer.Ordinal),
view => Assert.Same(item1, view.Item),
view => Assert.Same(item3, view.Item));
}
[Fact] [Fact]
public void PopulateFeature_ReturnsEmptySequenceIfNoDynamicAssemblyPartHasViewAssembly() public void PopulateFeature_ReturnsEmptySequenceIfNoDynamicAssemblyPartHasViewAssembly()
{ {
@ -160,7 +193,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
Assert.Empty(feature.ViewDescriptors); Assert.Empty(feature.ViewDescriptors);
} }
[Fact] [Fact]
public void PopulateFeature_DoesNotFail_IfAssemblyHasEmptyLocation() public void PopulateFeature_DoesNotFail_IfAssemblyHasEmptyLocation()
{ {
@ -212,7 +244,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
protected override IEnumerable<RazorViewAttribute> GetViewAttributes(AssemblyPart assemblyPart) protected override IEnumerable<RazorViewAttribute> GetViewAttributes(AssemblyPart assemblyPart)
{ {
return _attributes[assemblyPart]; if (_attributes.TryGetValue(assemblyPart, out var attributes))
{
return attributes;
}
return Enumerable.Empty<RazorViewAttribute>();
} }
protected override IReadOnlyList<RazorCompiledItem> LoadItems(AssemblyPart assemblyPart) protected override IReadOnlyList<RazorCompiledItem> LoadItems(AssemblyPart assemblyPart)
@ -225,7 +262,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
{ {
public override string Location => string.Empty; public override string Location => string.Empty;
public override string FullName => typeof(ViewsFeatureProviderTest).GetTypeInfo().Assembly.FullName; public override string FullName => typeof(ViewsFeatureProviderTest).Assembly.FullName;
public override IEnumerable<TypeInfo> DefinedTypes public override IEnumerable<TypeInfo> DefinedTypes
{ {

View File

@ -2,10 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BasicWebSite.Models; using BasicWebSite.Models;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
namespace BasicWebSite.Controllers namespace BasicWebSite.Controllers
@ -120,5 +122,16 @@ namespace BasicWebSite.Controllers
{ {
return RedirectToAction(); return RedirectToAction();
} }
[HttpGet]
public IActionResult GetAssemblyPartData([FromServices] ApplicationPartManager applicationPartManager)
{
// Ensures that the entry assembly part is marked correctly.
var assemblyPartMetadata = applicationPartManager.ApplicationParts
.Select(part => part.Name)
.ToArray();
return Ok(assemblyPartMetadata);
}
} }
} }