diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartManager.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartManager.cs index 4d0c190931..95a893037f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartManager.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/ApplicationPartManager.cs @@ -19,10 +19,15 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts new List(); /// - /// Gets the list of s. + /// Gets the list of instances. + /// + /// Instances in this collection are stored in precedence order. An that appears + /// earlier in the list has a higher precendence. + /// An may choose to use this an interface as a way to resolve conflicts when + /// multiple instances resolve equivalent feature values. + /// /// - public IList ApplicationParts { get; } = - new List(); + public IList ApplicationParts { get; } = new List(); /// /// Populates the given using the list of diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs index c48c37d6e7..e3df2d6a14 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/AssemblyPart.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.DependencyModel; namespace Microsoft.AspNetCore.Mvc.ApplicationParts { /// - /// An backed by an . + /// An backed by an . /// public class AssemblyPart : ApplicationPart, @@ -20,15 +20,10 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts /// /// Initializes a new instance. /// - /// + /// The backing . public AssemblyPart(Assembly assembly) { - if (assembly == null) - { - throw new ArgumentNullException(nameof(assembly)); - } - - Assembly = assembly; + Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IApplicationFeatureProviderOfT.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IApplicationFeatureProviderOfT.cs index f1716df485..cf7f870bca 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IApplicationFeatureProviderOfT.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationParts/IApplicationFeatureProviderOfT.cs @@ -14,10 +14,14 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationParts /// /// Updates the instance. /// - /// The list of s of the - /// application. + /// The list of instances in the application. /// /// The feature instance to populate. + /// + /// instances in appear in the same ordered sequence they + /// are stored in . This ordering may be used by the feature + /// provider to make precedence decisions. + /// void PopulateFeature(IEnumerable parts, TFeature feature); } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 86c4a1968c..b5a4dc8682 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -81,12 +81,17 @@ namespace Microsoft.Extensions.DependencyInjection manager = new ApplicationPartManager(); var environment = GetServiceFromCollection(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); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs index ade3da33e3..439bee7f2b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Compilation/ViewsFeatureProvider.cs @@ -27,15 +27,23 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Compilation /// public void PopulateFeature(IEnumerable parts, ViewsFeature feature) { + var knownIdentifiers = new HashSet(StringComparer.OrdinalIgnoreCase); + var descriptors = new List(); foreach (var assemblyPart in parts.OfType()) { 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); + } } } } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs index 9859961bc7..0e126fa5f5 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/BasicTests.cs @@ -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>(response); + var expected = new[] + { + "BasicWebSite", + "Microsoft.AspNetCore.Mvc.TagHelpers", + "Microsoft.AspNetCore.Mvc.Razor", + }; + + // Assert + Assert.Equal(expected, assemblyParts); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs index 7493f608d1..0430653869 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Compilation/ViewsFeatureProviderTest.cs @@ -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> { @@ -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> + { + { part1, new[] { item1 } }, + { part2, new[] { item2, item3, } }, + }; + + var featureProvider = new TestableViewsFeatureProvider(items, attributes: new Dictionary>()); + 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 GetViewAttributes(AssemblyPart assemblyPart) { - return _attributes[assemblyPart]; + if (_attributes.TryGetValue(assemblyPart, out var attributes)) + { + return attributes; + } + + return Enumerable.Empty(); } protected override IReadOnlyList 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 DefinedTypes { diff --git a/test/WebSites/BasicWebSite/Controllers/HomeController.cs b/test/WebSites/BasicWebSite/Controllers/HomeController.cs index 0730aa55b6..a68df9b313 100644 --- a/test/WebSites/BasicWebSite/Controllers/HomeController.cs +++ b/test/WebSites/BasicWebSite/Controllers/HomeController.cs @@ -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); + } } -} \ No newline at end of file +}