* Fix #6102 - Intense CPU utilization on page change The issue here was that every time a Razor Page changed, we would subscribe an additional time to the endpoint change notifications. This means that if you tweaked a page 30 times, we would update the address table 31 times when you save the file. If you were doing a lot of editing then this would grow to a really large amount of computation. The fix is to use DataSourceDependentCache, which is an existing utility type we developed for this purpose. I'm not sure why it wasn't being used for this already. We're already using DataSourceDependentCache in a bunch of other places, and it's well tested. I also tweaked the stucture of this code to be more similar to EndpointNameAddressScheme. This involved some test changes that all seemed like good cleanup. The way this was being tested was a little wonky.
This commit is contained in:
parent
71ce37ee91
commit
a5658a8c95
|
|
@ -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;
|
||||
|
|
@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http;
|
|||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal class DataSourceDependentCache<T> where T : class
|
||||
internal class DataSourceDependentCache<T> : IDisposable where T : class
|
||||
{
|
||||
private readonly EndpointDataSource _dataSource;
|
||||
private readonly Func<IReadOnlyList<Endpoint>, T> _initializeCore;
|
||||
|
|
@ -18,6 +18,9 @@ namespace Microsoft.AspNetCore.Routing
|
|||
private object _lock;
|
||||
private bool _initialized;
|
||||
private T _value;
|
||||
|
||||
private IDisposable _disposable;
|
||||
private bool _disposed;
|
||||
|
||||
public DataSourceDependentCache(EndpointDataSource dataSource, Func<IReadOnlyList<Endpoint>, T> initialize)
|
||||
{
|
||||
|
|
@ -26,6 +29,11 @@ namespace Microsoft.AspNetCore.Routing
|
|||
throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
if (initialize == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(initialize));
|
||||
}
|
||||
|
||||
_dataSource = dataSource;
|
||||
_initializeCore = initialize;
|
||||
|
||||
|
|
@ -51,9 +59,32 @@ namespace Microsoft.AspNetCore.Routing
|
|||
var changeToken = _dataSource.GetChangeToken();
|
||||
_value = _initializeCore(_dataSource.Endpoints);
|
||||
|
||||
changeToken.RegisterChangeCallback(_initializerWithState, null);
|
||||
// Don't resubscribe if we're already disposed.
|
||||
if (_disposed)
|
||||
{
|
||||
return _value;
|
||||
}
|
||||
|
||||
_disposable = changeToken.RegisterChangeCallback(_initializerWithState, null);
|
||||
return _value;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposable?.Dispose();
|
||||
_disposable = null;
|
||||
|
||||
// Tracking whether we're disposed or not prevents a race-condition
|
||||
// between disposal and Initialize(). If a change callback fires after
|
||||
// we dispose, then we don't want to reregister.
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ using Microsoft.Extensions.Options;
|
|||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal sealed class DefaultLinkGenerator : LinkGenerator
|
||||
internal sealed class DefaultLinkGenerator : LinkGenerator, IDisposable
|
||||
{
|
||||
private readonly ParameterPolicyFactory _parameterPolicyFactory;
|
||||
private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool;
|
||||
|
|
@ -364,6 +364,11 @@ namespace Microsoft.AspNetCore.Routing
|
|||
return httpContext?.Features.Get<IRouteValuesFeature>()?.RouteValues;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
}
|
||||
|
||||
private static class Log
|
||||
{
|
||||
public static class EventIds
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
services.TryAddSingleton<MatcherFactory, DfaMatcherFactory>();
|
||||
services.TryAddTransient<DfaMatcherBuilder>();
|
||||
services.TryAddSingleton<DfaGraphWriter>();
|
||||
services.TryAddTransient<DataSourceDependentMatcher.Lifetime>();
|
||||
|
||||
// Link generation related services
|
||||
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
|
||||
|
|
@ -93,7 +94,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
// Misc infrastructure
|
||||
//
|
||||
services.TryAddSingleton<RoutePatternTransformer, DefaultRoutePatternTransformer>();
|
||||
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Http;
|
|||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal class EndpointNameAddressScheme : IEndpointAddressScheme<string>
|
||||
internal class EndpointNameAddressScheme : IEndpointAddressScheme<string>, IDisposable
|
||||
{
|
||||
private readonly DataSourceDependentCache<Dictionary<string, Endpoint[]>> _cache;
|
||||
|
||||
|
|
@ -103,5 +103,10 @@ namespace Microsoft.AspNetCore.Routing
|
|||
return endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing.Matching
|
||||
{
|
||||
|
|
@ -16,12 +15,17 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
|
||||
public DataSourceDependentMatcher(
|
||||
EndpointDataSource dataSource,
|
||||
Lifetime lifetime,
|
||||
Func<MatcherBuilder> matcherBuilderFactory)
|
||||
{
|
||||
_matcherBuilderFactory = matcherBuilderFactory;
|
||||
|
||||
_cache = new DataSourceDependentCache<Matcher>(dataSource, CreateMatcher);
|
||||
_cache.EnsureInitialized();
|
||||
|
||||
// This will Dispose the cache when the lifetime is disposed, this allows
|
||||
// the service provider to manage the lifetime of the cache.
|
||||
lifetime.Cache = _cache;
|
||||
}
|
||||
|
||||
// Used in tests
|
||||
|
|
@ -48,5 +52,41 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
// Used to tie the lifetime of a DataSourceDependentCache to the service provider
|
||||
public class Lifetime : IDisposable
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
private DataSourceDependentCache<Matcher> _cache;
|
||||
private bool _disposed;
|
||||
|
||||
public DataSourceDependentCache<Matcher> Cache
|
||||
{
|
||||
get => _cache;
|
||||
set
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
value?.Dispose();
|
||||
}
|
||||
|
||||
_cache = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_cache?.Dispose();
|
||||
_cache = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -29,7 +29,11 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
throw new ArgumentNullException(nameof(dataSource));
|
||||
}
|
||||
|
||||
return new DataSourceDependentMatcher(dataSource, () =>
|
||||
// Creates a tracking entry in DI to stop listening for change events
|
||||
// when the services are disposed.
|
||||
var lifetime = _services.GetRequiredService<DataSourceDependentMatcher.Lifetime>();
|
||||
|
||||
return new DataSourceDependentMatcher(dataSource, lifetime, () =>
|
||||
{
|
||||
return _services.GetRequiredService<DfaMatcherBuilder>();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,46 +1,44 @@
|
|||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing.Internal;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using Microsoft.AspNetCore.Routing.Tree;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
internal class RouteValuesAddressScheme : IEndpointAddressScheme<RouteValuesAddress>
|
||||
internal class RouteValuesAddressScheme : IEndpointAddressScheme<RouteValuesAddress>, IDisposable
|
||||
{
|
||||
private readonly EndpointDataSource _dataSource;
|
||||
private LinkGenerationDecisionTree _allMatchesLinkGenerationTree;
|
||||
private Dictionary<string, List<OutboundMatchResult>> _namedMatchResults;
|
||||
|
||||
private readonly DataSourceDependentCache<StateEntry> _cache;
|
||||
|
||||
public RouteValuesAddressScheme(EndpointDataSource dataSource)
|
||||
{
|
||||
_dataSource = dataSource;
|
||||
|
||||
// Build initial matches
|
||||
BuildOutboundMatches();
|
||||
|
||||
// Register for changes in endpoints
|
||||
ChangeToken.OnChange(
|
||||
_dataSource.GetChangeToken,
|
||||
HandleChange);
|
||||
_cache = new DataSourceDependentCache<StateEntry>(dataSource, Initialize);
|
||||
}
|
||||
|
||||
// Internal for tests
|
||||
internal StateEntry State => _cache.EnsureInitialized();
|
||||
|
||||
public IEnumerable<Endpoint> FindEndpoints(RouteValuesAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
}
|
||||
|
||||
var state = State;
|
||||
|
||||
IList<OutboundMatchResult> matchResults = null;
|
||||
if (string.IsNullOrEmpty(address.RouteName))
|
||||
{
|
||||
matchResults = _allMatchesLinkGenerationTree.GetMatches(
|
||||
matchResults = state.AllMatchesLinkGenerationTree.GetMatches(
|
||||
address.ExplicitValues,
|
||||
address.AmbientValues);
|
||||
}
|
||||
else if (_namedMatchResults.TryGetValue(address.RouteName, out var namedMatchResults))
|
||||
else if (state.NamedMatches.TryGetValue(address.RouteName, out var namedMatchResults))
|
||||
{
|
||||
matchResults = namedMatchResults;
|
||||
}
|
||||
|
|
@ -74,52 +72,31 @@ namespace Microsoft.AspNetCore.Routing
|
|||
}
|
||||
}
|
||||
|
||||
private void HandleChange()
|
||||
{
|
||||
// rebuild the matches
|
||||
BuildOutboundMatches();
|
||||
|
||||
// re-register the callback as the change token is one time use only and a new change token
|
||||
// is produced every time
|
||||
ChangeToken.OnChange(
|
||||
_dataSource.GetChangeToken,
|
||||
HandleChange);
|
||||
}
|
||||
|
||||
private void BuildOutboundMatches()
|
||||
{
|
||||
// Refresh the matches in the case where a datasource's endpoints changes. The following is OK to do
|
||||
// as refresh of new endpoints happens within a lock and also these fields are not publicly accessible.
|
||||
var (allMatches, namedMatchResults) = GetOutboundMatches();
|
||||
_namedMatchResults = namedMatchResults;
|
||||
_allMatchesLinkGenerationTree = new LinkGenerationDecisionTree(allMatches);
|
||||
}
|
||||
|
||||
/// Decision tree is built using the 'required values' of actions.
|
||||
/// - When generating a url using route values, decision tree checks the explicitly supplied route values +
|
||||
/// ambient values to see if they have a match for the required-values-based-tree.
|
||||
/// - When generating a url using route name, route values for controller, action etc.might not be provided
|
||||
/// (this is expected because as a user I want to avoid writing all those and instead chose to use a
|
||||
/// routename which is quick). So since these values are not provided and might not be even in ambient
|
||||
/// values, decision tree would fail to find a match. So for this reason decision tree is not used for named
|
||||
/// matches. Instead all named matches are returned as is and the LinkGenerator uses a TemplateBinder to
|
||||
/// decide which of the matches can generate a url.
|
||||
/// For example, for a route defined like below with current ambient values like new { controller = "Home",
|
||||
/// action = "Index" }
|
||||
/// "api/orders/{id}",
|
||||
/// routeName: "OrdersApi",
|
||||
/// defaults: new { controller = "Orders", action = "GetById" },
|
||||
/// requiredValues: new { controller = "Orders", action = "GetById" },
|
||||
/// A call to GetLink("OrdersApi", new { id = "10" }) cannot generate url as neither the supplied values or
|
||||
/// current ambient values do not satisfy the decision tree that is built based on the required values.
|
||||
protected virtual (List<OutboundMatch>, Dictionary<string, List<OutboundMatchResult>>) GetOutboundMatches()
|
||||
private StateEntry Initialize(IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
var allOutboundMatches = new List<OutboundMatch>();
|
||||
var namedOutboundMatchResults = new Dictionary<string, List<OutboundMatchResult>>(
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var namedOutboundMatchResults = new Dictionary<string, List<OutboundMatchResult>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var endpoint in _dataSource.Endpoints)
|
||||
// Decision tree is built using the 'required values' of actions.
|
||||
// - When generating a url using route values, decision tree checks the explicitly supplied route values +
|
||||
// ambient values to see if they have a match for the required-values-based-tree.
|
||||
// - When generating a url using route name, route values for controller, action etc.might not be provided
|
||||
// (this is expected because as a user I want to avoid writing all those and instead chose to use a
|
||||
// routename which is quick). So since these values are not provided and might not be even in ambient
|
||||
// values, decision tree would fail to find a match. So for this reason decision tree is not used for named
|
||||
// matches. Instead all named matches are returned as is and the LinkGenerator uses a TemplateBinder to
|
||||
// decide which of the matches can generate a url.
|
||||
// For example, for a route defined like below with current ambient values like new { controller = "Home",
|
||||
// action = "Index" }
|
||||
// "api/orders/{id}",
|
||||
// routeName: "OrdersApi",
|
||||
// defaults: new { controller = "Orders", action = "GetById" },
|
||||
// requiredValues: new { controller = "Orders", action = "GetById" },
|
||||
// A call to GetLink("OrdersApi", new { id = "10" }) cannot generate url as neither the supplied values or
|
||||
// current ambient values do not satisfy the decision tree that is built based on the required values.
|
||||
for (var i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
var endpoint = endpoints[i];
|
||||
if (!(endpoint is RouteEndpoint routeEndpoint))
|
||||
{
|
||||
continue;
|
||||
|
|
@ -157,7 +134,10 @@ namespace Microsoft.AspNetCore.Routing
|
|||
matchResults.Add(new OutboundMatchResult(outboundMatch, isFallbackMatch: false));
|
||||
}
|
||||
|
||||
return (allOutboundMatches, namedOutboundMatchResults);
|
||||
return new StateEntry(
|
||||
allOutboundMatches,
|
||||
new LinkGenerationDecisionTree(allOutboundMatches),
|
||||
namedOutboundMatchResults);
|
||||
}
|
||||
|
||||
private OutboundRouteEntry CreateOutboundRouteEntry(
|
||||
|
|
@ -178,5 +158,28 @@ namespace Microsoft.AspNetCore.Routing
|
|||
entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults);
|
||||
return entry;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cache.Dispose();
|
||||
}
|
||||
|
||||
internal class StateEntry
|
||||
{
|
||||
// For testing
|
||||
public readonly List<OutboundMatch> AllMatches;
|
||||
public readonly LinkGenerationDecisionTree AllMatchesLinkGenerationTree;
|
||||
public readonly Dictionary<string, List<OutboundMatchResult>> NamedMatches;
|
||||
|
||||
public StateEntry(
|
||||
List<OutboundMatch> allMatches,
|
||||
LinkGenerationDecisionTree allMatchesLinkGenerationTree,
|
||||
Dictionary<string, List<OutboundMatchResult>> namedMatches)
|
||||
{
|
||||
AllMatches = allMatches;
|
||||
AllMatchesLinkGenerationTree = allMatchesLinkGenerationTree;
|
||||
NamedMatches = namedMatches;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -76,5 +76,50 @@ namespace Microsoft.AspNetCore.Routing
|
|||
Assert.Equal(2, count);
|
||||
Assert.Equal("hello, 2!", cache.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cache_CanDispose_WhenUninitialized()
|
||||
{
|
||||
// Arrange
|
||||
var count = 0;
|
||||
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var cache = new DataSourceDependentCache<string>(dataSource, (endpoints) =>
|
||||
{
|
||||
count++;
|
||||
return $"hello, {count}!";
|
||||
});
|
||||
|
||||
// Act
|
||||
cache.Dispose();
|
||||
|
||||
// Assert
|
||||
dataSource.AddEndpoint(null);
|
||||
Assert.Null(cache.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cache_CanDispose_WhenInitialized()
|
||||
{
|
||||
// Arrange
|
||||
var count = 0;
|
||||
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var cache = new DataSourceDependentCache<string>(dataSource, (endpoints) =>
|
||||
{
|
||||
count++;
|
||||
return $"hello, {count}!";
|
||||
});
|
||||
|
||||
cache.EnsureInitialized();
|
||||
Assert.Equal("hello, 1!", cache.Value);
|
||||
|
||||
// Act
|
||||
cache.Dispose();
|
||||
|
||||
// Assert
|
||||
dataSource.AddEndpoint(null);
|
||||
Assert.Equal("hello, 1!", cache.Value); // Ignores update
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -18,13 +18,16 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
{
|
||||
// Arrange
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var lifetime = new DataSourceDependentMatcher.Lifetime();
|
||||
|
||||
// Act
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, TestMatcherBuilder.Create);
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create);
|
||||
|
||||
// Assert
|
||||
var inner = Assert.IsType<TestMatcher>(matcher.CurrentMatcher);
|
||||
Assert.Empty(inner.Endpoints);
|
||||
|
||||
Assert.NotNull(lifetime.Cache);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -32,7 +35,8 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
{
|
||||
// Arrange
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, TestMatcherBuilder.Create);
|
||||
var lifetime = new DataSourceDependentMatcher.Lifetime();
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create);
|
||||
|
||||
var endpoint = new RouteEndpoint(
|
||||
TestConstants.EmptyRequestDelegate,
|
||||
|
|
@ -51,16 +55,42 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
e => Assert.Same(endpoint, e));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Matcher_IgnoresUpdate_WhenDisposed()
|
||||
{
|
||||
// Arrange
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var lifetime = new DataSourceDependentMatcher.Lifetime();
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create);
|
||||
|
||||
var endpoint = new RouteEndpoint(
|
||||
TestConstants.EmptyRequestDelegate,
|
||||
RoutePatternFactory.Parse("a/b/c"),
|
||||
0,
|
||||
EndpointMetadataCollection.Empty,
|
||||
"test");
|
||||
|
||||
lifetime.Dispose();
|
||||
|
||||
// Act
|
||||
dataSource.AddEndpoint(endpoint);
|
||||
|
||||
// Assert
|
||||
var inner = Assert.IsType<TestMatcher>(matcher.CurrentMatcher);
|
||||
Assert.Empty(inner.Endpoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Matcher_Ignores_NonRouteEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var lifetime = new DataSourceDependentMatcher.Lifetime();
|
||||
var endpoint = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "test");
|
||||
dataSource.AddEndpoint(endpoint);
|
||||
|
||||
// Act
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, TestMatcherBuilder.Create);
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create);
|
||||
|
||||
// Assert
|
||||
var inner = Assert.IsType<TestMatcher>(matcher.CurrentMatcher);
|
||||
|
|
@ -72,6 +102,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
{
|
||||
// Arrange
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var lifetime = new DataSourceDependentMatcher.Lifetime();
|
||||
var endpoint = new RouteEndpoint(
|
||||
TestConstants.EmptyRequestDelegate,
|
||||
RoutePatternFactory.Parse("/"),
|
||||
|
|
@ -81,7 +112,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
dataSource.AddEndpoint(endpoint);
|
||||
|
||||
// Act
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, TestMatcherBuilder.Create);
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create);
|
||||
|
||||
// Assert
|
||||
var inner = Assert.IsType<TestMatcher>(matcher.CurrentMatcher);
|
||||
|
|
@ -93,6 +124,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
{
|
||||
// Arrange
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var lifetime = new DataSourceDependentMatcher.Lifetime();
|
||||
var endpoint = new RouteEndpoint(
|
||||
TestConstants.EmptyRequestDelegate,
|
||||
RoutePatternFactory.Parse("/"),
|
||||
|
|
@ -102,37 +134,13 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
dataSource.AddEndpoint(endpoint);
|
||||
|
||||
// Act
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, TestMatcherBuilder.Create);
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create);
|
||||
|
||||
// Assert
|
||||
var inner = Assert.IsType<TestMatcher>(matcher.CurrentMatcher);
|
||||
Assert.Same(endpoint, Assert.Single(inner.Endpoints));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cache_Reinitializes_WhenDataSourceChanges()
|
||||
{
|
||||
// Arrange
|
||||
var count = 0;
|
||||
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var cache = new DataSourceDependentCache<string>(dataSource, (endpoints) =>
|
||||
{
|
||||
count++;
|
||||
return $"hello, {count}!";
|
||||
});
|
||||
|
||||
cache.EnsureInitialized();
|
||||
Assert.Equal("hello, 1!", cache.Value);
|
||||
|
||||
// Act
|
||||
dataSource.AddEndpoint(null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, count);
|
||||
Assert.Equal("hello, 2!", cache.Value);
|
||||
}
|
||||
|
||||
private class TestMatcherBuilder : MatcherBuilder
|
||||
{
|
||||
public static Func<MatcherBuilder> Create = () => new TestMatcherBuilder();
|
||||
|
|
|
|||
|
|
@ -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.Collections.Generic;
|
||||
|
|
@ -25,10 +25,10 @@ namespace Microsoft.AspNetCore.Routing
|
|||
var addressScheme = CreateAddressScheme(endpoint1, endpoint2);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(addressScheme.AllMatches);
|
||||
Assert.Equal(2, addressScheme.AllMatches.Count());
|
||||
Assert.NotNull(addressScheme.NamedMatches);
|
||||
Assert.True(addressScheme.NamedMatches.TryGetValue("named", out var namedMatches));
|
||||
Assert.NotNull(addressScheme.State.AllMatches);
|
||||
Assert.Equal(2, addressScheme.State.AllMatches.Count());
|
||||
Assert.NotNull(addressScheme.State.NamedMatches);
|
||||
Assert.True(addressScheme.State.NamedMatches.TryGetValue("named", out var namedMatches));
|
||||
var namedMatch = Assert.Single(namedMatches);
|
||||
var actual = Assert.IsType<RouteEndpoint>(namedMatch.Match.Entry.Data);
|
||||
Assert.Same(endpoint2, actual);
|
||||
|
|
@ -46,10 +46,10 @@ namespace Microsoft.AspNetCore.Routing
|
|||
var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(addressScheme.AllMatches);
|
||||
Assert.Equal(3, addressScheme.AllMatches.Count());
|
||||
Assert.NotNull(addressScheme.NamedMatches);
|
||||
Assert.True(addressScheme.NamedMatches.TryGetValue("named", out var namedMatches));
|
||||
Assert.NotNull(addressScheme.State.AllMatches);
|
||||
Assert.Equal(3, addressScheme.State.AllMatches.Count());
|
||||
Assert.NotNull(addressScheme.State.NamedMatches);
|
||||
Assert.True(addressScheme.State.NamedMatches.TryGetValue("named", out var namedMatches));
|
||||
Assert.Equal(2, namedMatches.Count);
|
||||
Assert.Same(endpoint2, Assert.IsType<RouteEndpoint>(namedMatches[0].Match.Entry.Data));
|
||||
Assert.Same(endpoint3, Assert.IsType<RouteEndpoint>(namedMatches[1].Match.Entry.Data));
|
||||
|
|
@ -67,10 +67,10 @@ namespace Microsoft.AspNetCore.Routing
|
|||
var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(addressScheme.AllMatches);
|
||||
Assert.Equal(3, addressScheme.AllMatches.Count());
|
||||
Assert.NotNull(addressScheme.NamedMatches);
|
||||
Assert.True(addressScheme.NamedMatches.TryGetValue("named", out var namedMatches));
|
||||
Assert.NotNull(addressScheme.State.AllMatches);
|
||||
Assert.Equal(3, addressScheme.State.AllMatches.Count());
|
||||
Assert.NotNull(addressScheme.State.NamedMatches);
|
||||
Assert.True(addressScheme.State.NamedMatches.TryGetValue("named", out var namedMatches));
|
||||
Assert.Equal(2, namedMatches.Count);
|
||||
Assert.Same(endpoint2, Assert.IsType<RouteEndpoint>(namedMatches[0].Match.Entry.Data));
|
||||
Assert.Same(endpoint3, Assert.IsType<RouteEndpoint>(namedMatches[1].Match.Entry.Data));
|
||||
|
|
@ -84,11 +84,12 @@ namespace Microsoft.AspNetCore.Routing
|
|||
var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 });
|
||||
|
||||
// Act 1
|
||||
var addressScheme = new CustomRouteValuesBasedAddressScheme(new CompositeEndpointDataSource(new[] { dynamicDataSource }));
|
||||
var addressScheme = new RouteValuesAddressScheme(new CompositeEndpointDataSource(new[] { dynamicDataSource }));
|
||||
|
||||
// Assert 1
|
||||
Assert.NotNull(addressScheme.AllMatches);
|
||||
var match = Assert.Single(addressScheme.AllMatches);
|
||||
var state = addressScheme.State;
|
||||
Assert.NotNull(state.AllMatches);
|
||||
var match = Assert.Single(state.AllMatches);
|
||||
var actual = Assert.IsType<RouteEndpoint>(match.Entry.Data);
|
||||
Assert.Same(endpoint1, actual);
|
||||
|
||||
|
|
@ -99,24 +100,35 @@ namespace Microsoft.AspNetCore.Routing
|
|||
// Trigger change
|
||||
dynamicDataSource.AddEndpoint(endpoint2);
|
||||
|
||||
// Arrange 2
|
||||
var endpoint3 = CreateEndpoint("/c", routeName: "c");
|
||||
|
||||
// Act 2
|
||||
// Trigger change
|
||||
dynamicDataSource.AddEndpoint(endpoint3);
|
||||
// Assert 2
|
||||
Assert.NotSame(state, addressScheme.State);
|
||||
state = addressScheme.State;
|
||||
|
||||
// Arrange 3
|
||||
var endpoint4 = CreateEndpoint("/d", routeName: "d");
|
||||
var endpoint3 = CreateEndpoint("/c", routeName: "c");
|
||||
|
||||
// Act 3
|
||||
// Trigger change
|
||||
dynamicDataSource.AddEndpoint(endpoint4);
|
||||
dynamicDataSource.AddEndpoint(endpoint3);
|
||||
|
||||
// Assert 3
|
||||
Assert.NotNull(addressScheme.AllMatches);
|
||||
Assert.NotSame(state, addressScheme.State);
|
||||
state = addressScheme.State;
|
||||
|
||||
// Arrange 4
|
||||
var endpoint4 = CreateEndpoint("/d", routeName: "d");
|
||||
|
||||
// Act 4
|
||||
// Trigger change
|
||||
dynamicDataSource.AddEndpoint(endpoint4);
|
||||
|
||||
// Assert 4
|
||||
Assert.NotSame(state, addressScheme.State);
|
||||
state = addressScheme.State;
|
||||
|
||||
Assert.NotNull(state.AllMatches);
|
||||
Assert.Collection(
|
||||
addressScheme.AllMatches,
|
||||
state.AllMatches,
|
||||
(m) =>
|
||||
{
|
||||
actual = Assert.IsType<RouteEndpoint>(m.Entry.Data);
|
||||
|
|
@ -337,7 +349,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
var addressScheme = CreateAddressScheme(endpoint);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(addressScheme.AllMatches);
|
||||
Assert.Empty(addressScheme.State.AllMatches);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -352,17 +364,17 @@ namespace Microsoft.AspNetCore.Routing
|
|||
var addressScheme = CreateAddressScheme(endpoint);
|
||||
|
||||
// Assert
|
||||
Assert.Same(endpoint, Assert.Single(addressScheme.AllMatches).Entry.Data);
|
||||
Assert.Same(endpoint, Assert.Single(addressScheme.State.AllMatches).Entry.Data);
|
||||
}
|
||||
|
||||
private CustomRouteValuesBasedAddressScheme CreateAddressScheme(params Endpoint[] endpoints)
|
||||
private RouteValuesAddressScheme CreateAddressScheme(params Endpoint[] endpoints)
|
||||
{
|
||||
return CreateAddressScheme(new DefaultEndpointDataSource(endpoints));
|
||||
}
|
||||
|
||||
private CustomRouteValuesBasedAddressScheme CreateAddressScheme(params EndpointDataSource[] dataSources)
|
||||
private RouteValuesAddressScheme CreateAddressScheme(params EndpointDataSource[] dataSources)
|
||||
{
|
||||
return new CustomRouteValuesBasedAddressScheme(new CompositeEndpointDataSource(dataSources));
|
||||
return new RouteValuesAddressScheme(new CompositeEndpointDataSource(dataSources));
|
||||
}
|
||||
|
||||
private RouteEndpoint CreateEndpoint(
|
||||
|
|
@ -391,26 +403,6 @@ namespace Microsoft.AspNetCore.Routing
|
|||
null);
|
||||
}
|
||||
|
||||
private class CustomRouteValuesBasedAddressScheme : RouteValuesAddressScheme
|
||||
{
|
||||
public CustomRouteValuesBasedAddressScheme(CompositeEndpointDataSource dataSource)
|
||||
: base(dataSource)
|
||||
{
|
||||
}
|
||||
|
||||
public IEnumerable<OutboundMatch> AllMatches { get; private set; }
|
||||
|
||||
public IDictionary<string, List<OutboundMatchResult>> NamedMatches { get; private set; }
|
||||
|
||||
protected override (List<OutboundMatch>, Dictionary<string, List<OutboundMatchResult>>) GetOutboundMatches()
|
||||
{
|
||||
var matches = base.GetOutboundMatches();
|
||||
AllMatches = matches.Item1;
|
||||
NamedMatches = matches.Item2;
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
private class EncourageLinkGenerationMetadata : ISuppressLinkGenerationMetadata
|
||||
{
|
||||
public bool SuppressLinkGeneration => false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue