Merge branch 'merge/release/2.2-to-master'
This commit is contained in:
commit
28d278cff5
|
|
@ -2,18 +2,21 @@
|
||||||
// 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.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
namespace Microsoft.AspNetCore.Routing
|
||||||
{
|
{
|
||||||
public interface ILinkGenerator
|
public abstract class LinkGenerator
|
||||||
{
|
{
|
||||||
bool TryGetLink(
|
public abstract bool TryGetLink(
|
||||||
|
HttpContext httpContext,
|
||||||
IEnumerable<Endpoint> endpoints,
|
IEnumerable<Endpoint> endpoints,
|
||||||
RouteValueDictionary explicitValues,
|
RouteValueDictionary explicitValues,
|
||||||
RouteValueDictionary ambientValues,
|
RouteValueDictionary ambientValues,
|
||||||
out string link);
|
out string link);
|
||||||
|
|
||||||
string GetLink(
|
public abstract string GetLink(
|
||||||
|
HttpContext httpContext,
|
||||||
IEnumerable<Endpoint> endpoints,
|
IEnumerable<Endpoint> endpoints,
|
||||||
RouteValueDictionary explicitValues,
|
RouteValueDictionary explicitValues,
|
||||||
RouteValueDictionary ambientValues);
|
RouteValueDictionary ambientValues);
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
@ -12,9 +13,9 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
{
|
{
|
||||||
private readonly EndpointDataSource[] _dataSources;
|
private readonly EndpointDataSource[] _dataSources;
|
||||||
private readonly object _lock;
|
private readonly object _lock;
|
||||||
|
|
||||||
private IChangeToken _changeToken;
|
|
||||||
private IReadOnlyList<Endpoint> _endpoints;
|
private IReadOnlyList<Endpoint> _endpoints;
|
||||||
|
private IChangeToken _consumerChangeToken;
|
||||||
|
private CancellationTokenSource _cts;
|
||||||
|
|
||||||
internal CompositeEndpointDataSource(IEnumerable<EndpointDataSource> dataSources)
|
internal CompositeEndpointDataSource(IEnumerable<EndpointDataSource> dataSources)
|
||||||
{
|
{
|
||||||
|
|
@ -23,6 +24,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
throw new ArgumentNullException(nameof(dataSources));
|
throw new ArgumentNullException(nameof(dataSources));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CreateChangeToken();
|
||||||
_dataSources = dataSources.ToArray();
|
_dataSources = dataSources.ToArray();
|
||||||
_lock = new object();
|
_lock = new object();
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +34,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
return _changeToken;
|
return _consumerChangeToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,7 +50,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
// Defer initialization to avoid doing lots of reflection on startup.
|
// Defer initialization to avoid doing lots of reflection on startup.
|
||||||
private void EnsureInitialized()
|
private void EnsureInitialized()
|
||||||
{
|
{
|
||||||
if (_changeToken == null)
|
if (_endpoints == null)
|
||||||
{
|
{
|
||||||
Initialize();
|
Initialize();
|
||||||
}
|
}
|
||||||
|
|
@ -60,11 +62,49 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_changeToken = new CompositeChangeToken(_dataSources.Select(d => d.ChangeToken).ToArray());
|
if (_endpoints == null)
|
||||||
|
{
|
||||||
|
_endpoints = _dataSources.SelectMany(d => d.Endpoints).ToArray();
|
||||||
|
|
||||||
|
foreach (var dataSource in _dataSources)
|
||||||
|
{
|
||||||
|
Extensions.Primitives.ChangeToken.OnChange(
|
||||||
|
() => dataSource.ChangeToken,
|
||||||
|
() => HandleChange());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleChange()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
// Refresh the endpoints from datasource so that callbacks can get the latest endpoints
|
||||||
_endpoints = _dataSources.SelectMany(d => d.Endpoints).ToArray();
|
_endpoints = _dataSources.SelectMany(d => d.Endpoints).ToArray();
|
||||||
|
|
||||||
_changeToken.RegisterChangeCallback((state) => Initialize(), null);
|
// Prevent consumers from re-registering callback to inflight events as that can
|
||||||
|
// cause a stackoverflow
|
||||||
|
// Example:
|
||||||
|
// 1. B registers A
|
||||||
|
// 2. A fires event causing B's callback to get called
|
||||||
|
// 3. B executes some code in its callback, but needs to re-register callback
|
||||||
|
// in the same callback
|
||||||
|
var oldTokenSource = _cts;
|
||||||
|
var oldToken = _consumerChangeToken;
|
||||||
|
|
||||||
|
CreateChangeToken();
|
||||||
|
|
||||||
|
// Raise consumer callbacks. Any new callback registration would happen on the new token
|
||||||
|
// created in earlier step.
|
||||||
|
oldTokenSource.Cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CreateChangeToken()
|
||||||
|
{
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
_consumerChangeToken = new CancellationChangeToken(_cts.Token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
using Microsoft.AspNetCore.Routing.Matchers;
|
using Microsoft.AspNetCore.Routing.Matchers;
|
||||||
using Microsoft.AspNetCore.Routing.Template;
|
using Microsoft.AspNetCore.Routing.Template;
|
||||||
|
|
@ -13,7 +14,7 @@ using Microsoft.Extensions.ObjectPool;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing
|
namespace Microsoft.AspNetCore.Routing
|
||||||
{
|
{
|
||||||
internal class DefaultLinkGenerator : ILinkGenerator
|
internal class DefaultLinkGenerator : LinkGenerator
|
||||||
{
|
{
|
||||||
private readonly MatchProcessorFactory _matchProcessorFactory;
|
private readonly MatchProcessorFactory _matchProcessorFactory;
|
||||||
private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool;
|
private readonly ObjectPool<UriBuildingContext> _uriBuildingContextPool;
|
||||||
|
|
@ -29,12 +30,13 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetLink(
|
public override string GetLink(
|
||||||
|
HttpContext httpContext,
|
||||||
IEnumerable<Endpoint> endpoints,
|
IEnumerable<Endpoint> endpoints,
|
||||||
RouteValueDictionary explicitValues,
|
RouteValueDictionary explicitValues,
|
||||||
RouteValueDictionary ambientValues)
|
RouteValueDictionary ambientValues)
|
||||||
{
|
{
|
||||||
if (TryGetLink(endpoints, explicitValues, ambientValues, out var link))
|
if (TryGetLink(httpContext, endpoints, explicitValues, ambientValues, out var link))
|
||||||
{
|
{
|
||||||
return link;
|
return link;
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +44,8 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
throw new InvalidOperationException("Could not find a matching endpoint to generate a link.");
|
throw new InvalidOperationException("Could not find a matching endpoint to generate a link.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetLink(
|
public override bool TryGetLink(
|
||||||
|
HttpContext httpContext,
|
||||||
IEnumerable<Endpoint> endpoints,
|
IEnumerable<Endpoint> endpoints,
|
||||||
RouteValueDictionary explicitValues,
|
RouteValueDictionary explicitValues,
|
||||||
RouteValueDictionary ambientValues,
|
RouteValueDictionary ambientValues,
|
||||||
|
|
@ -64,7 +67,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
foreach (var endpoint in matcherEndpoints)
|
foreach (var endpoint in matcherEndpoints)
|
||||||
{
|
{
|
||||||
link = GetLink(endpoint, explicitValues, ambientValues);
|
link = GetLink(httpContext, endpoint, explicitValues, ambientValues);
|
||||||
if (link != null)
|
if (link != null)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -75,6 +78,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetLink(
|
private string GetLink(
|
||||||
|
HttpContext httpContext,
|
||||||
MatcherEndpoint endpoint,
|
MatcherEndpoint endpoint,
|
||||||
RouteValueDictionary explicitValues,
|
RouteValueDictionary explicitValues,
|
||||||
RouteValueDictionary ambientValues)
|
RouteValueDictionary ambientValues)
|
||||||
|
|
@ -92,7 +96,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Match(endpoint, templateValuesResult.CombinedValues))
|
if (!Match(httpContext, endpoint, templateValuesResult.CombinedValues))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +104,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
return templateBinder.BindValues(templateValuesResult.AcceptedValues);
|
return templateBinder.BindValues(templateValuesResult.AcceptedValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool Match(MatcherEndpoint endpoint, RouteValueDictionary routeValues)
|
private bool Match(HttpContext httpContext, MatcherEndpoint endpoint, RouteValueDictionary routeValues)
|
||||||
{
|
{
|
||||||
if (routeValues == null)
|
if (routeValues == null)
|
||||||
{
|
{
|
||||||
|
|
@ -117,7 +121,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
}
|
}
|
||||||
|
|
||||||
var matchProcessor = _matchProcessorFactory.Create(matchProcessorReference);
|
var matchProcessor = _matchProcessorFactory.Create(matchProcessorReference);
|
||||||
if (!matchProcessor.ProcessOutbound(httpContext: null, routeValues))
|
if (!matchProcessor.ProcessOutbound(httpContext, routeValues))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
// Link generation related services
|
// Link generation related services
|
||||||
services.TryAddSingleton<IEndpointFinder<string>, NameBasedEndpointFinder>();
|
services.TryAddSingleton<IEndpointFinder<string>, NameBasedEndpointFinder>();
|
||||||
services.TryAddSingleton<IEndpointFinder<RouteValuesBasedEndpointFinderContext>, RouteValuesBasedEndpointFinder>();
|
services.TryAddSingleton<IEndpointFinder<RouteValuesBasedEndpointFinderContext>, RouteValuesBasedEndpointFinder>();
|
||||||
services.TryAddSingleton<ILinkGenerator, DefaultLinkGenerator>();
|
services.TryAddSingleton<LinkGenerator, DefaultLinkGenerator>();
|
||||||
//
|
//
|
||||||
// Endpoint Selection
|
// Endpoint Selection
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,13 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
_endpointDataSource = endpointDataSource;
|
_endpointDataSource = endpointDataSource;
|
||||||
_objectPool = objectPool;
|
_objectPool = objectPool;
|
||||||
|
|
||||||
|
// Build initial matches
|
||||||
BuildOutboundMatches();
|
BuildOutboundMatches();
|
||||||
|
|
||||||
|
// Register for changes in endpoints
|
||||||
|
Extensions.Primitives.ChangeToken.OnChange(
|
||||||
|
() => _endpointDataSource.ChangeToken,
|
||||||
|
() => HandleChange());
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Endpoint> FindEndpoints(RouteValuesBasedEndpointFinderContext context)
|
public IEnumerable<Endpoint> FindEndpoints(RouteValuesBasedEndpointFinderContext context)
|
||||||
|
|
@ -56,14 +62,28 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
.Select(match => (MatcherEndpoint)match.Entry.Data);
|
.Select(match => (MatcherEndpoint)match.Entry.Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildOutboundMatches()
|
private void HandleChange()
|
||||||
{
|
{
|
||||||
var (allOutboundMatches, namedOutboundMatches) = GetOutboundMatches();
|
// rebuild the matches
|
||||||
_namedMatches = GetNamedMatches(namedOutboundMatches);
|
BuildOutboundMatches();
|
||||||
_allMatchesLinkGenerationTree = new LinkGenerationDecisionTree(allOutboundMatches.ToArray());
|
|
||||||
|
// re-register the callback as the change token is one time use only and a new change token
|
||||||
|
// is produced every time
|
||||||
|
Extensions.Primitives.ChangeToken.OnChange(
|
||||||
|
() => _endpointDataSource.ChangeToken,
|
||||||
|
() => HandleChange());
|
||||||
}
|
}
|
||||||
|
|
||||||
private (IEnumerable<OutboundMatch>, IDictionary<string, List<OutboundMatch>>) GetOutboundMatches()
|
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, namedMatches) = GetOutboundMatches();
|
||||||
|
_namedMatches = GetNamedMatches(namedMatches);
|
||||||
|
_allMatchesLinkGenerationTree = new LinkGenerationDecisionTree(allMatches.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual (IEnumerable<OutboundMatch>, IDictionary<string, List<OutboundMatch>>) GetOutboundMatches()
|
||||||
{
|
{
|
||||||
var allOutboundMatches = new List<OutboundMatch>();
|
var allOutboundMatches = new List<OutboundMatch>();
|
||||||
var namedOutboundMatches = new Dictionary<string, List<OutboundMatch>>(StringComparer.OrdinalIgnoreCase);
|
var namedOutboundMatches = new Dictionary<string, List<OutboundMatch>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
// 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;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Routing.Matchers;
|
||||||
|
using Microsoft.AspNetCore.Routing.TestObjects;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
{
|
||||||
|
public class CompositeEndpointDataSourceTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CreatesShallowCopyOf_ListOfEndpoints()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint1 = CreateEndpoint("/a");
|
||||||
|
var endpoint2 = CreateEndpoint("/b");
|
||||||
|
var dataSource = new DefaultEndpointDataSource(new Endpoint[] { endpoint1, endpoint2 });
|
||||||
|
var compositeDataSource = new CompositeEndpointDataSource(new[] { dataSource });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var endpoints = compositeDataSource.Endpoints;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotSame(endpoints, dataSource.Endpoints);
|
||||||
|
Assert.Equal(endpoints, dataSource.Endpoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Endpoints_ReturnsAllEndpoints_FromMultipleDataSources()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint1 = CreateEndpoint("/a");
|
||||||
|
var endpoint2 = CreateEndpoint("/b");
|
||||||
|
var endpoint3 = CreateEndpoint("/c");
|
||||||
|
var endpoint4 = CreateEndpoint("/d");
|
||||||
|
var endpoint5 = CreateEndpoint("/e");
|
||||||
|
var compositeDataSource = new CompositeEndpointDataSource(new[]
|
||||||
|
{
|
||||||
|
new DefaultEndpointDataSource(new Endpoint[] { endpoint1, endpoint2 }),
|
||||||
|
new DefaultEndpointDataSource(new Endpoint[] { endpoint3, endpoint4 }),
|
||||||
|
new DefaultEndpointDataSource(new Endpoint[] { endpoint5 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var endpoints = compositeDataSource.Endpoints;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Collection(
|
||||||
|
endpoints,
|
||||||
|
(ep) => Assert.Same(endpoint1, ep),
|
||||||
|
(ep) => Assert.Same(endpoint2, ep),
|
||||||
|
(ep) => Assert.Same(endpoint3, ep),
|
||||||
|
(ep) => Assert.Same(endpoint4, ep),
|
||||||
|
(ep) => Assert.Same(endpoint5, ep));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DataSourceChanges_AreReflected_InEndpoints()
|
||||||
|
{
|
||||||
|
// Arrange1
|
||||||
|
var endpoint1 = CreateEndpoint("/a");
|
||||||
|
var dataSource1 = new DynamicEndpointDataSource(endpoint1);
|
||||||
|
var compositeDataSource = new CompositeEndpointDataSource(new[] { dataSource1 });
|
||||||
|
|
||||||
|
// Act1
|
||||||
|
var endpoints = compositeDataSource.Endpoints;
|
||||||
|
|
||||||
|
// Assert1
|
||||||
|
var endpoint = Assert.Single(endpoints);
|
||||||
|
Assert.Same(endpoint1, endpoint);
|
||||||
|
|
||||||
|
// Arrange2
|
||||||
|
var endpoint2 = CreateEndpoint("/b");
|
||||||
|
|
||||||
|
// Act2
|
||||||
|
dataSource1.AddEndpoint(endpoint2);
|
||||||
|
|
||||||
|
// Assert2
|
||||||
|
Assert.Collection(
|
||||||
|
compositeDataSource.Endpoints,
|
||||||
|
(ep) => Assert.Same(endpoint1, ep),
|
||||||
|
(ep) => Assert.Same(endpoint2, ep));
|
||||||
|
|
||||||
|
// Arrange3
|
||||||
|
var endpoint3 = CreateEndpoint("/c");
|
||||||
|
|
||||||
|
// Act2
|
||||||
|
dataSource1.AddEndpoint(endpoint3);
|
||||||
|
|
||||||
|
// Assert2
|
||||||
|
Assert.Collection(
|
||||||
|
compositeDataSource.Endpoints,
|
||||||
|
(ep) => Assert.Same(endpoint1, ep),
|
||||||
|
(ep) => Assert.Same(endpoint2, ep),
|
||||||
|
(ep) => Assert.Same(endpoint3, ep));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConsumerChangeToken_IsRefreshed_WhenDataSourceCallbackFires()
|
||||||
|
{
|
||||||
|
// Arrange1
|
||||||
|
var endpoint1 = CreateEndpoint("/a");
|
||||||
|
var dataSource1 = new DynamicEndpointDataSource(endpoint1);
|
||||||
|
var compositeDataSource = new CompositeEndpointDataSource(new[] { dataSource1 });
|
||||||
|
|
||||||
|
// Act1
|
||||||
|
var endpoints = compositeDataSource.Endpoints;
|
||||||
|
|
||||||
|
// Assert1
|
||||||
|
var changeToken1 = compositeDataSource.ChangeToken;
|
||||||
|
var token = Assert.IsType<CancellationChangeToken>(changeToken1);
|
||||||
|
Assert.False(token.HasChanged); // initial state
|
||||||
|
|
||||||
|
// Arrange2
|
||||||
|
var endpoint2 = CreateEndpoint("/b");
|
||||||
|
|
||||||
|
// Act2
|
||||||
|
dataSource1.AddEndpoint(endpoint2);
|
||||||
|
|
||||||
|
// Assert2
|
||||||
|
Assert.True(changeToken1.HasChanged); // old token is expected to be changed
|
||||||
|
var changeToken2 = compositeDataSource.ChangeToken; // new token is in a unchanged state
|
||||||
|
Assert.NotSame(changeToken2, changeToken1);
|
||||||
|
token = Assert.IsType<CancellationChangeToken>(changeToken2);
|
||||||
|
Assert.False(token.HasChanged);
|
||||||
|
|
||||||
|
// Arrange3
|
||||||
|
var endpoint3 = CreateEndpoint("/c");
|
||||||
|
|
||||||
|
// Act2
|
||||||
|
dataSource1.AddEndpoint(endpoint3);
|
||||||
|
|
||||||
|
// Assert2
|
||||||
|
Assert.True(changeToken2.HasChanged); // old token is expected to be changed
|
||||||
|
var changeToken3 = compositeDataSource.ChangeToken; // new token is in a unchanged state
|
||||||
|
Assert.NotSame(changeToken3, changeToken2);
|
||||||
|
Assert.NotSame(changeToken3, changeToken1);
|
||||||
|
token = Assert.IsType<CancellationChangeToken>(changeToken3);
|
||||||
|
Assert.False(token.HasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MatcherEndpoint CreateEndpoint(
|
||||||
|
string template,
|
||||||
|
object defaultValues = null,
|
||||||
|
object requiredValues = null,
|
||||||
|
int order = 0,
|
||||||
|
string routeName = null)
|
||||||
|
{
|
||||||
|
var defaults = defaultValues == null ? new RouteValueDictionary() : new RouteValueDictionary(defaultValues);
|
||||||
|
var required = requiredValues == null ? new RouteValueDictionary() : new RouteValueDictionary(requiredValues);
|
||||||
|
|
||||||
|
return new MatcherEndpoint(
|
||||||
|
next => (httpContext) => Task.CompletedTask,
|
||||||
|
template,
|
||||||
|
defaults,
|
||||||
|
required,
|
||||||
|
order,
|
||||||
|
EndpointMetadataCollection.Empty,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomEndpointDataSource : EndpointDataSource
|
||||||
|
{
|
||||||
|
private readonly CancellationTokenSource _cts;
|
||||||
|
private readonly CancellationChangeToken _token;
|
||||||
|
|
||||||
|
public CustomEndpointDataSource()
|
||||||
|
{
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
_token = new CancellationChangeToken(_cts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IChangeToken ChangeToken => _token;
|
||||||
|
public override IReadOnlyList<Endpoint> Endpoints => Array.Empty<Endpoint>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,6 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Routing.EndpointFinders;
|
using Microsoft.AspNetCore.Routing.EndpointFinders;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
using Microsoft.AspNetCore.Routing.Matchers;
|
using Microsoft.AspNetCore.Routing.Matchers;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.ObjectPool;
|
using Microsoft.Extensions.ObjectPool;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
@ -27,7 +26,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
var context = CreateRouteValuesContext(new { controller = "Home" });
|
var context = CreateRouteValuesContext(new { controller = "Home" });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues);
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("/Home", link);
|
Assert.Equal("/Home", link);
|
||||||
|
|
@ -44,7 +47,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var exception = Assert.Throws<InvalidOperationException>(
|
var exception = Assert.Throws<InvalidOperationException>(
|
||||||
() => linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues));
|
() => linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues));
|
||||||
Assert.Equal(expectedMessage, exception.Message);
|
Assert.Equal(expectedMessage, exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,6 +65,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var canGenerateLink = linkGenerator.TryGetLink(
|
var canGenerateLink = linkGenerator.TryGetLink(
|
||||||
|
httpContext: null,
|
||||||
new[] { endpoint },
|
new[] { endpoint },
|
||||||
context.ExplicitValues,
|
context.ExplicitValues,
|
||||||
context.AmbientValues,
|
context.AmbientValues,
|
||||||
|
|
@ -80,6 +88,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
new[] { endpoint1, endpoint2, endpoint3 },
|
new[] { endpoint1, endpoint2, endpoint3 },
|
||||||
context.ExplicitValues,
|
context.ExplicitValues,
|
||||||
context.AmbientValues);
|
context.AmbientValues);
|
||||||
|
|
@ -100,6 +109,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
new[] { endpoint1, endpoint2, endpoint3 },
|
new[] { endpoint1, endpoint2, endpoint3 },
|
||||||
context.ExplicitValues,
|
context.ExplicitValues,
|
||||||
context.AmbientValues);
|
context.AmbientValues);
|
||||||
|
|
@ -119,7 +129,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
ambientValues: new { controller = "Home", action = "Index" });
|
ambientValues: new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues);
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("/Home/Index?name=name%20with%20%25special%20%23characters", link);
|
Assert.Equal("/Home/Index?name=name%20with%20%25special%20%23characters", link);
|
||||||
|
|
@ -136,7 +150,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
new { controller = "Home", action = "Index" });
|
new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues);
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("/Home/Index?color=red&color=green&color=blue", link);
|
Assert.Equal("/Home/Index?color=red&color=green&color=blue", link);
|
||||||
|
|
@ -153,7 +171,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
new { controller = "Home", action = "Index" });
|
new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues);
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("/Home/Index?items=10&items=20&items=30", link);
|
Assert.Equal("/Home/Index?items=10&items=20&items=30", link);
|
||||||
|
|
@ -170,7 +192,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
new { controller = "Home", action = "Index" });
|
new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues);
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("/Home/Index", link);
|
Assert.Equal("/Home/Index", link);
|
||||||
|
|
@ -187,7 +213,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
new { controller = "Home", action = "Index" });
|
new { controller = "Home", action = "Index" });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues);
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("/Home/Index?page=1&color=red&color=green&color=blue&message=textfortest", link);
|
Assert.Equal("/Home/Index?page=1&color=red&color=green&color=blue&message=textfortest", link);
|
||||||
|
|
@ -204,7 +234,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
ambientValues: new { controller = "Home" });
|
ambientValues: new { controller = "Home" });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues);
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("/Home/Index", link);
|
Assert.Equal("/Home/Index", link);
|
||||||
|
|
@ -582,7 +616,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
suppliedValues: new { action = "Index", controller = "Home", name = "products" });
|
suppliedValues: new { action = "Index", controller = "Home", name = "products" });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues);
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("/Home/Index/products", link);
|
Assert.Equal("/Home/Index/products", link);
|
||||||
|
|
@ -598,7 +636,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
suppliedValues: new { action = "Index", controller = "Home" });
|
suppliedValues: new { action = "Index", controller = "Home" });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues);
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("/Home/Index", link);
|
Assert.Equal("/Home/Index", link);
|
||||||
|
|
@ -616,7 +658,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
suppliedValues: new { action = "Index", controller = "Home", name = "products" });
|
suppliedValues: new { action = "Index", controller = "Home", name = "products" });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues);
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("/Home/Index/products", link);
|
Assert.Equal("/Home/Index/products", link);
|
||||||
|
|
@ -634,7 +680,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
suppliedValues: new { action = "Index", controller = "Home" });
|
suppliedValues: new { action = "Index", controller = "Home" });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues);
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("/Home/Index", link);
|
Assert.Equal("/Home/Index", link);
|
||||||
|
|
@ -650,7 +700,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
suppliedValues: new { action = "Index", controller = "Home", name = "products", format = "json" });
|
suppliedValues: new { action = "Index", controller = "Home", name = "products", format = "json" });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues);
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("/Home/Index/products?format=json", link);
|
Assert.Equal("/Home/Index/products?format=json", link);
|
||||||
|
|
@ -710,7 +764,11 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
suppliedValues: new { action = "Index", controller = "Home" });
|
suppliedValues: new { action = "Index", controller = "Home" });
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var link = linkGenerator.GetLink(new[] { endpoint }, context.ExplicitValues, context.AmbientValues);
|
var link = linkGenerator.GetLink(
|
||||||
|
httpContext: null,
|
||||||
|
new[] { endpoint },
|
||||||
|
context.ExplicitValues,
|
||||||
|
context.AmbientValues);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("/Home/Index", link);
|
Assert.Equal("/Home/Index", link);
|
||||||
|
|
@ -796,7 +854,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ILinkGenerator CreateLinkGenerator()
|
private LinkGenerator CreateLinkGenerator()
|
||||||
{
|
{
|
||||||
return new DefaultLinkGenerator(
|
return new DefaultLinkGenerator(
|
||||||
new DefaultMatchProcessorFactory(
|
new DefaultMatchProcessorFactory(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
// 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;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
|
using Microsoft.AspNetCore.Routing.Matchers;
|
||||||
|
using Microsoft.AspNetCore.Routing.TestObjects;
|
||||||
|
using Microsoft.AspNetCore.Routing.Tree;
|
||||||
|
using Microsoft.Extensions.ObjectPool;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
{
|
||||||
|
public class RouteValueBasedEndpointFinderTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetOutboundMatches_GetsNamedMatchesFor_EndpointsHaving_IRouteNameMetadata()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint1 = CreateEndpoint("/a");
|
||||||
|
var endpoint2 = CreateEndpoint("/a", routeName: "named");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var finder = CreateEndpointFinder(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(finder.AllMatches);
|
||||||
|
Assert.Equal(2, finder.AllMatches.Count());
|
||||||
|
Assert.NotNull(finder.NamedMatches);
|
||||||
|
Assert.True(finder.NamedMatches.TryGetValue("named", out var namedMatches));
|
||||||
|
var namedMatch = Assert.Single(namedMatches);
|
||||||
|
var actual = Assert.IsType<MatcherEndpoint>(namedMatch.Entry.Data);
|
||||||
|
Assert.Same(endpoint2, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetOutboundMatches_GroupsMultipleEndpoints_WithSameName()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint1 = CreateEndpoint("/a");
|
||||||
|
var endpoint2 = CreateEndpoint("/a", routeName: "named");
|
||||||
|
var endpoint3 = CreateEndpoint("/b", routeName: "named");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var finder = CreateEndpointFinder(endpoint1, endpoint2, endpoint3);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(finder.AllMatches);
|
||||||
|
Assert.Equal(3, finder.AllMatches.Count());
|
||||||
|
Assert.NotNull(finder.NamedMatches);
|
||||||
|
Assert.True(finder.NamedMatches.TryGetValue("named", out var namedMatches));
|
||||||
|
Assert.Equal(2, namedMatches.Count);
|
||||||
|
Assert.Same(endpoint2, Assert.IsType<MatcherEndpoint>(namedMatches[0].Entry.Data));
|
||||||
|
Assert.Same(endpoint3, Assert.IsType<MatcherEndpoint>(namedMatches[1].Entry.Data));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetOutboundMatches_GroupsMultipleEndpoints_WithSameName_IgnoringCase()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint1 = CreateEndpoint("/a");
|
||||||
|
var endpoint2 = CreateEndpoint("/a", routeName: "named");
|
||||||
|
var endpoint3 = CreateEndpoint("/b", routeName: "NaMed");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var finder = CreateEndpointFinder(endpoint1, endpoint2, endpoint3);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(finder.AllMatches);
|
||||||
|
Assert.Equal(3, finder.AllMatches.Count());
|
||||||
|
Assert.NotNull(finder.NamedMatches);
|
||||||
|
Assert.True(finder.NamedMatches.TryGetValue("named", out var namedMatches));
|
||||||
|
Assert.Equal(2, namedMatches.Count);
|
||||||
|
Assert.Same(endpoint2, Assert.IsType<MatcherEndpoint>(namedMatches[0].Entry.Data));
|
||||||
|
Assert.Same(endpoint3, Assert.IsType<MatcherEndpoint>(namedMatches[1].Entry.Data));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetOutboundMatches_DoesNotGetNamedMatchesFor_EndpointsHaving_INameMetadata()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var endpoint1 = CreateEndpoint("/a");
|
||||||
|
var endpoint2 = CreateEndpoint("/a", routeName: "named");
|
||||||
|
var endpoint3 = CreateEndpoint(
|
||||||
|
"/b",
|
||||||
|
metadataCollection: new EndpointMetadataCollection(new[] { new NameMetadata("named") }));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var finder = CreateEndpointFinder(endpoint1, endpoint2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(finder.AllMatches);
|
||||||
|
Assert.Equal(2, finder.AllMatches.Count());
|
||||||
|
Assert.NotNull(finder.NamedMatches);
|
||||||
|
Assert.True(finder.NamedMatches.TryGetValue("named", out var namedMatches));
|
||||||
|
var namedMatch = Assert.Single(namedMatches);
|
||||||
|
var actual = Assert.IsType<MatcherEndpoint>(namedMatch.Entry.Data);
|
||||||
|
Assert.Same(endpoint2, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EndpointDataSource_ChangeCallback_Refreshes_OutboundMatches()
|
||||||
|
{
|
||||||
|
// Arrange 1
|
||||||
|
var endpoint1 = CreateEndpoint("/a");
|
||||||
|
var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 });
|
||||||
|
var objectPoolProvider = new DefaultObjectPoolProvider();
|
||||||
|
var objectPool = objectPoolProvider.Create(new UriBuilderContextPooledObjectPolicy());
|
||||||
|
|
||||||
|
// Act 1
|
||||||
|
var finder = new CustomRouteValuesBasedEndpointFinder(
|
||||||
|
new CompositeEndpointDataSource(new[] { dynamicDataSource }),
|
||||||
|
objectPool);
|
||||||
|
|
||||||
|
// Assert 1
|
||||||
|
Assert.NotNull(finder.AllMatches);
|
||||||
|
var match = Assert.Single(finder.AllMatches);
|
||||||
|
var actual = Assert.IsType<MatcherEndpoint>(match.Entry.Data);
|
||||||
|
Assert.Same(endpoint1, actual);
|
||||||
|
|
||||||
|
// Arrange 2
|
||||||
|
var endpoint2 = CreateEndpoint("/b");
|
||||||
|
|
||||||
|
// Act 2
|
||||||
|
// Trigger change
|
||||||
|
dynamicDataSource.AddEndpoint(endpoint2);
|
||||||
|
|
||||||
|
// Arrange 2
|
||||||
|
var endpoint3 = CreateEndpoint("/c");
|
||||||
|
|
||||||
|
// Act 2
|
||||||
|
// Trigger change
|
||||||
|
dynamicDataSource.AddEndpoint(endpoint3);
|
||||||
|
|
||||||
|
// Arrange 3
|
||||||
|
var endpoint4 = CreateEndpoint("/d");
|
||||||
|
|
||||||
|
// Act 3
|
||||||
|
// Trigger change
|
||||||
|
dynamicDataSource.AddEndpoint(endpoint4);
|
||||||
|
|
||||||
|
// Assert 3
|
||||||
|
Assert.NotNull(finder.AllMatches);
|
||||||
|
Assert.Collection(
|
||||||
|
finder.AllMatches,
|
||||||
|
(m) =>
|
||||||
|
{
|
||||||
|
actual = Assert.IsType<MatcherEndpoint>(m.Entry.Data);
|
||||||
|
Assert.Same(endpoint1, actual);
|
||||||
|
},
|
||||||
|
(m) =>
|
||||||
|
{
|
||||||
|
actual = Assert.IsType<MatcherEndpoint>(m.Entry.Data);
|
||||||
|
Assert.Same(endpoint2, actual);
|
||||||
|
},
|
||||||
|
(m) =>
|
||||||
|
{
|
||||||
|
actual = Assert.IsType<MatcherEndpoint>(m.Entry.Data);
|
||||||
|
Assert.Same(endpoint3, actual);
|
||||||
|
},
|
||||||
|
(m) =>
|
||||||
|
{
|
||||||
|
actual = Assert.IsType<MatcherEndpoint>(m.Entry.Data);
|
||||||
|
Assert.Same(endpoint4, actual);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private CustomRouteValuesBasedEndpointFinder CreateEndpointFinder(params Endpoint[] endpoints)
|
||||||
|
{
|
||||||
|
return CreateEndpointFinder(new DefaultEndpointDataSource(endpoints));
|
||||||
|
}
|
||||||
|
|
||||||
|
private CustomRouteValuesBasedEndpointFinder CreateEndpointFinder(params EndpointDataSource[] endpointDataSources)
|
||||||
|
{
|
||||||
|
var objectPoolProvider = new DefaultObjectPoolProvider();
|
||||||
|
var objectPool = objectPoolProvider.Create(new UriBuilderContextPooledObjectPolicy());
|
||||||
|
|
||||||
|
return new CustomRouteValuesBasedEndpointFinder(
|
||||||
|
new CompositeEndpointDataSource(endpointDataSources),
|
||||||
|
objectPool);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MatcherEndpoint CreateEndpoint(
|
||||||
|
string template,
|
||||||
|
object defaultValues = null,
|
||||||
|
object requiredValues = null,
|
||||||
|
int order = 0,
|
||||||
|
string routeName = null,
|
||||||
|
EndpointMetadataCollection metadataCollection = null)
|
||||||
|
{
|
||||||
|
var defaults = defaultValues == null ? new RouteValueDictionary() : new RouteValueDictionary(defaultValues);
|
||||||
|
var required = requiredValues == null ? new RouteValueDictionary() : new RouteValueDictionary(requiredValues);
|
||||||
|
|
||||||
|
if (metadataCollection == null)
|
||||||
|
{
|
||||||
|
metadataCollection = EndpointMetadataCollection.Empty;
|
||||||
|
if (!string.IsNullOrEmpty(routeName))
|
||||||
|
{
|
||||||
|
metadataCollection = new EndpointMetadataCollection(new[] { new RouteNameMetadata(routeName) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MatcherEndpoint(
|
||||||
|
next => (httpContext) => Task.CompletedTask,
|
||||||
|
template,
|
||||||
|
defaults,
|
||||||
|
required,
|
||||||
|
order,
|
||||||
|
metadataCollection,
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RouteNameMetadata : IRouteNameMetadata
|
||||||
|
{
|
||||||
|
public RouteNameMetadata(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
public string Name { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NameMetadata : INameMetadata
|
||||||
|
{
|
||||||
|
public NameMetadata(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
public string Name { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomRouteValuesBasedEndpointFinder : RouteValuesBasedEndpointFinder
|
||||||
|
{
|
||||||
|
public CustomRouteValuesBasedEndpointFinder(
|
||||||
|
CompositeEndpointDataSource endpointDataSource,
|
||||||
|
ObjectPool<UriBuildingContext> objectPool)
|
||||||
|
: base(endpointDataSource, objectPool)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<OutboundMatch> AllMatches { get; private set; }
|
||||||
|
|
||||||
|
public IDictionary<string, List<OutboundMatch>> NamedMatches { get; private set; }
|
||||||
|
|
||||||
|
protected override (IEnumerable<OutboundMatch>, IDictionary<string, List<OutboundMatch>>) GetOutboundMatches()
|
||||||
|
{
|
||||||
|
var matches = base.GetOutboundMatches();
|
||||||
|
AllMatches = matches.Item1;
|
||||||
|
NamedMatches = matches.Item2;
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
// 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;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing.TestObjects
|
||||||
|
{
|
||||||
|
public class DynamicEndpointDataSource : EndpointDataSource
|
||||||
|
{
|
||||||
|
private readonly List<Endpoint> _endpoints;
|
||||||
|
private CancellationTokenSource _cts;
|
||||||
|
private CancellationChangeToken _changeToken;
|
||||||
|
private readonly object _lock;
|
||||||
|
|
||||||
|
public DynamicEndpointDataSource(params Endpoint[] endpoints)
|
||||||
|
{
|
||||||
|
_endpoints = new List<Endpoint>();
|
||||||
|
_endpoints.AddRange(endpoints);
|
||||||
|
_lock = new object();
|
||||||
|
|
||||||
|
CreateChangeToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IChangeToken ChangeToken => _changeToken;
|
||||||
|
|
||||||
|
public override IReadOnlyList<Endpoint> Endpoints => _endpoints;
|
||||||
|
|
||||||
|
// Trigger change
|
||||||
|
public void AddEndpoint(Endpoint endpoint)
|
||||||
|
{
|
||||||
|
_endpoints.Add(endpoint);
|
||||||
|
|
||||||
|
// Capture the old tokens so that we can raise the callbacks on them. This is important so that
|
||||||
|
// consumers do not register callbacks on an inflight event causing a stackoverflow.
|
||||||
|
var oldTokenSource = _cts;
|
||||||
|
var oldToken = _changeToken;
|
||||||
|
|
||||||
|
CreateChangeToken();
|
||||||
|
|
||||||
|
// Raise consumer callbacks. Any new callback registration would happen on the new token
|
||||||
|
// created in earlier step.
|
||||||
|
oldTokenSource.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateChangeToken()
|
||||||
|
{
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
_changeToken = new CancellationChangeToken(_cts.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue