Cache the result of ComponentResolver (#11810)

* Add caching to ComponentFactory and ComponentResolver
This commit is contained in:
Pranav K 2019-07-16 13:38:08 -07:00 committed by GitHub
parent 71d39e59d6
commit 9f82b7be75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 405 additions and 261 deletions

View File

@ -132,8 +132,8 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
var serviceProvider = new TestServiceProvider();
serviceProvider.AddService<IMyService1>(new MyService1Impl());
serviceProvider.AddService<IMyService2>(new MyService2Impl());
var componentFactory = new ComponentFactory(serviceProvider);
var component = componentFactory.InstantiateComponent(componentType);
var componentFactory = new ComponentFactory();
var component = componentFactory.InstantiateComponent(serviceProvider, componentType);
var frames = GetRenderTree(component);
// Assert 2: Rendered component behaves correctly

View File

@ -1,44 +1,41 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Components.Reflection;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Components.Reflection;
namespace Microsoft.AspNetCore.Components
{
/// <remarks>
/// The <see cref="Instance"/> property on this type is used as a static global cache. Ensure any changes to this type
/// are thread safe and can be safely cached statically.
/// </remarks>
internal class ComponentFactory
{
private readonly static BindingFlags _injectablePropertyBindingFlags
private static readonly BindingFlags _injectablePropertyBindingFlags
= BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
private readonly IServiceProvider _serviceProvider;
private readonly IDictionary<Type, Action<IComponent>> _cachedInitializers
= new ConcurrentDictionary<Type, Action<IComponent>>();
private readonly ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>> _cachedInitializers
= new ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>>();
public ComponentFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider
?? throw new ArgumentNullException(nameof(serviceProvider));
}
public static readonly ComponentFactory Instance = new ComponentFactory();
public IComponent InstantiateComponent(Type componentType)
public IComponent InstantiateComponent(IServiceProvider serviceProvider, Type componentType)
{
if (!typeof(IComponent).IsAssignableFrom(componentType))
var instance = Activator.CreateInstance(componentType);
if (!(instance is IComponent component))
{
throw new ArgumentException($"The type {componentType.FullName} does not " +
$"implement {nameof(IComponent)}.", nameof(componentType));
throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));
}
var instance = (IComponent)Activator.CreateInstance(componentType);
PerformPropertyInjection(instance);
return instance;
PerformPropertyInjection(serviceProvider, component);
return component;
}
private void PerformPropertyInjection(IComponent instance)
private void PerformPropertyInjection(IServiceProvider serviceProvider, IComponent instance)
{
// This is thread-safe because _cachedInitializers is a ConcurrentDictionary.
// We might generate the initializer more than once for a given type, but would
@ -47,18 +44,19 @@ namespace Microsoft.AspNetCore.Components
if (!_cachedInitializers.TryGetValue(instanceType, out var initializer))
{
initializer = CreateInitializer(instanceType);
_cachedInitializers[instanceType] = initializer;
_cachedInitializers.TryAdd(instanceType, initializer);
}
initializer(instance);
initializer(serviceProvider, instance);
}
private Action<IComponent> CreateInitializer(Type type)
private Action<IServiceProvider, IComponent> CreateInitializer(Type type)
{
// Do all the reflection up front
var injectableProperties =
MemberAssignment.GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags)
.Where(p => p.GetCustomAttribute<InjectAttribute>() != null);
.Where(p => p.IsDefined(typeof(InjectAttribute)));
var injectables = injectableProperties.Select(property =>
(
propertyName: property.Name,
@ -66,23 +64,25 @@ namespace Microsoft.AspNetCore.Components
setter: MemberAssignment.CreatePropertySetter(type, property)
)).ToArray();
return Initialize;
// Return an action whose closure can write all the injected properties
// without any further reflection calls (just typecasts)
return instance =>
void Initialize(IServiceProvider serviceProvider, IComponent component)
{
foreach (var injectable in injectables)
foreach (var (propertyName, propertyType, setter) in injectables)
{
var serviceInstance = _serviceProvider.GetService(injectable.propertyType);
var serviceInstance = serviceProvider.GetService(propertyType);
if (serviceInstance == null)
{
throw new InvalidOperationException($"Cannot provide a value for property " +
$"'{injectable.propertyName}' on type '{type.FullName}'. There is no " +
$"registered service of type '{injectable.propertyType}'.");
$"'{propertyName}' on type '{type.FullName}'. There is no " +
$"registered service of type '{propertyType}'.");
}
injectable.setter.SetValue(instance, serviceInstance);
setter.SetValue(component, serviceInstance);
}
};
}
}
}
}

