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();
|
var serviceProvider = new TestServiceProvider();
|
||||||
serviceProvider.AddService<IMyService1>(new MyService1Impl());
|
serviceProvider.AddService<IMyService1>(new MyService1Impl());
|
||||||
serviceProvider.AddService<IMyService2>(new MyService2Impl());
|
serviceProvider.AddService<IMyService2>(new MyService2Impl());
|
||||||
var componentFactory = new ComponentFactory(serviceProvider);
|
var componentFactory = new ComponentFactory();
|
||||||
var component = componentFactory.InstantiateComponent(componentType);
|
var component = componentFactory.InstantiateComponent(serviceProvider, componentType);
|
||||||
var frames = GetRenderTree(component);
|
var frames = GetRenderTree(component);
|
||||||
|
|
||||||
// Assert 2: Rendered component behaves correctly
|
// 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.
|
// 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;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using Microsoft.AspNetCore.Components.Reflection;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components
|
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
|
internal class ComponentFactory
|
||||||
{
|
{
|
||||||
private readonly static BindingFlags _injectablePropertyBindingFlags
|
private static readonly BindingFlags _injectablePropertyBindingFlags
|
||||||
= BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
= BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||||
|
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>> _cachedInitializers
|
||||||
private readonly IDictionary<Type, Action<IComponent>> _cachedInitializers
|
= new ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>>();
|
||||||
= new ConcurrentDictionary<Type, Action<IComponent>>();
|
|
||||||
|
|
||||||
public ComponentFactory(IServiceProvider serviceProvider)
|
public static readonly ComponentFactory Instance = new ComponentFactory();
|
||||||
{
|
|
||||||
_serviceProvider = serviceProvider
|
|
||||||
?? throw new ArgumentNullException(nameof(serviceProvider));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 " +
|
throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));
|
||||||
$"implement {nameof(IComponent)}.", nameof(componentType));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance = (IComponent)Activator.CreateInstance(componentType);
|
PerformPropertyInjection(serviceProvider, component);
|
||||||
PerformPropertyInjection(instance);
|
return component;
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PerformPropertyInjection(IComponent instance)
|
private void PerformPropertyInjection(IServiceProvider serviceProvider, IComponent instance)
|
||||||
{
|
{
|
||||||
// This is thread-safe because _cachedInitializers is a ConcurrentDictionary.
|
// This is thread-safe because _cachedInitializers is a ConcurrentDictionary.
|
||||||
// We might generate the initializer more than once for a given type, but would
|
// 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))
|
if (!_cachedInitializers.TryGetValue(instanceType, out var initializer))
|
||||||
{
|
{
|
||||||
initializer = CreateInitializer(instanceType);
|
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
|
// Do all the reflection up front
|
||||||
var injectableProperties =
|
var injectableProperties =
|
||||||
MemberAssignment.GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags)
|
MemberAssignment.GetPropertiesIncludingInherited(type, _injectablePropertyBindingFlags)
|
||||||
.Where(p => p.GetCustomAttribute<InjectAttribute>() != null);
|
.Where(p => p.IsDefined(typeof(InjectAttribute)));
|
||||||
|
|
||||||
var injectables = injectableProperties.Select(property =>
|
var injectables = injectableProperties.Select(property =>
|
||||||
(
|
(
|
||||||
propertyName: property.Name,
|
propertyName: property.Name,
|
||||||
|
|
@ -66,23 +64,25 @@ namespace Microsoft.AspNetCore.Components
|
||||||
setter: MemberAssignment.CreatePropertySetter(type, property)
|
setter: MemberAssignment.CreatePropertySetter(type, property)
|
||||||
)).ToArray();
|
)).ToArray();
|
||||||
|
|
||||||
|
return Initialize;
|
||||||
|
|
||||||
// Return an action whose closure can write all the injected properties
|
// Return an action whose closure can write all the injected properties
|
||||||
// without any further reflection calls (just typecasts)
|
// 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)
|
if (serviceInstance == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"Cannot provide a value for property " +
|
throw new InvalidOperationException($"Cannot provide a value for property " +
|
||||||
$"'{injectable.propertyName}' on type '{type.FullName}'. There is no " +
|
$"'{propertyName}' on type '{type.FullName}'. There is no " +
|
||||||
$"registered service of type '{injectable.propertyType}'.");
|
$"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>
|
/// </summary>
|
||||||
public abstract partial class Renderer : IDisposable
|
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 Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
|
||||||
private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
|
private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
|
||||||
private readonly Dictionary<int, EventCallback> _eventBindings = new Dictionary<int, EventCallback>();
|
private readonly Dictionary<int, EventCallback> _eventBindings = new Dictionary<int, EventCallback>();
|
||||||
|
|
@ -61,9 +61,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
throw new ArgumentNullException(nameof(loggerFactory));
|
throw new ArgumentNullException(nameof(loggerFactory));
|
||||||
}
|
}
|
||||||
|
|
||||||
_componentFactory = new ComponentFactory(serviceProvider);
|
_serviceProvider = serviceProvider;
|
||||||
_logger = loggerFactory.CreateLogger<Renderer>();
|
_logger = loggerFactory.CreateLogger<Renderer>();
|
||||||
_componentFactory = new ComponentFactory(serviceProvider);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -77,7 +76,7 @@ namespace Microsoft.AspNetCore.Components.Rendering
|
||||||
/// <param name="componentType">The type of the component to instantiate.</param>
|
/// <param name="componentType">The type of the component to instantiate.</param>
|
||||||
/// <returns>The component instance.</returns>
|
/// <returns>The component instance.</returns>
|
||||||
protected IComponent InstantiateComponent(Type componentType)
|
protected IComponent InstantiateComponent(Type componentType)
|
||||||
=> _componentFactory.InstantiateComponent(componentType);
|
=> ComponentFactory.Instance.InstantiateComponent(_serviceProvider, componentType);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning
|
/// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -26,8 +26,8 @@ namespace Microsoft.AspNetCore.Components.Routing
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parameters will be lazily initialized.
|
// Parameters will be lazily initialized.
|
||||||
IDictionary<string, object> parameters = null;
|
Dictionary<string, object> parameters = null;
|
||||||
for (int i = 0; i < Template.Segments.Length; i++)
|
for (var i = 0; i < Template.Segments.Length; i++)
|
||||||
{
|
{
|
||||||
var segment = Template.Segments[i];
|
var segment = Template.Segments[i];
|
||||||
var pathSegment = context.Segments[i];
|
var pathSegment = context.Segments[i];
|
||||||
|
|
@ -39,23 +39,14 @@ namespace Microsoft.AspNetCore.Components.Routing
|
||||||
{
|
{
|
||||||
if (segment.IsParameter)
|
if (segment.IsParameter)
|
||||||
{
|
{
|
||||||
GetParameters()[segment.Value] = matchedParameterValue;
|
parameters ??= new Dictionary<string, object>(StringComparer.Ordinal);
|
||||||
|
parameters[segment.Value] = matchedParameterValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context.Parameters = parameters;
|
context.Parameters = parameters;
|
||||||
context.Handler = Handler;
|
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.
|
// 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.
|
// 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
|
namespace Microsoft.AspNetCore.Components.Routing
|
||||||
{
|
{
|
||||||
internal class RouteTable
|
internal class RouteTable
|
||||||
|
|
@ -16,117 +10,13 @@ namespace Microsoft.AspNetCore.Components.Routing
|
||||||
Routes = routes;
|
Routes = routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RouteEntry[] Routes { get; set; }
|
public RouteEntry[] Routes { get; }
|
||||||
|
|
||||||
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}'
|
|
||||||
");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void Route(RouteContext routeContext)
|
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)
|
if (routeContext.Handler != null)
|
||||||
{
|
{
|
||||||
return;
|
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.
|
// 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
|
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;
|
Segments = segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,7 @@ namespace Microsoft.AspNetCore.Components.Routing
|
||||||
public Task SetParametersAsync(ParameterCollection parameters)
|
public Task SetParametersAsync(ParameterCollection parameters)
|
||||||
{
|
{
|
||||||
parameters.SetParameterProperties(this);
|
parameters.SetParameterProperties(this);
|
||||||
var types = ComponentResolver.ResolveComponents(AppAssembly);
|
Routes = RouteTableFactory.Create(AppAssembly);
|
||||||
Routes = RouteTable.Create(types);
|
|
||||||
Refresh(isNavigationIntercepted: false);
|
Refresh(isNavigationIntercepted: false);
|
||||||
return Task.CompletedTask;
|
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
|
namespace Microsoft.AspNetCore.Components.Test.Routing
|
||||||
{
|
{
|
||||||
public class RouteTableTests
|
public class RouteTableFactoryTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanDiscoverRoute()
|
public void CanDiscoverRoute()
|
||||||
{
|
{
|
||||||
// Arrange & Act
|
// Arrange & Act
|
||||||
var routes = RouteTable.Create(new[] { typeof(MyComponent), });
|
var routes = RouteTableFactory.Create(new[] { typeof(MyComponent), });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("Test1", Assert.Single(routes.Routes).Template.TemplateText);
|
Assert.Equal("Test1", Assert.Single(routes.Routes).Template.TemplateText);
|
||||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
||||||
public void CanDiscoverRoutes_WithInheritance()
|
public void CanDiscoverRoutes_WithInheritance()
|
||||||
{
|
{
|
||||||
// Arrange & Act
|
// Arrange & Act
|
||||||
var routes = RouteTable.Create(new[] { typeof(MyComponent), typeof(MyInheritedComponent), });
|
var routes = RouteTableFactory.Create(new[] { typeof(MyComponent), typeof(MyInheritedComponent), });
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
|
|
@ -363,7 +363,7 @@ namespace Microsoft.AspNetCore.Components.Test.Routing
|
||||||
{
|
{
|
||||||
return new RouteTable(_routeTemplates
|
return new RouteTable(_routeTemplates
|
||||||
.Select(rt => new RouteEntry(TemplateParser.ParseTemplate(rt.Item1), rt.Item2))
|
.Select(rt => new RouteEntry(TemplateParser.ParseTemplate(rt.Item1), rt.Item2))
|
||||||
.OrderBy(id => id, RouteTable.RoutePrecedence)
|
.OrderBy(id => id, RouteTableFactory.RoutePrecedence)
|
||||||
.ToArray());
|
.ToArray());
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex) when (ex.InnerException is InvalidOperationException)
|
catch (InvalidOperationException ex) when (ex.InnerException is InvalidOperationException)
|
||||||
|
|
@ -159,22 +159,12 @@ namespace Microsoft.AspNetCore.Components.Routing
|
||||||
|
|
||||||
public bool Equals(RouteTemplate x, RouteTemplate y)
|
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)
|
if (x.Segments.Length != y.Segments.Length)
|
||||||
{
|
{
|
||||||
return false;
|
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 xSegment = x.Segments[i];
|
||||||
var ySegment = y.Segments[i];
|
var ySegment = y.Segments[i];
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using BasicTestApp;
|
using BasicTestApp;
|
||||||
using BasicTestApp.RouterTest;
|
using BasicTestApp.RouterTest;
|
||||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
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);
|
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)
|
private string SetUrlViaPushState(string relativeUri)
|
||||||
{
|
{
|
||||||
var pathBaseWithoutHash = ServerPathBase.Split('#')[0];
|
var pathBaseWithoutHash = ServerPathBase.Split('#')[0];
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
@page "/"
|
@page "/"
|
||||||
|
@page "/Default.html"
|
||||||
<div id="test-info">This is the default page.</div>
|
<div id="test-info">This is the default page.</div>
|
||||||
<Links />
|
<Links />
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
@inject Microsoft.AspNetCore.Components.IUriHelper uriHelper
|
@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>
|
<ul>
|
||||||
<li><NavLink href="/subdir/" Match=NavLinkMatch.All>Default (matches all)</NavLink></li>
|
<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="" 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/?abc=123">Default with query</NavLink></li>
|
||||||
<li><NavLink href="/subdir/#blah">Default with hash</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="/subdir/Other">Other</NavLink></li>
|
||||||
<li><NavLink href="Other" Match=NavLinkMatch.All>Other with base-relative URL (matches all)</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>
|
<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="/" target="_blank">Target (_blank)</a>
|
||||||
|
|
||||||
<a href="/subdir/NotAComponent.html">Not a component</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.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue