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(); 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

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. // 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);
} }
}; }
} }
} }
} }

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> /// </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

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. // 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;
}
} }
} }
} }

View File

@ -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;

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. // 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;
} }

View File

@ -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;
} }

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 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)

View File

@ -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];

View File

@ -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];

View File

@ -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 />

View File

@ -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>

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.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;