View File

@ -1,73 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Resolves components for an application.
/// </summary>
internal static class ComponentResolver
{
/// <summary>
/// Lists all the types
/// </summary>
/// <param name="appAssembly"></param>
/// <returns></returns>
public static IEnumerable<Type> ResolveComponents(Assembly appAssembly)
{
var componentsAssembly = typeof(IComponent).Assembly;
return EnumerateAssemblies(appAssembly.GetName(), componentsAssembly, new HashSet<Assembly>(new AssemblyComparer()))
.SelectMany(a => a.ExportedTypes)
.Where(t => typeof(IComponent).IsAssignableFrom(t));
}
private static IEnumerable<Assembly> EnumerateAssemblies(
AssemblyName assemblyName,
Assembly componentAssembly,
HashSet<Assembly> visited)
{
var assembly = Assembly.Load(assemblyName);
if (visited.Contains(assembly))
{
// Avoid traversing visited assemblies.
yield break;
}
visited.Add(assembly);
var references = assembly.GetReferencedAssemblies();
if (!references.Any(r => string.Equals(r.FullName, componentAssembly.FullName, StringComparison.Ordinal)))
{
// Avoid traversing references that don't point to Components (like netstandard2.0)
yield break;
}
else
{
yield return assembly;
// Look at the list of transitive dependencies for more components.
foreach (var reference in references.SelectMany(r => EnumerateAssemblies(r, componentAssembly, visited)))
{
yield return reference;
}
}
}
private class AssemblyComparer : IEqualityComparer<Assembly>
{
public bool Equals(Assembly x, Assembly y)
{
return string.Equals(x?.FullName, y?.FullName, StringComparison.Ordinal);
}
public int GetHashCode(Assembly obj)
{
return obj.FullName.GetHashCode();
}
}
}
}

View File

@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// </summary>
public abstract partial class Renderer : IDisposable
{
private readonly ComponentFactory _componentFactory;
private readonly IServiceProvider _serviceProvider;
private readonly Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
private readonly Dictionary<int, EventCallback> _eventBindings = new Dictionary<int, EventCallback>();
@ -61,9 +61,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
throw new ArgumentNullException(nameof(loggerFactory));
}
_componentFactory = new ComponentFactory(serviceProvider);
_serviceProvider = serviceProvider;
_logger = loggerFactory.CreateLogger<Renderer>();
_componentFactory = new ComponentFactory(serviceProvider);
}
/// <summary>
@ -77,7 +76,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
/// <param name="componentType">The type of the component to instantiate.</param>
/// <returns>The component instance.</returns>
protected IComponent InstantiateComponent(Type componentType)
=> _componentFactory.InstantiateComponent(componentType);
=> ComponentFactory.Instance.InstantiateComponent(_serviceProvider, componentType);
/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -26,8 +26,8 @@ namespace Microsoft.AspNetCore.Components.Routing
}
// Parameters will be lazily initialized.
IDictionary<string, object> parameters = null;
for (int i = 0; i < Template.Segments.Length; i++)
Dictionary<string, object> parameters = null;
for (var i = 0; i < Template.Segments.Length; i++)
{
var segment = Template.Segments[i];
var pathSegment = context.Segments[i];
@ -39,23 +39,14 @@ namespace Microsoft.AspNetCore.Components.Routing
{
if (segment.IsParameter)
{
GetParameters()[segment.Value] = matchedParameterValue;
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
parameters[segment.Value] = matchedParameterValue;
}
}
}
context.Parameters = parameters;
context.Handler = Handler;
IDictionary<string, object> GetParameters()
{
if (parameters == null)
{
parameters = new Dictionary<string, object>();
}
return parameters;
}
}
}
}

View File

