Fix #6102 - Intense CPU utilization on page change (#6542)

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

(cherry picked from commit a5658a8c95)
This commit is contained in:
Ryan Nowak 2019-01-13 21:03:26 -08:00
parent cd308e7a8b
commit 3e5b37f22c
10 changed files with 275 additions and 150 deletions

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing
{
internal class DataSourceDependentCache<T> where T : class
internal sealed class DataSourceDependentCache<T> : IDisposable where T : class
{
private readonly EndpointDataSource _dataSource;
private readonly Func<IReadOnlyList<Endpoint>, T> _initializeCore;
@ -19,6 +19,9 @@ namespace Microsoft.AspNetCore.Routing
private bool _initialized;
private T _value;
private IDisposable _disposable;
private bool _disposed;
public DataSourceDependentCache(EndpointDataSource dataSource, Func<IReadOnlyList<Endpoint>, T> initialize)
{
if (dataSource == null)
@ -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;
}
}
}
}
}

View File

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

View File

@ -66,6 +66,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>();
@ -77,7 +78,6 @@ namespace Microsoft.Extensions.DependencyInjection
//
services.TryAddSingleton<EndpointSelector, DefaultEndpointSelector>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, HttpMethodMatcherPolicy>());
return services;
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing
{
internal class EndpointNameAddressScheme : IEndpointAddressScheme<string>
internal sealed 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();
}
}
}

View File

@ -1,27 +1,31 @@
// 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
{
internal class DataSourceDependentMatcher : Matcher
internal sealed class DataSourceDependentMatcher : Matcher
{
private readonly Func<MatcherBuilder> _matcherBuilderFactory;
private readonly DataSourceDependentCache<Matcher> _cache;
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 sealed 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;
}
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -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>();
});

View File

@ -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 sealed class RouteValuesAddressScheme : IEndpointAddressScheme<RouteValuesAddress>, IDisposable
{
private readonly CompositeEndpointDataSource _dataSource;
private LinkGenerationDecisionTree _allMatchesLinkGenerationTree;
private Dictionary<string, List<OutboundMatchResult>> _namedMatchResults;
private readonly DataSourceDependentCache<StateEntry> _cache;
public RouteValuesAddressScheme(CompositeEndpointDataSource 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;
@ -148,7 +125,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(RouteEndpoint endpoint)
@ -167,5 +147,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;
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -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
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -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();

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.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);
@ -102,21 +103,24 @@ namespace Microsoft.AspNetCore.Routing
// Arrange 2
var endpoint3 = CreateEndpoint("/c");
// Act 2
// Act 3
// Trigger change
dynamicDataSource.AddEndpoint(endpoint3);
// Arrange 3
var endpoint4 = CreateEndpoint("/d");
// Act 3
// Act 4
// Trigger change
dynamicDataSource.AddEndpoint(endpoint4);
// Assert 3
Assert.NotNull(addressScheme.AllMatches);
// 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);
@ -293,7 +297,7 @@ namespace Microsoft.AspNetCore.Routing
var addressScheme = CreateAddressScheme(endpoint);
// Assert
Assert.Empty(addressScheme.AllMatches);
Assert.Empty(addressScheme.State.AllMatches);
}
[Fact]
@ -308,17 +312,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(
@ -347,26 +351,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;