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

View File

@ -14,10 +14,14 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts
/// <summary>
/// Updates the <paramref name="feature"/> instance.
/// </summary>
/// <param name="parts">The list of <see cref="ApplicationPart"/>s of the
/// application.
/// <param name="parts">The list of <see cref="ApplicationPart"/> instances in the application.
/// </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);
}
}

View File

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

View File

@ -27,15 +27,23 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
/// <inheritdoc />
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>())
{
var attributes = GetViewAttributes(assemblyPart);
var items = LoadItems(assemblyPart);
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.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
@ -489,5 +490,22 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(422, (int)response.StatusCode);
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
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());
var feature = new ViewsFeature();
@ -35,8 +35,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
public void PopulateFeature_ReturnsViewsFromAllAvailableApplicationParts()
{
// Arrange
var part1 = new AssemblyPart(typeof(object).GetTypeInfo().Assembly);
var part2 = new AssemblyPart(GetType().GetTypeInfo().Assembly);
var part1 = new AssemblyPart(typeof(object).Assembly);
var part2 = new AssemblyPart(GetType().Assembly);
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]
public void PopulateFeature_ReturnsEmptySequenceIfNoDynamicAssemblyPartHasViewAssembly()
{
@ -160,7 +193,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
Assert.Empty(feature.ViewDescriptors);
}
[Fact]
public void PopulateFeature_DoesNotFail_IfAssemblyHasEmptyLocation()
{
@ -212,7 +244,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
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)
@ -225,7 +262,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation
{
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
{

View File

@ -2,10 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BasicWebSite.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Newtonsoft.Json.Serialization;
namespace BasicWebSite.Controllers
@ -120,5 +122,16 @@ namespace BasicWebSite.Controllers
{
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);
}
}
}
}