@ -1,12 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Components;
namespace Microsoft.AspNetCore.Components.Routing
{
internal class RouteTable
@ -16,117 +10,13 @@ namespace Microsoft.AspNetCore.Components.Routing
Routes = routes;
}
public RouteEntry[] Routes { get; set; }
public static RouteTable Create(IEnumerable<Type> types)
{
var routes = new List<RouteEntry>();
foreach (var type in types)
{
// We're deliberately using inherit = false here.
//
// RouteAttribute is defined as non-inherited, because inheriting a route attribute always causes an
// ambiguity. You end up with two components (base class and derived class) with the same route.
var routeAttributes = type.GetCustomAttributes<RouteAttribute>(inherit: false);
foreach (var routeAttribute in routeAttributes)
{
var template = TemplateParser.ParseTemplate(routeAttribute.Template);
var entry = new RouteEntry(template, type);
routes.Add(entry);
}
}
return new RouteTable(routes.OrderBy(id => id, RoutePrecedence).ToArray());
}
public static IComparer<RouteEntry> RoutePrecedence { get; } = Comparer<RouteEntry>.Create(RouteComparison);
/// <summary>
/// Route precedence algorithm.
/// We collect all the routes and sort them from most specific to
/// less specific. The specificity of a route is given by the specificity
/// of its segments and the position of those segments in the route.
/// * A literal segment is more specific than a parameter segment.
/// * A parameter segment with more constraints is more specific than one with fewer constraints
/// * Segment earlier in the route are evaluated before segments later in the route.
/// For example:
/// /Literal is more specific than /Parameter
/// /Route/With/{parameter} is more specific than /{multiple}/With/{parameters}
/// /Product/{id:int} is more specific than /Product/{id}
///
/// Routes can be ambiguous if:
/// They are composed of literals and those literals have the same values (case insensitive)
/// They are composed of a mix of literals and parameters, in the same relative order and the
/// literals have the same values.
/// For example:
/// * /literal and /Literal
/// /{parameter}/literal and /{something}/literal
/// /{parameter:constraint}/literal and /{something:constraint}/literal
///
/// To calculate the precedence we sort the list of routes as follows:
/// * Shorter routes go first.
/// * A literal wins over a parameter in precedence.
/// * For literals with different values (case insensitive) we choose the lexical order
/// * For parameters with different numbers of constraints, the one with more wins
/// If we get to the end of the comparison routing we've detected an ambiguous pair of routes.
/// </summary>
internal static int RouteComparison(RouteEntry x, RouteEntry y)
{
var xTemplate = x.Template;
var yTemplate = y.Template;
if (xTemplate.Segments.Length != y.Template.Segments.Length)
{
return xTemplate.Segments.Length < y.Template.Segments.Length ? -1 : 1;
}
else
{
for (int i = 0; i < xTemplate.Segments.Length; i++)
{
var xSegment = xTemplate.Segments[i];
var ySegment = yTemplate.Segments[i];
if (!xSegment.IsParameter && ySegment.IsParameter)
{
return -1;
}
if (xSegment.IsParameter && !ySegment.IsParameter)
{
return 1;
}
if (xSegment.IsParameter)
{
if (xSegment.Constraints.Length > ySegment.Constraints.Length)
{
return -1;
}
else if (xSegment.Constraints.Length < ySegment.Constraints.Length)
{
return 1;
}
}
else
{
var comparison = string.Compare(xSegment.Value, ySegment.Value, StringComparison.OrdinalIgnoreCase);
if (comparison != 0)
{
return comparison;
}
}
}
throw new InvalidOperationException($@"The following routes are ambiguous:
'{x.Template.TemplateText}' in '{x.Handler.FullName}'
'{y.Template.TemplateText}' in '{y.Handler.FullName}'
");
}
}
public RouteEntry[] Routes { get; }
internal void Route(RouteContext routeContext)
{
foreach (var route in Routes)
for (var i = 0; i < Routes.Length; i++)
{
route.Match(routeContext);
Routes[i].Match(routeContext);
if (routeContext.Handler != null)
{
return;

View File

@ -0,0 +1,137 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Components.Routing;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Resolves components for an application.
/// </summary>
internal static class RouteTableFactory
{
private static readonly ConcurrentDictionary<Assembly, RouteTable> Cache =
new ConcurrentDictionary<Assembly, RouteTable>();
public static readonly IComparer<RouteEntry> RoutePrecedence = Comparer<RouteEntry>.Create(RouteComparison);
public static RouteTable Create(Assembly appAssembly)
{
if (Cache.TryGetValue(appAssembly, out var resolvedComponents))
{
return resolvedComponents;
}
var componentTypes = appAssembly.ExportedTypes.Where(t => typeof(IComponent).IsAssignableFrom(t));
var routeTable = Create(componentTypes);
Cache.TryAdd(appAssembly, routeTable);
return routeTable;
}
internal static RouteTable Create(IEnumerable<Type> componentTypes)
{
var routes = new List<RouteEntry>();
foreach (var type in componentTypes)
{
// We're deliberately using inherit = false here.
//
// RouteAttribute is defined as non-inherited, because inheriting a route attribute always causes an
// ambiguity. You end up with two components (base class and derived class) with the same route.
var routeAttributes = type.GetCustomAttributes<RouteAttribute>(inherit: false);
foreach (var routeAttribute in routeAttributes)
{
var template = TemplateParser.ParseTemplate(routeAttribute.Template);
var entry = new RouteEntry(template, type);
routes.Add(entry);
}
}
return new RouteTable(routes.OrderBy(id => id, RoutePrecedence).ToArray());
}
/// <summary>
/// Route precedence algorithm.
/// We collect all the routes and sort them from most specific to
/// less specific. The specificity of a route is given by the specificity
/// of its segments and the position of those segments in the route.
/// * A literal segment is more specific than a parameter segment.
/// * A parameter segment with more constraints is more specific than one with fewer constraints
/// * Segment earlier in the route are evaluated before segments later in the route.
/// For example:
/// /Literal is more specific than /Parameter
/// /Route/With/{parameter} is more specific than /{multiple}/With/{parameters}
/// /Product/{id:int} is more specific than /Product/{id}
///
/// Routes can be ambiguous if:
/// They are composed of literals and those literals have the same values (case insensitive)
/// They are composed of a mix of literals and parameters, in the same relative order and the
/// literals have the same values.
/// For example:
/// * /literal and /Literal
/// /{parameter}/literal and /{something}/literal
/// /{parameter:constraint}/literal and /{something:constraint}/literal
///
/// To calculate the precedence we sort the list of routes as follows:
/// * Shorter routes go first.
/// * A literal wins over a parameter in precedence.
/// * For literals with different values (case insensitive) we choose the lexical order
/// * For parameters with different numbers of constraints, the one with more wins
/// If we get to the end of the comparison routing we've detected an ambiguous pair of routes.
/// </summary>
internal static int RouteComparison(RouteEntry x, RouteEntry y)
{
var xTemplate = x.Template;
var yTemplate = y.Template;
if (xTemplate.Segments.Length != y.Template.Segments.Length)
{
return xTemplate.Segments.Length < y.Template.Segments.Length ? -1 : 1;
}
else
{
for (var i = 0; i < xTemplate.Segments.Length; i++)
{
var xSegment = xTemplate.Segments[i];
var ySegment = yTemplate.Segments[i];
if (!xSegment.IsParameter && ySegment.IsParameter)
{
return -1;
}
if (xSegment.IsParameter && !ySegment.IsParameter)
{
return 1;
}
if (xSegment.IsParameter)
{
if (xSegment.Constraints.Length > ySegment.Constraints.Length)
{
return -1;
}
else if (xSegment.Constraints.Length < ySegment.Constraints.Length)
{
return 1;
}
}
else
{
var comparison = string.Compare(xSegment.Value, ySegment.Value, StringComparison.OrdinalIgnoreCase);
if (comparison != 0)
{
return comparison;
}
}
}
throw new InvalidOperationException($@"The following routes are ambiguous:
'{x.Template.TemplateText}' in '{x.Handler.FullName}'
'{y.Template.TemplateText}' in '{y.Handler.FullName}'
");
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
@ -6,11 +6,9 @@ namespace Microsoft.AspNetCore.Components.Routing
{
internal class RouteTemplate
{
public static readonly char[] Separators = new[] { '/' };
public RouteTemplate(string TemplateText, TemplateSegment[] segments)
public RouteTemplate(string templateText, TemplateSegment[] segments)
{
this.TemplateText = TemplateText;
TemplateText = templateText;
Segments = segments;
}

View File

@ -70,8 +70,7 @@ namespace Microsoft.AspNetCore.Components.Routing
public Task SetParametersAsync(ParameterCollection parameters)
{
parameters.SetParameterProperties(this);
var types = ComponentResolver.ResolveComponents(AppAssembly);
Routes = RouteTable.Create(types);
Routes = RouteTableFactory.Create(AppAssembly);
Refresh(isNavigationIntercepted: false);
return Task.CompletedTask;
}

View File

@ -0,0 +1,168 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Components
{
public class ComponentFactoryTest
{
[Fact]
public void InstantiateComponent_CreatesInstance()
{
// Arrange
var componentType = typeof(EmptyComponent);
var factory = new ComponentFactory();
// Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
// Assert
Assert.NotNull(instance);
Assert.IsType<EmptyComponent>(instance);
}
[Fact]
public void InstantiateComponent_AssignsPropertiesWithInjectAttribute()
{
// Arrange
var componentType = typeof(ComponentWithInjectProperties);
var factory = new ComponentFactory();
// Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
// Assert
Assert.NotNull(instance);
var component = Assert.IsType<ComponentWithInjectProperties>(instance);
// Public, and non-public properties, and properties with non-public setters should get assigned
Assert.NotNull(component.Property1);
Assert.NotNull(component.GetProperty2());
Assert.NotNull(component.Property3);
Assert.NotNull(component.Property4);
}
[Fact]
public void InstantiateComponent_AssignsPropertiesWithInjectAttributeOnBaseType()
{
// Arrange
var componentType = typeof(DerivedComponent);
var factory = new ComponentFactory();
// Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
// Assert
Assert.NotNull(instance);
var component = Assert.IsType<DerivedComponent>(instance);
Assert.NotNull(component.Property1);
Assert.NotNull(component.GetProperty2());
Assert.NotNull(component.Property3);
// Property on derived type without [Inject] should not be assigned
Assert.Null(component.Property4);
// Property on the base type with the [Inject] attribute should
Assert.NotNull(((ComponentWithInjectProperties)component).Property4);
}
[Fact]
public void InstantiateComponent_IgnoresPropertiesWithoutInjectAttribute()
{
// Arrange
var componentType = typeof(ComponentWithNonInjectableProperties);
var factory = new ComponentFactory();
// Act
var instance = factory.InstantiateComponent(GetServiceProvider(), componentType);
// Assert
Assert.NotNull(instance);
var component = Assert.IsType<ComponentWithNonInjectableProperties>(instance);
// Public, and non-public properties, and properties with non-public setters should get assigned
Assert.NotNull(component.Property1);
Assert.Null(component.Property2);
}
private static IServiceProvider GetServiceProvider()
{
return new ServiceCollection()
.AddTransient<TestService1>()
.AddTransient<TestService2>()
.BuildServiceProvider();
}
private class EmptyComponent : IComponent
{
public void Configure(RenderHandle renderHandle)
{
throw new NotImplementedException();
}
public Task SetParametersAsync(ParameterCollection parameters)
{
throw new NotImplementedException();
}
}
private class ComponentWithInjectProperties : IComponent
{
[Inject]
public TestService1 Property1 { get; set; }
[Inject]
private TestService2 Property2 { get; set; }
[Inject]
public TestService1 Property3 { get; private set; }
[Inject]
public TestService1 Property4 { get; set; }
public TestService2 GetProperty2() => Property2;
public void Configure(RenderHandle renderHandle)
{
throw new NotImplementedException();
}
public Task SetParametersAsync(ParameterCollection parameters)
{
throw new NotImplementedException();
}
}
private class ComponentWithNonInjectableProperties : IComponent
{
[Inject]
public TestService1 Property1 { get; set; }
public TestService1 Property2 { get; set; }
public void Configure(RenderHandle renderHandle)
{
throw new NotImplementedException();
}
public Task SetParametersAsync(ParameterCollection parameters)
{
throw new NotImplementedException();
}
}
private class DerivedComponent : ComponentWithInjectProperties
{
public new TestService2 Property4 { get; set; }
[Inject]
public TestService2 Property5 { get; set; }
}
public class TestService1 { }
public class TestService2 { }
}
}

View File

@ -9,13 +9,13 @@ using Xunit;
namespace Microsoft.AspNetCore.Components.Test.Routing
{
public class RouteTableTests
public class RouteTableFactoryTests
{
[Fact]
public void CanDiscoverRoute()
{
// Arrange & Act
var routes = RouteTable.Create(new[] { typeof(MyComponent), });
var routes = RouteTableFactory.Create(new[] { typeof(MyComponent), });
// Assert
Assert.Equal("Test1", Assert.Single(routes.Routes).Template.TemplateText);
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
public void CanDiscoverRoutes_WithInheritance()
{
// Arrange & Act
var routes = RouteTable.Create(new[] { typeof(MyComponent), typeof(MyInheritedComponent), });
var routes = RouteTableFactory.Create(new[] { typeof(MyComponent), typeof(MyInheritedComponent), });
// Assert
Assert.Collection(
@ -363,7 +363,7 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
{
return new RouteTable(_routeTemplates
.Select(rt => new RouteEntry(TemplateParser.ParseTemplate(rt.Item1), rt.Item2))
.OrderBy(id => id, RouteTable.RoutePrecedence)
.OrderBy(id => id, RouteTableFactory.RoutePrecedence)
.ToArray());
}
catch (InvalidOperationException ex) when (ex.InnerException is InvalidOperationException)

View File

@ -159,22 +159,12 @@ namespace Microsoft.AspNetCore.Components.Routing
public bool Equals(RouteTemplate x, RouteTemplate y)
{
if (x == null && y == null)
{
return true;
}
if ((x == null) != (y == null))
{
return false;
}
if (x.Segments.Length != y.Segments.Length)
{
return false;
}
for (int i = 0; i < x.Segments.Length; i++)
for (var i = 0; i < x.Segments.Length; i++)
{
var xSegment = x.Segments[i];
var ySegment = y.Segments[i];

View File

@ -4,7 +4,6 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using BasicTestApp;
using BasicTestApp.RouterTest;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
@ -389,6 +388,27 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal(initialUrl, () => app.FindElement(By.Id("test-info")).Text);
}
[Fact]
public void CanArriveAtRouteWithExtension()
{
// This is an odd test, but it's primarily here to verify routing for routeablecomponentfrompackage isn't available due to
// some unknown reason
SetUrlViaPushState("/Default.html");
var app = MountTestComponent<TestRouter>();
Assert.Equal("This is the default page.", app.FindElement(By.Id("test-info")).Text);
AssertHighlightedLinks("With extension");
}
[Fact]
public void RoutingToComponentOutsideMainAppDoesNotWork()
{
SetUrlViaPushState("/routeablecomponentfrompackage.html");
var app = MountTestComponent<TestRouter>();
Assert.Equal("Oops, that component wasn't found!", app.FindElement(By.Id("test-info")).Text);
}
private string SetUrlViaPushState(string relativeUri)
{
var pathBaseWithoutHash = ServerPathBase.Split('#')[0];

View File

@ -1,3 +1,4 @@
@page "/"
@page "/Default.html"
<div id="test-info">This is the default page.</div>
<Links />

View File

@ -1,11 +1,17 @@
@using Microsoft.AspNetCore.Components.Routing
@inject Microsoft.AspNetCore.Components.IUriHelper uriHelper
<style type="text/css">a.active { background-color: yellow; font-weight: bold; }</style>
<style type="text/css">
a.active {
background-color: yellow;
font-weight: bold;
}
</style>
<ul>
<li><NavLink href="/subdir/" Match=NavLinkMatch.All>Default (matches all)</NavLink></li>
<li><NavLink href="" Match=NavLinkMatch.All>Default with base-relative URL (matches all)</NavLink></li>
<li><NavLink href="/subdir/?abc=123">Default with query</NavLink></li>
<li><NavLink href="/subdir/#blah">Default with hash</NavLink></li>
<li><NavLink href="/subdir/Default.html">With extension</NavLink></li>
<li><NavLink href="/subdir/Other">Other</NavLink></li>
<li><NavLink href="Other" Match=NavLinkMatch.All>Other with base-relative URL (matches all)</NavLink></li>
<li><NavLink href="/subdir/Other?abc=123">Other with query</NavLink></li>
@ -29,4 +35,5 @@
<a href="/" target="_blank">Target (_blank)</a>
<a href="/subdir/NotAComponent.html">Not a component</a>
<a href="/subdir/routeablecomponentfrompackage.html">Cannot route to me</a>

View File

@ -0,0 +1,16 @@
@page "/routeablecomponentfrompackage.html"
<div class="special-style">
This component, including the CSS and image required to produce its
elegant styling, is in an external NuGet package.
<button @onclick="ChangeLabel">@buttonLabel </button>
</div>
@code {
string buttonLabel = "Click me";
void ChangeLabel()
{
buttonLabel = "It works";
}
}

View File

@ -3,6 +3,7 @@ using BasicTestApp;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;