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(null);