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:
Ryan Nowak 2018-08-16 09:52:51 -07:00 committed by GitHub
commit 2b4df294d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 449 additions and 11 deletions

View File

@ -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 { }
}
}

View File

@ -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>

View File

@ -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; }
}
}

View File

@ -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;

View File

@ -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; }
}
}

View File

@ -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
{
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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
{
}
}

View File

@ -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 { }
}
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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()
{