Merge pull request #726 from dotnet-maestro-bot/merge/release/2.2-to-master
[automated] Merge branch 'release/2.2' => 'master'
This commit is contained in:
commit
2b4df294d6
|
|
@ -0,0 +1,127 @@
|
|||
// 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 BenchmarkDotNet.Attributes;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
public class EndpointMetadataCollectionBenchmark
|
||||
{
|
||||
private object[] _items;
|
||||
private EndpointMetadataCollection _collection;
|
||||
|
||||
[Params(3, 10, 25)]
|
||||
public int Count { get; set; }
|
||||
|
||||
[GlobalSetup]
|
||||
public void Setup()
|
||||
{
|
||||
var seeds = new Type[]
|
||||
{
|
||||
typeof(Metadata1),
|
||||
typeof(Metadata2),
|
||||
typeof(Metadata3),
|
||||
typeof(Metadata4),
|
||||
typeof(Metadata5),
|
||||
typeof(Metadata6),
|
||||
typeof(Metadata7),
|
||||
typeof(Metadata8),
|
||||
typeof(Metadata9),
|
||||
};
|
||||
|
||||
_items = new object[Count];
|
||||
for (var i = 0; i < _items.Length; i++)
|
||||
{
|
||||
_items[i] = seeds[i % seeds.Length];
|
||||
}
|
||||
|
||||
_collection = new EndpointMetadataCollection(_items);
|
||||
}
|
||||
|
||||
// This is a synthetic baseline that visits each item and does an as-cast.
|
||||
[Benchmark(Baseline = true, OperationsPerInvoke = 5)]
|
||||
public void Baseline()
|
||||
{
|
||||
var items = _items;
|
||||
for (var i = items.Length - 1; i >= 0; i--)
|
||||
{
|
||||
GC.KeepAlive(_items[i] as IMetadata1);
|
||||
}
|
||||
|
||||
for (var i = items.Length - 1; i >= 0; i--)
|
||||
{
|
||||
GC.KeepAlive(_items[i] as IMetadata2);
|
||||
}
|
||||
|
||||
for (var i = items.Length - 1; i >= 0; i--)
|
||||
{
|
||||
GC.KeepAlive(_items[i] as IMetadata3);
|
||||
}
|
||||
|
||||
for (var i = items.Length - 1; i >= 0; i--)
|
||||
{
|
||||
GC.KeepAlive(_items[i] as IMetadata4);
|
||||
}
|
||||
|
||||
for (var i = items.Length - 1; i >= 0; i--)
|
||||
{
|
||||
GC.KeepAlive(_items[i] as IMetadata5);
|
||||
}
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 5)]
|
||||
public void GetMetadata()
|
||||
{
|
||||
GC.KeepAlive(_collection.GetMetadata<IMetadata1>());
|
||||
GC.KeepAlive(_collection.GetMetadata<IMetadata2>());
|
||||
GC.KeepAlive(_collection.GetMetadata<IMetadata3>());
|
||||
GC.KeepAlive(_collection.GetMetadata<IMetadata4>());
|
||||
GC.KeepAlive(_collection.GetMetadata<IMetadata5>());
|
||||
}
|
||||
|
||||
[Benchmark(OperationsPerInvoke = 5)]
|
||||
public void GetOrderedMetadata()
|
||||
{
|
||||
foreach (var item in _collection.GetOrderedMetadata<IMetadata1>())
|
||||
{
|
||||
GC.KeepAlive(item);
|
||||
}
|
||||
|
||||
foreach (var item in _collection.GetOrderedMetadata<IMetadata2>())
|
||||
{
|
||||
GC.KeepAlive(item);
|
||||
}
|
||||
|
||||
foreach (var item in _collection.GetOrderedMetadata<IMetadata3>())
|
||||
{
|
||||
GC.KeepAlive(item);
|
||||
}
|
||||
|
||||
foreach (var item in _collection.GetOrderedMetadata<IMetadata4>())
|
||||
{
|
||||
GC.KeepAlive(item);
|
||||
}
|
||||
|
||||
foreach (var item in _collection.GetOrderedMetadata<IMetadata5>())
|
||||
{
|
||||
GC.KeepAlive(item);
|
||||
}
|
||||
}
|
||||
|
||||
private interface IMetadata1 { }
|
||||
private interface IMetadata2 { }
|
||||
private interface IMetadata3 { }
|
||||
private interface IMetadata4 { }
|
||||
private interface IMetadata5 { }
|
||||
private class Metadata1 : IMetadata1 { }
|
||||
private class Metadata2 : IMetadata2 { }
|
||||
private class Metadata3 : IMetadata3 { }
|
||||
private class Metadata4 : IMetadata4 { }
|
||||
private class Metadata5 : IMetadata5 { }
|
||||
private class Metadata6 : IMetadata1, IMetadata2 { }
|
||||
private class Metadata7 : IMetadata2, IMetadata3 { }
|
||||
private class Metadata8 : IMetadata4, IMetadata5 { }
|
||||
private class Metadata9 : IMetadata1, IMetadata2 { }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,10 @@
|
|||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
|
|
@ -24,6 +26,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty<object>());
|
||||
|
||||
private readonly object[] _items;
|
||||
private readonly ConcurrentDictionary<Type, object[]> _cache;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="EndpointMetadataCollection"/>.
|
||||
|
|
@ -37,6 +40,7 @@ namespace Microsoft.AspNetCore.Routing
|
|||
}
|
||||
|
||||
_items = items.ToArray();
|
||||
_cache = new ConcurrentDictionary<Type, object[]>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -67,18 +71,23 @@ namespace Microsoft.AspNetCore.Routing
|
|||
/// <returns>
|
||||
/// The most significant metadata of type <typeparamref name="T"/> or <c>null</c>.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public T GetMetadata<T>() where T : class
|
||||
{
|
||||
for (var i = _items.Length - 1; i >= 0; i--)
|
||||
if (_cache.TryGetValue(typeof(T), out var result))
|
||||
{
|
||||
var item = _items[i] as T;
|
||||
if (item != null)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
var length = result.Length;
|
||||
return length > 0 ? (T)result[length - 1] : default;
|
||||
}
|
||||
|
||||
return default;
|
||||
return GetMetadataSlow<T>();
|
||||
}
|
||||
|
||||
private T GetMetadataSlow<T>() where T : class
|
||||
{
|
||||
var array = GetOrderedMetadataSlow<T>();
|
||||
var length = array.Length;
|
||||
return length > 0 ? array[length - 1] : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -87,16 +96,32 @@ namespace Microsoft.AspNetCore.Routing
|
|||
/// </summary>
|
||||
/// <typeparam name="T">The type of metadata.</typeparam>
|
||||
/// <returns>A sequence of metadata items of <typeparamref name="T"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public IEnumerable<T> GetOrderedMetadata<T>() where T : class
|
||||
{
|
||||
if (_cache.TryGetValue(typeof(T), out var result))
|
||||
{
|
||||
return (T[])result;
|
||||
}
|
||||
|
||||
return GetOrderedMetadataSlow<T>();
|
||||
}
|
||||
|
||||
private T[] GetOrderedMetadataSlow<T>() where T : class
|
||||
{
|
||||
var items = new List<T>();
|
||||
for (var i = 0; i < _items.Length; i++)
|
||||
{
|
||||
var item = _items[i] as T;
|
||||
if (item != null)
|
||||
{
|
||||
yield return item;
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
var array = items.ToArray();
|
||||
_cache.TryAdd(typeof(T), array);
|
||||
return array;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata that defines data tokens for an <see cref="Endpoint"/>. This metadata
|
||||
/// type provides data tokens value for <see cref="RouteData.DataTokens"/> associated
|
||||
/// with an endpoint.
|
||||
/// </summary>
|
||||
public sealed class DataTokensMetadata : IDataTokensMetadata
|
||||
{
|
||||
public DataTokensMetadata(IReadOnlyDictionary<string, object> dataTokens)
|
||||
{
|
||||
if (dataTokens == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(dataTokens));
|
||||
}
|
||||
|
||||
DataTokens = dataTokens;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the data tokens.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, object> DataTokens { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IDataTokensMetadata>();
|
||||
if (dataTokensMetadata != null)
|
||||
{
|
||||
var dataTokens = _routeData.DataTokens;
|
||||
foreach (var kvp in dataTokensMetadata.DataTokens)
|
||||
{
|
||||
_routeData.DataTokens.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _routeData;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata that defines data tokens for an <see cref="Endpoint"/>. This metadata
|
||||
/// type provides data tokens value for <see cref="RouteData.DataTokens"/> associated
|
||||
/// with an endpoint.
|
||||
/// </summary>
|
||||
public interface IDataTokensMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the data tokens.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, object> DataTokens { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata used to prevent URL matching. The associated endpoint will not be
|
||||
/// considered URL matching for incoming requests.
|
||||
/// </summary>
|
||||
public interface ISuppressMatchingMetadata
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
|||
for (var i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
var endpoint = endpoints[i] as MatcherEndpoint;
|
||||
if (endpoint != null)
|
||||
if (endpoint != null && endpoint.Metadata.GetMetadata<ISuppressMatchingMetadata>() == null)
|
||||
{
|
||||
builder.AddEndpoint(endpoint);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
// register other endpoint types, which are non-routable, and it's
|
||||
// ok that we won't route to them.
|
||||
var endpoint = endpoints[i] as MatcherEndpoint;
|
||||
if (endpoint != null)
|
||||
if (endpoint != null && endpoint.Metadata.GetMetadata<ISuppressMatchingMetadata>() == null)
|
||||
{
|
||||
builder.AddEndpoint(endpoint);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
// 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.
|
||||
|
||||
namespace Microsoft.AspNetCore.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata used to prevent URL matching. The associated endpoint will not be
|
||||
/// considered URL matching for incoming requests.
|
||||
/// </summary>
|
||||
public sealed class SuppressMatchingMetadata : ISuppressMatchingMetadata
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -44,5 +44,98 @@ namespace Microsoft.AspNetCore.Routing
|
|||
value => Assert.Equal(2, value),
|
||||
value => Assert.Equal(3, value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMetadata_Match_ReturnsLastMatchingEntry()
|
||||
{
|
||||
// Arrange
|
||||
var items = new object[]
|
||||
{
|
||||
new Metadata1(),
|
||||
new Metadata2(),
|
||||
new Metadata3(),
|
||||
};
|
||||
|
||||
var metadata = new EndpointMetadataCollection(items);
|
||||
|
||||
// Act
|
||||
var result = metadata.GetMetadata<IMetadata5>();
|
||||
|
||||
// Assert
|
||||
Assert.Same(items[1], result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetMetadata_NoMatch_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var items = new object[]
|
||||
{
|
||||
new Metadata3(),
|
||||
new Metadata3(),
|
||||
new Metadata3(),
|
||||
};
|
||||
|
||||
var metadata = new EndpointMetadataCollection(items);
|
||||
|
||||
// Act
|
||||
var result = metadata.GetMetadata<IMetadata5>();
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrderedMetadata_Match_ReturnsItemsInAscendingOrder()
|
||||
{
|
||||
// Arrange
|
||||
var items = new object[]
|
||||
{
|
||||
new Metadata1(),
|
||||
new Metadata2(),
|
||||
new Metadata3(),
|
||||
};
|
||||
|
||||
var metadata = new EndpointMetadataCollection(items);
|
||||
|
||||
// Act
|
||||
var result = metadata.GetOrderedMetadata<IMetadata5>();
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
result,
|
||||
i => Assert.Same(items[0], i),
|
||||
i => Assert.Same(items[1], i));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrderedMetadata_NoMatch_ReturnsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
var items = new object[]
|
||||
{
|
||||
new Metadata3(),
|
||||
new Metadata3(),
|
||||
new Metadata3(),
|
||||
};
|
||||
|
||||
var metadata = new EndpointMetadataCollection(items);
|
||||
|
||||
// Act
|
||||
var result = metadata.GetOrderedMetadata<IMetadata5>();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
private interface IMetadata1 { }
|
||||
private interface IMetadata2 { }
|
||||
private interface IMetadata3 { }
|
||||
private interface IMetadata4 { }
|
||||
private interface IMetadata5 { }
|
||||
private class Metadata1 : IMetadata1, IMetadata4, IMetadata5 { }
|
||||
private class Metadata2 : IMetadata2, IMetadata5 { }
|
||||
private class Metadata3 : IMetadata3 { }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IEndpointFeature>();
|
||||
|
||||
// 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<EndpointRoutingMiddleware> logger = null)
|
||||
{
|
||||
RequestDelegate next = (c) => Task.FromResult<object>(null);
|
||||
|
|
|
|||
|
|
@ -67,6 +67,27 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
Assert.Empty(inner.Endpoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Matcher_Ignores_SuppressedEndpoint()
|
||||
{
|
||||
// Arrange
|
||||
var dataSource = new DynamicEndpointDataSource();
|
||||
var endpoint = new MatcherEndpoint(
|
||||
MatcherEndpoint.EmptyInvoker,
|
||||
RoutePatternFactory.Parse("/"),
|
||||
0,
|
||||
new EndpointMetadataCollection(new SuppressMatchingMetadata()),
|
||||
"test");
|
||||
dataSource.AddEndpoint(endpoint);
|
||||
|
||||
// Act
|
||||
var matcher = new DataSourceDependentMatcher(dataSource, TestMatcherBuilder.Create);
|
||||
|
||||
// Assert
|
||||
var inner = Assert.IsType<TestMatcher>(matcher.CurrentMatcher);
|
||||
Assert.Empty(inner.Endpoints);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cache_Reinitializes_WhenDataSourceChanges()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue