Support conflict resolution when multiple precompiled views have the same path
Fixes #7223
This commit is contained in:
parent
946b64143e
commit
ab3134e373
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue