Add AdditionalAssemblies to Router

Adds the ability to specify multiple assemblies to the Router
component.

Prior to preview 8, the router would search all dependencies of
`AppAssembly` for routable components. We made an intentional change to
stop that. However, we haven't yet give users a way to specify multiple
assemblies if their components are split across assemblies.
This commit is contained in:
Ryan Nowak 2019-08-18 14:41:31 -07:00
parent 5678f84d60
commit 8035ef0a27
10 changed files with 147 additions and 8 deletions

View File

@ -7,11 +7,13 @@
<Compile Include="Microsoft.AspNetCore.Components.netstandard2.0.cs" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<Reference Include="Microsoft.Extensions.HashCodeCombiner.Sources" />
<Reference Include="System.Buffers" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
<Compile Include="Microsoft.AspNetCore.Components.netcoreapp3.0.cs" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<Reference Include="Microsoft.Extensions.HashCodeCombiner.Sources" />
</ItemGroup>
</Project>

View File

@ -532,6 +532,8 @@ namespace Microsoft.AspNetCore.Components.Routing
{
public Router() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public System.Collections.Generic.IEnumerable<System.Reflection.Assembly> 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<Microsoft.AspNetCore.Components.RouteData> Found { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }

View File

@ -532,6 +532,8 @@ namespace Microsoft.AspNetCore.Components.Routing
{
public Router() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public System.Collections.Generic.IEnumerable<System.Reflection.Assembly> 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<Microsoft.AspNetCore.Components.RouteData> Found { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netcoreapp3.0</TargetFrameworks>
@ -16,6 +16,7 @@
<ItemGroup>
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<Reference Include="Microsoft.Extensions.HashCodeCombiner.Sources" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">

View File

@ -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
/// </summary>
internal static class RouteTableFactory
{
private static readonly ConcurrentDictionary<Assembly, RouteTable> Cache =
new ConcurrentDictionary<Assembly, RouteTable>();
private static readonly ConcurrentDictionary<Key, RouteTable> Cache =
new ConcurrentDictionary<Key, RouteTable>();
public static readonly IComparer<RouteEntry> RoutePrecedence = Comparer<RouteEntry>.Create(RouteComparison);
public static RouteTable Create(Assembly appAssembly)
public static RouteTable Create(IEnumerable<Assembly> 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<Key>
{
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;
}
}
}
}

View File

@ -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
/// </summary>
[Parameter] public Assembly AppAssembly { get; set; }
/// <summary>
/// Gets or sets a collection of additional assemblies that should be searched for components
/// that can match URIs.
/// </summary>
[Parameter] public IEnumerable<Assembly> AdditionalAssemblies { get; set; }
/// <summary>
/// Gets or sets the content to display when no match is found for the requested route.
/// </summary>
@ -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 <RouteView ...> 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;
}

View File

@ -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()
{

View File

@ -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<TestRouterWithAdditionalAssembly>();
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()
{

View File

@ -61,6 +61,7 @@
<option value="BasicTestApp.ReorderingFocusComponent">Reordering focus retention</option>
<option value="BasicTestApp.RouterTest.NavigationManagerComponent">NavigationManager Test</option>
<option value="BasicTestApp.RouterTest.TestRouter">Router</option>
<option value="BasicTestApp.RouterTest.TestRouterWithAdditionalAssembly">Router with additional assembly</option>
<option value="BasicTestApp.SvgComponent">SVG</option>
<option value="BasicTestApp.SvgWithChildComponent">SVG with child component</option>
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>

View File

@ -0,0 +1,11 @@
@using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly="@typeof(BasicTestApp.Program).Assembly" AdditionalAssemblies="@(new[] { typeof(TestContentPackage.RouteableComponentFromPackage).Assembly, })">
<Found Context="routeData">
<RouteView RouteData="@routeData" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(RouterTestLayout)">
<div id="test-info">Oops, that component wasn't found!</div>
</LayoutView>
</NotFound>
</Router>