diff --git a/src/Microsoft.AspNetCore.Routing/DataTokensMetadata.cs b/src/Microsoft.AspNetCore.Routing/DataTokensMetadata.cs
new file mode 100644
index 0000000000..99a07f038d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Routing/DataTokensMetadata.cs
@@ -0,0 +1,31 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ ///
+ /// Metadata that defines data tokens for an . This metadata
+ /// type provides data tokens value for associated
+ /// with an endpoint.
+ ///
+ public sealed class DataTokensMetadata : IDataTokensMetadata
+ {
+ public DataTokensMetadata(IReadOnlyDictionary dataTokens)
+ {
+ if (dataTokens == null)
+ {
+ throw new ArgumentNullException(nameof(dataTokens));
+ }
+
+ DataTokens = dataTokens;
+ }
+
+ ///
+ /// Get the data tokens.
+ ///
+ public IReadOnlyDictionary DataTokens { get; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Routing/EndpointFeature.cs b/src/Microsoft.AspNetCore.Routing/EndpointFeature.cs
index 396f9b00ee..c87db04900 100644
--- a/src/Microsoft.AspNetCore.Routing/EndpointFeature.cs
+++ b/src/Microsoft.AspNetCore.Routing/EndpointFeature.cs
@@ -54,7 +54,20 @@ namespace Microsoft.AspNetCore.Routing
{
if (_routeData == null)
{
- _routeData = new RouteData(_values);
+ _routeData = _values == null ? new RouteData() : new RouteData(_values);
+
+ // Note: DataTokens won't update if someone else overwrites the Endpoint
+ // after route values has been set. This seems find since endpoints are a new
+ // feature and DataTokens are for back-compat.
+ var dataTokensMetadata = Endpoint?.Metadata.GetMetadata();
+ if (dataTokensMetadata != null)
+ {
+ var dataTokens = _routeData.DataTokens;
+ foreach (var kvp in dataTokensMetadata.DataTokens)
+ {
+ _routeData.DataTokens.Add(kvp.Key, kvp.Value);
+ }
+ }
}
return _routeData;
diff --git a/src/Microsoft.AspNetCore.Routing/IDataTokenMetadata.cs b/src/Microsoft.AspNetCore.Routing/IDataTokenMetadata.cs
new file mode 100644
index 0000000000..9cd6455d6c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Routing/IDataTokenMetadata.cs
@@ -0,0 +1,20 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ ///
+ /// Metadata that defines data tokens for an . This metadata
+ /// type provides data tokens value for associated
+ /// with an endpoint.
+ ///
+ public interface IDataTokensMetadata
+ {
+ ///
+ /// Get the data tokens.
+ ///
+ IReadOnlyDictionary DataTokens { get; }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Routing.Tests/EndpointFeatureTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/EndpointFeatureTest.cs
new file mode 100644
index 0000000000..5ffd868630
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Routing.Tests/EndpointFeatureTest.cs
@@ -0,0 +1,58 @@
+// 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.Linq;
+using Microsoft.AspNetCore.Routing.Matching;
+using Microsoft.AspNetCore.Routing.Patterns;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Routing
+{
+ public class EndpointFeatureTest
+ {
+ [Fact]
+ public void RouteData_CanIntializeDataTokens_WithMetadata()
+ {
+ // Arrange
+ var expected = new RouteValueDictionary(new { foo = 17, bar = "hello", });
+
+ var feature = new EndpointFeature()
+ {
+ Endpoint = new MatcherEndpoint(
+ MatcherEndpoint.EmptyInvoker,
+ RoutePatternFactory.Parse("/"),
+ 0,
+ new EndpointMetadataCollection(new DataTokensMetadata(expected)),
+ "test"),
+ };
+
+ // Act
+ var routeData = ((IRoutingFeature)feature).RouteData;
+
+ // Assert
+ Assert.NotSame(expected, routeData.DataTokens);
+ Assert.Equal(expected.OrderBy(kvp => kvp.Key), routeData.DataTokens.OrderBy(kvp => kvp.Key));
+ }
+
+ [Fact]
+ public void RouteData_DataTokensIsEmpty_WithoutMetadata()
+ {
+ // Arrange
+ var feature = new EndpointFeature()
+ {
+ Endpoint = new MatcherEndpoint(
+ MatcherEndpoint.EmptyInvoker,
+ RoutePatternFactory.Parse("/"),
+ 0,
+ new EndpointMetadataCollection(),
+ "test"),
+ };
+
+ // Act
+ var routeData = ((IRoutingFeature)feature).RouteData;
+
+ // Assert
+ Assert.Empty(routeData.DataTokens);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Routing.Tests/EndpointRoutingMiddlewareTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/EndpointRoutingMiddlewareTest.cs
index df5c8d831c..186eb9f0e3 100644
--- a/test/Microsoft.AspNetCore.Routing.Tests/EndpointRoutingMiddlewareTest.cs
+++ b/test/Microsoft.AspNetCore.Routing.Tests/EndpointRoutingMiddlewareTest.cs
@@ -82,6 +82,30 @@ namespace Microsoft.AspNetCore.Routing
Assert.Equal("testValue", endpointFeature.Values["testKey"]);
}
+ [Fact]
+ public async Task Invoke_BackCompatGetDataTokens_ValueUsedFromEndpointMetadata()
+ {
+ // Arrange
+ var httpContext = new DefaultHttpContext();
+ httpContext.RequestServices = new TestServiceProvider();
+
+ var middleware = CreateMiddleware();
+
+ // Act
+ await middleware.Invoke(httpContext);
+ var routeData = httpContext.GetRouteData();
+ var routeValue = httpContext.GetRouteValue("controller");
+ var endpointFeature = httpContext.Features.Get();
+
+ // Assert
+ Assert.NotNull(routeData);
+ Assert.Equal("Home", (string)routeValue);
+
+ // changing route data value is reflected in endpoint feature values
+ routeData.Values["testKey"] = "testValue";
+ Assert.Equal("testValue", endpointFeature.Values["testKey"]);
+ }
+
private EndpointRoutingMiddleware CreateMiddleware(Logger logger = null)
{
RequestDelegate next = (c) => Task.FromResult