Cache the result of ComponentResolver (#11810)
* Add caching to ComponentFactory and ComponentResolver
This commit is contained in:
parent
71d39e59d6
commit
9f82b7be75
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}'
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
@page "/"
|
||||
@page "/Default.html"
|
||||
<div id="test-info">This is the default page.</div>
|
||||
<Links />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue