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>();
|
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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue