diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/ref/Microsoft.AspNetCore.Components.csproj index 145bf52102..3f0b80424f 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.csproj @@ -7,11 +7,13 @@ + + diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp3.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp3.0.cs index ed79b9c2ee..72adf28aa7 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp3.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp3.0.cs @@ -532,6 +532,8 @@ namespace Microsoft.AspNetCore.Components.Routing { public Router() { } [Microsoft.AspNetCore.Components.ParameterAttribute] + public System.Collections.Generic.IEnumerable AdditionalAssemblies { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Components.ParameterAttribute] public System.Reflection.Assembly AppAssembly { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] public Microsoft.AspNetCore.Components.RenderFragment Found { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs index ed79b9c2ee..72adf28aa7 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs @@ -532,6 +532,8 @@ namespace Microsoft.AspNetCore.Components.Routing { public Router() { } [Microsoft.AspNetCore.Components.ParameterAttribute] + public System.Collections.Generic.IEnumerable AdditionalAssemblies { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + [Microsoft.AspNetCore.Components.ParameterAttribute] public System.Reflection.Assembly AppAssembly { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } [Microsoft.AspNetCore.Components.ParameterAttribute] public Microsoft.AspNetCore.Components.RenderFragment Found { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj index e92ae0634c..a7ad3493c9 100644 --- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj +++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;netcoreapp3.0 @@ -16,6 +16,7 @@ + diff --git a/src/Components/Components/src/Routing/RouteTableFactory.cs b/src/Components/Components/src/Routing/RouteTableFactory.cs index e0601bc95a..fc148b708a 100644 --- a/src/Components/Components/src/Routing/RouteTableFactory.cs +++ b/src/Components/Components/src/Routing/RouteTableFactory.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Components.Routing; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Components { @@ -15,20 +16,21 @@ namespace Microsoft.AspNetCore.Components /// internal static class RouteTableFactory { - private static readonly ConcurrentDictionary Cache = - new ConcurrentDictionary(); + private static readonly ConcurrentDictionary Cache = + new ConcurrentDictionary(); public static readonly IComparer RoutePrecedence = Comparer.Create(RouteComparison); - public static RouteTable Create(Assembly appAssembly) + public static RouteTable Create(IEnumerable assemblies) { - if (Cache.TryGetValue(appAssembly, out var resolvedComponents)) + var key = new Key(assemblies.OrderBy(a => a.FullName).ToArray()); + if (Cache.TryGetValue(key, out var resolvedComponents)) { return resolvedComponents; } - var componentTypes = appAssembly.ExportedTypes.Where(t => typeof(IComponent).IsAssignableFrom(t)); + var componentTypes = key.Assemblies.SelectMany(a => a.ExportedTypes.Where(t => typeof(IComponent).IsAssignableFrom(t))); var routeTable = Create(componentTypes); - Cache.TryAdd(appAssembly, routeTable); + Cache.TryAdd(key, routeTable); return routeTable; } @@ -160,5 +162,61 @@ namespace Microsoft.AspNetCore.Components "); } } + + private readonly struct Key : IEquatable + { + public readonly Assembly[] Assemblies; + + public Key(Assembly[] assemblies) + { + Assemblies = assemblies; + } + + public override bool Equals(object obj) + { + return obj is Key other ? base.Equals(other) : false; + } + + public bool Equals(Key other) + { + if (Assemblies == null && other.Assemblies == null) + { + return true; + } + else if (Assemblies == null ^ other.Assemblies == null) + { + return false; + } + else if (Assemblies.Length != other.Assemblies.Length) + { + return false; + } + + for (var i = 0; i < Assemblies.Length; i++) + { + if (!Assemblies[i].Equals(other.Assemblies[i])) + { + return false; + } + } + + return true; + } + + public override int GetHashCode() + { + var hash = new HashCodeCombiner(); + + if (Assemblies != null) + { + for (var i = 0; i < Assemblies.Length; i++) + { + hash.Add(Assemblies[i]); + } + } + + return hash; + } + } } } diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index ef34a47957..b93db4b6ac 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Rendering; @@ -37,6 +38,12 @@ namespace Microsoft.AspNetCore.Components.Routing /// [Parameter] public Assembly AppAssembly { get; set; } + /// + /// Gets or sets a collection of additional assemblies that should be searched for components + /// that can match URIs. + /// + [Parameter] public IEnumerable AdditionalAssemblies { get; set; } + /// /// Gets or sets the content to display when no match is found for the requested route. /// @@ -64,6 +71,11 @@ namespace Microsoft.AspNetCore.Components.Routing { parameters.SetParameterProperties(this); + if (AppAssembly == null) + { + throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(AppAssembly)}."); + } + // Found content is mandatory, because even though we could use something like as a // reasonable default, if it's not declared explicitly in the template then people will have no way // to discover how to customize this (e.g., to add authorization). @@ -79,7 +91,9 @@ namespace Microsoft.AspNetCore.Components.Routing throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(NotFound)}."); } - Routes = RouteTableFactory.Create(AppAssembly); + + var assemblies = AdditionalAssemblies == null ? new[] { AppAssembly } : new[] { AppAssembly }.Concat(AdditionalAssemblies); + Routes = RouteTableFactory.Create(assemblies); Refresh(isNavigationIntercepted: false); return Task.CompletedTask; } diff --git a/src/Components/Components/test/Routing/RouteTableFactoryTests.cs b/src/Components/Components/test/Routing/RouteTableFactoryTests.cs index 8250fab16b..89bf2db1cc 100644 --- a/src/Components/Components/test/Routing/RouteTableFactoryTests.cs +++ b/src/Components/Components/test/Routing/RouteTableFactoryTests.cs @@ -11,6 +11,45 @@ namespace Microsoft.AspNetCore.Components.Test.Routing { public class RouteTableFactoryTests { + [Fact] + public void CanCacheRouteTable() + { + // Arrange + var routes1 = RouteTableFactory.Create(new[] { GetType().Assembly, }); + + // Act + var routes2 = RouteTableFactory.Create(new[] { GetType().Assembly, }); + + // Assert + Assert.Same(routes1, routes2); + } + + [Fact] + public void CanCacheRouteTableWithDifferentAssembliesAndOrder() + { + // Arrange + var routes1 = RouteTableFactory.Create(new[] { typeof(object).Assembly, GetType().Assembly, }); + + // Act + var routes2 = RouteTableFactory.Create(new[] { GetType().Assembly, typeof(object).Assembly, }); + + // Assert + Assert.Same(routes1, routes2); + } + + [Fact] + public void DoesNotCacheRouteTableForDifferentAssemblies() + { + // Arrange + var routes1 = RouteTableFactory.Create(new[] { GetType().Assembly, }); + + // Act + var routes2 = RouteTableFactory.Create(new[] { GetType().Assembly, typeof(object).Assembly, }); + + // Assert + Assert.NotSame(routes1, routes2); + } + [Fact] public void CanDiscoverRoute() { diff --git a/src/Components/test/E2ETest/Tests/RoutingTest.cs b/src/Components/test/E2ETest/Tests/RoutingTest.cs index dd161bc7a3..4937fa9df9 100644 --- a/src/Components/test/E2ETest/Tests/RoutingTest.cs +++ b/src/Components/test/E2ETest/Tests/RoutingTest.cs @@ -418,6 +418,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests Assert.Equal("Oops, that component wasn't found!", app.FindElement(By.Id("test-info")).Text); } + [Fact] + public void RoutingToComponentOutsideMainAppWorksWithAdditionalAssemblySpecified() + { + SetUrlViaPushState("/routeablecomponentfrompackage.html"); + + var app = MountTestComponent(); + Assert.Contains("This component, including the CSS and image required to produce its", app.FindElement(By.CssSelector("div.special-style")).Text); + } + [Fact] public void ResetsScrollPositionWhenPerformingInternalNavigation_LinkClick() { diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index 5acfe91fa0..f7751abd82 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -61,6 +61,7 @@ Reordering focus retention NavigationManager Test Router + Router with additional assembly SVG SVG with child component Plain text diff --git a/src/Components/test/testassets/BasicTestApp/RouterTest/TestRouterWithAdditionalAssembly.razor b/src/Components/test/testassets/BasicTestApp/RouterTest/TestRouterWithAdditionalAssembly.razor new file mode 100644 index 0000000000..c69f08f8c3 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/RouterTest/TestRouterWithAdditionalAssembly.razor @@ -0,0 +1,11 @@ +@using Microsoft.AspNetCore.Components.Routing + + + + + + + Oops, that component wasn't found! + + +