From 434bf614d52bfef3f6ac1e39e6d41077cc436b99 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 8 May 2019 08:49:41 -0700 Subject: [PATCH] Make GetOrdereredMetadata return something good --- ...NetCore.Http.Abstractions.netcoreapp3.0.cs | 26 +++- .../src/Routing/EndpointMetadataCollection.cs | 41 +++--- .../OrderedEndpointMetadataCollection.cs | 118 ++++++++++++++++++ .../test/EndpointMetadataCollectionTests.cs | 30 ++++- .../OrderedEndpointMetadataCollectionTest.cs | 49 ++++++++ 5 files changed, 243 insertions(+), 21 deletions(-) create mode 100644 src/Http/Http.Abstractions/src/Routing/OrderedEndpointMetadataCollection.cs create mode 100644 src/Http/Http.Abstractions/test/OrderedEndpointMetadataCollectionTest.cs diff --git a/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp3.0.cs b/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp3.0.cs index 1c2f4371cc..60d9f52032 100644 --- a/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp3.0.cs +++ b/src/Http/Http.Abstractions/ref/Microsoft.AspNetCore.Http.Abstractions.netcoreapp3.0.cs @@ -161,7 +161,7 @@ namespace Microsoft.AspNetCore.Http public object this[int index] { get { throw null; } } public Microsoft.AspNetCore.Http.EndpointMetadataCollection.Enumerator GetEnumerator() { throw null; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public T GetMetadata() where T : class { throw null; } - [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public System.Collections.Generic.IEnumerable GetOrderedMetadata() where T : class { throw null; } + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]public Microsoft.AspNetCore.Http.OrderedEndpointMetadataCollection GetOrderedMetadata() where T : class { throw null; } System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] @@ -327,6 +327,30 @@ namespace Microsoft.AspNetCore.Http Microsoft.AspNetCore.Http.IMiddleware Create(System.Type middlewareType); void Release(Microsoft.AspNetCore.Http.IMiddleware middleware); } + public sealed partial class OrderedEndpointMetadataCollection : System.Collections.Generic.IEnumerable, System.Collections.Generic.IReadOnlyCollection, System.Collections.Generic.IReadOnlyList, System.Collections.IEnumerable where T : class + { + internal OrderedEndpointMetadataCollection() { } + public int Count { get { throw null; } } + public T this[int index] { get { throw null; } } + public Microsoft.AspNetCore.Http.OrderedEndpointMetadataCollection.Enumerator GetEnumerator() { throw null; } + System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public partial struct Enumerator : System.Collections.Generic.IEnumerator, System.Collections.IEnumerator, System.IDisposable + { + private T[] _items; + [System.Diagnostics.DebuggerBrowsableAttribute(System.Diagnostics.DebuggerBrowsableState.Never)] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute] + private T _Current_k__BackingField; + private object _dummy; + private int _dummyPrimitive; + public T Current { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + public void Dispose() { } + public bool MoveNext() { throw null; } + public void Reset() { } + } + } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct PathString : System.IEquatable { diff --git a/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs b/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs index 1f3705da77..ef7b4b1286 100644 --- a/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs +++ b/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs @@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Http public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty()); private readonly object[] _items; - private readonly ConcurrentDictionary _cache; + private readonly ConcurrentDictionary _cache; /// /// Creates a new . @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Http } _items = items.ToArray(); - _cache = new ConcurrentDictionary(); + _cache = new ConcurrentDictionary(); } /// @@ -74,10 +74,11 @@ namespace Microsoft.AspNetCore.Http [MethodImpl(MethodImplOptions.AggressiveInlining)] public T GetMetadata() where T : class { - if (_cache.TryGetValue(typeof(T), out var result)) + if (_cache.TryGetValue(typeof(T), out var obj)) { - var length = result.Length; - return length > 0 ? (T)result[length - 1] : default; + var result = (OrderedEndpointMetadataCollection)obj; + var count = result.Count; + return count > 0 ? result[count - 1] : default; } return GetMetadataSlow(); @@ -85,9 +86,9 @@ namespace Microsoft.AspNetCore.Http private T GetMetadataSlow() where T : class { - var array = GetOrderedMetadataSlow(); - var length = array.Length; - return length > 0 ? array[length - 1] : default; + var result = GetOrderedMetadataSlow(); + var count = result.Count; + return count > 0 ? result[count - 1] : default; } /// @@ -97,30 +98,34 @@ namespace Microsoft.AspNetCore.Http /// The type of metadata. /// A sequence of metadata items of . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IEnumerable GetOrderedMetadata() where T : class + public OrderedEndpointMetadataCollection GetOrderedMetadata() where T : class { if (_cache.TryGetValue(typeof(T), out var result)) { - return (T[])result; + return (OrderedEndpointMetadataCollection)result; } return GetOrderedMetadataSlow(); } - private T[] GetOrderedMetadataSlow() where T : class + private OrderedEndpointMetadataCollection GetOrderedMetadataSlow() where T : class { - var items = new List(); - for (var i = 0; i < _items.Length; i++) + // Perf: avoid allocations totally for the common case where there are no matching metadata. + List matches = null; + + var items = _items; + for (var i = 0; i < items.Length; i++) { - if (_items[i] is T item) + if (items[i] is T item) { - items.Add(item); + matches ??= new List(); + matches.Add(item); } } - var array = items.ToArray(); - _cache.TryAdd(typeof(T), array); - return array; + var results = matches == null ? OrderedEndpointMetadataCollection.Empty : new OrderedEndpointMetadataCollection(matches.ToArray()); + _cache.TryAdd(typeof(T), results); + return results; } /// diff --git a/src/Http/Http.Abstractions/src/Routing/OrderedEndpointMetadataCollection.cs b/src/Http/Http.Abstractions/src/Routing/OrderedEndpointMetadataCollection.cs new file mode 100644 index 0000000000..223aee3c09 --- /dev/null +++ b/src/Http/Http.Abstractions/src/Routing/OrderedEndpointMetadataCollection.cs @@ -0,0 +1,118 @@ +// 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; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// A collection of ordered, strongly-typed metadata associated with an . + /// + /// The metadata type. + public sealed class OrderedEndpointMetadataCollection : IReadOnlyList + where T : class + { + internal static readonly OrderedEndpointMetadataCollection Empty = new OrderedEndpointMetadataCollection(Array.Empty()); + + private readonly T[] _items; + + internal OrderedEndpointMetadataCollection(T[] items) + { + _items = items; + } + + /// + /// Gets the item at . + /// + /// The index of the item to retrieve. + /// The item at . + public T this[int index] => _items[index]; + + /// + /// Gets the count of metadata items. + /// + public int Count => _items.Length; + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + public Enumerator GetEnumerator() => new Enumerator(this); + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Enumerates the elements of an . + /// + public struct Enumerator : IEnumerator + { + // Intentionally not readonly to prevent defensive struct copies + private T[] _items; + private int _index; + + internal Enumerator(OrderedEndpointMetadataCollection collection) + { + _items = collection._items; + _index = 0; + Current = null; + } + + /// + /// Gets the element at the current position of the enumerator + /// + public T Current { get; private set; } + + /// + /// Gets the element at the current position of the enumerator + /// + object IEnumerator.Current => Current; + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + } + + /// + /// Advances the enumerator to the next element of the . + /// + /// + /// true if the enumerator was successfully advanced to the next element; + /// false if the enumerator has passed the end of the collection. + /// + public bool MoveNext() + { + if (_index < _items.Length) + { + Current = _items[_index++]; + return true; + } + + Current = null; + return false; + } + + /// + /// Sets the enumerator to its initial position, which is before the first element in the collection. + /// + public void Reset() + { + _index = 0; + Current = null; + } + } + } +} diff --git a/src/Http/Http.Abstractions/test/EndpointMetadataCollectionTests.cs b/src/Http/Http.Abstractions/test/EndpointMetadataCollectionTests.cs index 62432b7b2b..fe8697a131 100644 --- a/src/Http/Http.Abstractions/test/EndpointMetadataCollectionTests.cs +++ b/src/Http/Http.Abstractions/test/EndpointMetadataCollectionTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -45,5 +45,31 @@ namespace Microsoft.AspNetCore.Routing value => Assert.Equal(2, value), value => Assert.Equal(3, value)); } + + [Fact] + public void GetOrderedMetadata_CanReturnEmptyCollection() + { + // Arrange + var metadata = new EndpointMetadataCollection(1, 2, 3); + + // Act + var ordered = metadata.GetOrderedMetadata(); + + Assert.Same(OrderedEndpointMetadataCollection.Empty, ordered); + } + + [Fact] + public void GetOrderedMetadata_CanReturnNonEmptyCollection() + { + // Arrange + var metadata = new EndpointMetadataCollection("1", "2"); + + // Act + var ordered1 = metadata.GetOrderedMetadata(); + var ordered2 = metadata.GetOrderedMetadata(); + + Assert.Same(ordered1, ordered2); + Assert.Equal(new string[] { "1", "2" }, ordered1); + } } -} \ No newline at end of file +} diff --git a/src/Http/Http.Abstractions/test/OrderedEndpointMetadataCollectionTest.cs b/src/Http/Http.Abstractions/test/OrderedEndpointMetadataCollectionTest.cs new file mode 100644 index 0000000000..899c1f43be --- /dev/null +++ b/src/Http/Http.Abstractions/test/OrderedEndpointMetadataCollectionTest.cs @@ -0,0 +1,49 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Http.Abstractions.Tests +{ + public class OrderedEndpointMetadataCollectionTest + { + [Fact] + public void Constructor_Enumeration_ContainsValues() + { + // Arrange & Act + var metadata = new OrderedEndpointMetadataCollection(new string[] + { + "1", + "2", + "3", + }); + + // Assert + Assert.Equal(3, metadata.Count); + + Assert.Collection(metadata, + value => Assert.Equal("1", value), + value => Assert.Equal("2", value), + value => Assert.Equal("3", value)); + } + + [Fact] + public void Constructor_Loop_ContainsValues() + { + // Arrange & Act + var metadata = new OrderedEndpointMetadataCollection(new string[] + { + "1", + "2", + "3", + }); + + // Assert + Assert.Equal(3, metadata.Count); + for (var i = 0; i < metadata.Count; i++) + { + Assert.Equal((i + 1).ToString(), metadata[i]); + } + } + } +}