diff --git a/src/Microsoft.AspNetCore.Dispatcher/CompositeDispatcherDataSource.cs b/src/Microsoft.AspNetCore.Dispatcher/CompositeDispatcherDataSource.cs
index 336b01558c..9925d8a188 100644
--- a/src/Microsoft.AspNetCore.Dispatcher/CompositeDispatcherDataSource.cs
+++ b/src/Microsoft.AspNetCore.Dispatcher/CompositeDispatcherDataSource.cs
@@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Dispatcher
public override IChangeToken ChangeToken { get; }
- protected override IReadOnlyList
GetAddesses()
+ protected override IReadOnlyList GetAddresses()
{
var addresses = new List();
for (var i = 0; i < _dataSources.Length; i++)
diff --git a/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherDataSource.cs b/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherDataSource.cs
index 5d36971ad3..2f279122d2 100644
--- a/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherDataSource.cs
+++ b/src/Microsoft.AspNetCore.Dispatcher/DefaultDispatcherDataSource.cs
@@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Dispatcher
public IList Endpoints => _endpoints;
- protected override IReadOnlyList GetAddesses() => _addresses;
+ protected override IReadOnlyList GetAddresses() => _addresses;
protected override IReadOnlyList GetEndpoints() => _endpoints;
}
diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherDataSource.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherDataSource.cs
index 2767e408dc..b04149bf09 100644
--- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherDataSource.cs
+++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherDataSource.cs
@@ -10,11 +10,11 @@ namespace Microsoft.AspNetCore.Dispatcher
{
public abstract IChangeToken ChangeToken { get; }
- protected abstract IReadOnlyList GetAddesses();
+ protected abstract IReadOnlyList GetAddresses();
protected abstract IReadOnlyList GetEndpoints();
- IReadOnlyList IAddressCollectionProvider.Addresses => GetAddesses();
+ IReadOnlyList IAddressCollectionProvider.Addresses => GetAddresses();
IReadOnlyList IEndpointCollectionProvider.Endpoints => GetEndpoints();
}
diff --git a/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs b/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs
index 292f99ee22..8a534315ac 100644
--- a/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs
+++ b/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -12,11 +11,13 @@ using Microsoft.AspNetCore.Dispatcher.Patterns;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
+using Primitives = Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Dispatcher
{
public class TreeMatcher : MatcherBase
{
+ private bool _onChange;
private bool _dataInitialized;
private object _lock;
private Cache _cache;
@@ -30,8 +31,6 @@ namespace Microsoft.AspNetCore.Dispatcher
_initializer = CreateCache;
}
- public int Version { get; private set; }
-
public override async Task MatchAsync(MatcherContext context)
{
if (context == null)
@@ -39,7 +38,7 @@ namespace Microsoft.AspNetCore.Dispatcher
throw new ArgumentNullException(nameof(context));
}
- EnsureServicesInitialized(context);
+ EnsureTreeMatcherServicesInitialized(context);
var cache = LazyInitializer.EnsureInitialized(ref _cache, ref _dataInitialized, ref _lock, _initializer);
@@ -101,6 +100,24 @@ namespace Microsoft.AspNetCore.Dispatcher
}
}
+ private void EnsureTreeMatcherServicesInitialized(MatcherContext context)
+ {
+ EnsureServicesInitialized(context);
+ if (Volatile.Read(ref _onChange))
+ {
+ return;
+ }
+
+ lock (_lock)
+ {
+ if (!Volatile.Read(ref _onChange))
+ {
+ _onChange = true;
+ Primitives.ChangeToken.OnChange(() => ChangeToken, () => Volatile.Write(ref _dataInitialized, false));
+ }
+ }
+ }
+
private bool MatchConstraints(HttpContext httpContext, DispatcherValueCollection values, IDictionary constraints)
{
if (constraints != null)
@@ -136,8 +153,7 @@ namespace Microsoft.AspNetCore.Dispatcher
{
var endpoint = endpoints[i];
- var templateEndpoint = endpoint as IRoutePatternEndpoint;
- if (templateEndpoint == null)
+ if (!(endpoint is IRoutePatternEndpoint templateEndpoint))
{
continue;
}
diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/Tree/TreeMatcherTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Tree/TreeMatcherTest.cs
index a4d2ad88c5..d89663b77d 100644
--- a/test/Microsoft.AspNetCore.Dispatcher.Test/Tree/TreeMatcherTest.cs
+++ b/test/Microsoft.AspNetCore.Dispatcher.Test/Tree/TreeMatcherTest.cs
@@ -1,12 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
using Moq;
using Xunit;
@@ -776,6 +780,37 @@ namespace Microsoft.AspNetCore.Dispatcher
Assert.Null(context.Endpoint);
}
+ [Fact]
+ public async Task ChangeTokenTriggers_CreateCache()
+ {
+ // Arrange 1
+ var factory = new TreeMatcherFactory();
+ var dataSource = new TestDataSource(new List() { new RoutePatternEndpoint("test/{p1}", new object(), Test_Delegate, "Test"), });
+ var matcher = factory.CreateMatcher(dataSource, new List());
+ var context = CreateMatcherContext("/test/parameter1");
+
+ // Act 1
+ await matcher.MatchAsync(context);
+
+ // Assert 1
+ Assert.Equal("Test", context.Endpoint.DisplayName);
+ context.Values.TryGetValue("p1", out var value);
+ Assert.Equal("parameter1", value);
+
+ // Arrange 2
+ dataSource.UpdateEndpoints(new List() { new RoutePatternEndpoint("test2/{p2}", new object(), Test_Delegate, "Test2"), });
+ context = CreateMatcherContext("/test2/parameter2");
+
+ // Act 2
+ await matcher.MatchAsync(context);
+
+ // Assert 2
+ Assert.Equal("Test2", context.Endpoint.DisplayName);
+ Assert.False(context.Values.TryGetValue("p1", out value));
+ context.Values.TryGetValue("p2", out value);
+ Assert.Equal("parameter2", value);
+ }
+
private static MatcherContext CreateMatcherContext(string requestPath)
{
var request = new Mock(MockBehavior.Strict);
@@ -804,5 +839,66 @@ namespace Microsoft.AspNetCore.Dispatcher
{
return Task.CompletedTask;
}
+
+ public class TestDataSource : DispatcherDataSource, IAddressCollectionProvider, IEndpointCollectionProvider
+ {
+ private IList _addresses;
+ private IList _endpoints;
+ private TestChangeToken _changeToken;
+ private readonly object _lock = new object();
+
+ public TestDataSource(IList endpoints)
+ {
+ _addresses = new List();
+ _endpoints = endpoints;
+ _changeToken = new TestChangeToken();
+ }
+
+ public void UpdateEndpoints(IList endpoints)
+ {
+ lock (_lock)
+ {
+ _endpoints = endpoints;
+ // Trigger change token here
+ var previousToken = _changeToken;
+ _changeToken = new TestChangeToken();
+ previousToken.OnChange();
+ }
+ }
+
+ public override IChangeToken ChangeToken
+ {
+ get
+ {
+ lock (_lock)
+ {
+ return _changeToken;
+ }
+ }
+ }
+
+ IReadOnlyList IAddressCollectionProvider.Addresses => GetAddresses();
+
+ IReadOnlyList IEndpointCollectionProvider.Endpoints => GetEndpoints();
+
+ protected override IReadOnlyList GetAddresses() => _addresses.ToList();
+
+ protected override IReadOnlyList GetEndpoints() => _endpoints.ToList();
+
+ public IList Endpoints => _endpoints;
+
+ private class TestChangeToken : IChangeToken
+ {
+ private CancellationTokenSource _cts = new CancellationTokenSource();
+
+ public bool HasChanged => true;
+
+ public bool ActiveChangeCallbacks => true;
+
+ public IDisposable RegisterChangeCallback(Action