Copy core endpoint routing types to HttpAbstractions (#1030)
This commit is contained in:
parent
55ba639657
commit
05a581a132
|
|
@ -13,6 +13,7 @@
|
||||||
<MicrosoftExtensionsObjectPoolPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsObjectPoolPackageVersion>
|
<MicrosoftExtensionsObjectPoolPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsObjectPoolPackageVersion>
|
||||||
<MicrosoftExtensionsOptionsPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsOptionsPackageVersion>
|
<MicrosoftExtensionsOptionsPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsOptionsPackageVersion>
|
||||||
<MicrosoftExtensionsPrimitivesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsPrimitivesPackageVersion>
|
<MicrosoftExtensionsPrimitivesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsPrimitivesPackageVersion>
|
||||||
|
<MicrosoftExtensionsPropertyHelperSourcesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsPropertyHelperSourcesPackageVersion>
|
||||||
<MicrosoftExtensionsWebEncodersSourcesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsWebEncodersSourcesPackageVersion>
|
<MicrosoftExtensionsWebEncodersSourcesPackageVersion>3.0.0-alpha1-10352</MicrosoftExtensionsWebEncodersSourcesPackageVersion>
|
||||||
<MicrosoftNETCoreApp20PackageVersion>2.0.9</MicrosoftNETCoreApp20PackageVersion>
|
<MicrosoftNETCoreApp20PackageVersion>2.0.9</MicrosoftNETCoreApp20PackageVersion>
|
||||||
<MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion>
|
<MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ Microsoft.AspNetCore.Http.HttpResponse</Description>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.ActivatorUtilities.Sources" PrivateAssets="All" Version="$(MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.ActivatorUtilities.Sources" PrivateAssets="All" Version="$(MicrosoftExtensionsActivatorUtilitiesSourcesPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.PropertyHelper.Sources" Version="$(MicrosoftExtensionsPropertyHelperSourcesPackageVersion)" PrivateAssets="All" />
|
||||||
<PackageReference Include="System.Text.Encodings.Web" Version="$(SystemTextEncodingsWebPackageVersion)" />
|
<PackageReference Include="System.Text.Encodings.Web" Version="$(SystemTextEncodingsWebPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,34 @@ namespace Microsoft.AspNetCore.Http.Abstractions
|
||||||
internal static string FormatArgumentCannotBeNullOrEmpty()
|
internal static string FormatArgumentCannotBeNullOrEmpty()
|
||||||
=> GetString("ArgumentCannotBeNullOrEmpty");
|
=> GetString("ArgumentCannotBeNullOrEmpty");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An element with the key '{0}' already exists in the {1}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string RouteValueDictionary_DuplicateKey
|
||||||
|
{
|
||||||
|
get => GetString("RouteValueDictionary_DuplicateKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An element with the key '{0}' already exists in the {1}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatRouteValueDictionary_DuplicateKey(object p0, object p1)
|
||||||
|
=> string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicateKey"), p0, p1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.
|
||||||
|
/// </summary>
|
||||||
|
internal static string RouteValueDictionary_DuplicatePropertyName
|
||||||
|
{
|
||||||
|
get => GetString("RouteValueDictionary_DuplicatePropertyName");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatRouteValueDictionary_DuplicatePropertyName(object p0, object p1, object p2, object p3)
|
||||||
|
=> string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicatePropertyName"), p0, p1, p2, p3);
|
||||||
|
|
||||||
private static string GetString(string name, params string[] formatterNames)
|
private static string GetString(string name, params string[] formatterNames)
|
||||||
{
|
{
|
||||||
var value = _resourceManager.GetString(name);
|
var value = _resourceManager.GetString(name);
|
||||||
|
|
|
||||||
|
|
@ -156,4 +156,10 @@
|
||||||
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
|
<data name="ArgumentCannotBeNullOrEmpty" xml:space="preserve">
|
||||||
<value>Argument cannot be null or empty.</value>
|
<value>Argument cannot be null or empty.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="RouteValueDictionary_DuplicateKey" xml:space="preserve">
|
||||||
|
<value>An element with the key '{0}' already exists in the {1}.</value>
|
||||||
|
</data>
|
||||||
|
<data name="RouteValueDictionary_DuplicatePropertyName" xml:space="preserve">
|
||||||
|
<value>The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Http
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Respresents a logical endpoint in an application.
|
||||||
|
/// </summary>
|
||||||
|
public class Endpoint
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of <see cref="Endpoint"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requestDelegate">The delegate used to process requests for the endpoint.</param>
|
||||||
|
/// <param name="metadata">
|
||||||
|
/// The endpoint <see cref="EndpointMetadataCollection"/>. May be null.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="displayName">
|
||||||
|
/// The informational display name of the endpoint. May be null.
|
||||||
|
/// </param>
|
||||||
|
public Endpoint(
|
||||||
|
RequestDelegate requestDelegate,
|
||||||
|
EndpointMetadataCollection metadata,
|
||||||
|
string displayName)
|
||||||
|
{
|
||||||
|
// All are allowed to be null
|
||||||
|
RequestDelegate = requestDelegate;
|
||||||
|
Metadata = metadata ?? EndpointMetadataCollection.Empty;
|
||||||
|
DisplayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the informational display name of this endpoint.
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the collection of metadata associated with this endpoint.
|
||||||
|
/// </summary>
|
||||||
|
public EndpointMetadataCollection Metadata { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the delegate used to process requests for the endpoint.
|
||||||
|
/// </summary>
|
||||||
|
public RequestDelegate RequestDelegate { get; }
|
||||||
|
|
||||||
|
public override string ToString() => DisplayName ?? base.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
// 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.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Http
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A collection of arbitrary metadata associated with an endpoint.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="EndpointMetadataCollection"/> instances contain a list of metadata items
|
||||||
|
/// of arbitrary types. The metadata items are stored as an ordered collection with
|
||||||
|
/// items arranged in ascending order of precedence.
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class EndpointMetadataCollection : IReadOnlyList<object>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An empty <see cref="EndpointMetadataCollection"/>.
|
||||||
|
/// </summary>
|
||||||
|
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"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="items">The metadata items.</param>
|
||||||
|
public EndpointMetadataCollection(IEnumerable<object> items)
|
||||||
|
{
|
||||||
|
if (items == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(items));
|
||||||
|
}
|
||||||
|
|
||||||
|
_items = items.ToArray();
|
||||||
|
_cache = new ConcurrentDictionary<Type, object[]>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="EndpointMetadataCollection"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="items">The metadata items.</param>
|
||||||
|
public EndpointMetadataCollection(params object[] items)
|
||||||
|
: this((IEnumerable<object>)items)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the item at <paramref name="index"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">The index of the item to retrieve.</param>
|
||||||
|
/// <returns>The item at <paramref name="index"/>.</returns>
|
||||||
|
public object this[int index] => _items[index];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the count of metadata items.
|
||||||
|
/// </summary>
|
||||||
|
public int Count => _items.Length;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the most significant metadata item of type <typeparamref name="T"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of metadata to retrieve.</typeparam>
|
||||||
|
/// <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
|
||||||
|
{
|
||||||
|
if (_cache.TryGetValue(typeof(T), out var result))
|
||||||
|
{
|
||||||
|
var length = result.Length;
|
||||||
|
return length > 0 ? (T)result[length - 1] : 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>
|
||||||
|
/// Gets the metadata items of type <typeparamref name="T"/> in ascending
|
||||||
|
/// order of precedence.
|
||||||
|
/// </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++)
|
||||||
|
{
|
||||||
|
if (_items[i] is T item)
|
||||||
|
{
|
||||||
|
items.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var array = items.ToArray();
|
||||||
|
_cache.TryAdd(typeof(T), array);
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an <see cref="IEnumerator"/> of all metadata items.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An <see cref="IEnumerator"/> of all metadata items.</returns>
|
||||||
|
public Enumerator GetEnumerator() => new Enumerator(this);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an <see cref="IEnumerator{Object}"/> of all metadata items.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An <see cref="IEnumerator{Object}"/> of all metadata items.</returns>
|
||||||
|
IEnumerator<object> IEnumerable<object>.GetEnumerator() => GetEnumerator();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an <see cref="IEnumerator"/> of all metadata items.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An <see cref="IEnumerator"/> of all metadata items.</returns>
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enumerates the elements of an <see cref="EndpointMetadataCollection"/>.
|
||||||
|
/// </summary>
|
||||||
|
public struct Enumerator : IEnumerator<object>
|
||||||
|
{
|
||||||
|
// Intentionally not readonly to prevent defensive struct copies
|
||||||
|
private object[] _items;
|
||||||
|
private int _index;
|
||||||
|
|
||||||
|
internal Enumerator(EndpointMetadataCollection collection)
|
||||||
|
{
|
||||||
|
_items = collection._items;
|
||||||
|
_index = 0;
|
||||||
|
Current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the element at the current position of the enumerator
|
||||||
|
/// </summary>
|
||||||
|
public object Current { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases all resources used by the <see cref="Enumerator"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Advances the enumerator to the next element of the <see cref="Enumerator"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// <c>true</c> if the enumerator was successfully advanced to the next element;
|
||||||
|
/// <c>false</c> if the enumerator has passed the end of the collection.
|
||||||
|
/// </returns>
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
if (_index < _items.Length)
|
||||||
|
{
|
||||||
|
Current = _items[_index++];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Current = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the enumerator to its initial position, which is before the first element in the collection.
|
||||||
|
/// </summary>
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_index = 0;
|
||||||
|
Current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
// 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.Http.Features
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A feature interface for endpoint routing. Use <see cref="HttpContext.Features"/>
|
||||||
|
/// to access an instance associated with the current request.
|
||||||
|
/// </summary>
|
||||||
|
public interface IEndpointFeature
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the selected <see cref="Http.Endpoint"/> for the current
|
||||||
|
/// request.
|
||||||
|
/// </summary>
|
||||||
|
Endpoint Endpoint { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Http.Features
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A feature interface for routing values. Use <see cref="HttpContext.Features"/>
|
||||||
|
/// to access the values associated with the current request.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRouteValuesFeature
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the <see cref="RouteValueDictionary"/> associated with the currrent
|
||||||
|
/// request.
|
||||||
|
/// </summary>
|
||||||
|
RouteValueDictionary RouteValues { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,598 @@
|
||||||
|
// 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.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Microsoft.AspNetCore.Http.Abstractions;
|
||||||
|
using Microsoft.Extensions.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="IDictionary{String, Object}"/> type for route values.
|
||||||
|
/// </summary>
|
||||||
|
public class RouteValueDictionary : IDictionary<string, object>, IReadOnlyDictionary<string, object>
|
||||||
|
{
|
||||||
|
// 4 is a good default capacity here because that leaves enough space for area/controller/action/id
|
||||||
|
private const int DefaultCapacity = 4;
|
||||||
|
|
||||||
|
internal KeyValuePair<string, object>[] _arrayStorage;
|
||||||
|
internal PropertyStorage _propertyStorage;
|
||||||
|
private int _count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="RouteValueDictionary"/> from the provided array.
|
||||||
|
/// The new instance will take ownership of the array, and may mutate it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="items">The items array.</param>
|
||||||
|
/// <returns>A new <see cref="RouteValueDictionary"/>.</returns>
|
||||||
|
public static RouteValueDictionary FromArray(KeyValuePair<string, object>[] items)
|
||||||
|
{
|
||||||
|
if (items == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(items));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to compress the array by removing non-contiguous items. We
|
||||||
|
// typically have a very small number of items to process. We don't need
|
||||||
|
// to preserve order.
|
||||||
|
var start = 0;
|
||||||
|
var end = items.Length - 1;
|
||||||
|
|
||||||
|
// We walk forwards from the beginning of the array and fill in 'null' slots.
|
||||||
|
// We walk backwards from the end of the array end move items in non-null' slots
|
||||||
|
// into whatever start is pointing to. O(n)
|
||||||
|
while (start <= end)
|
||||||
|
{
|
||||||
|
if (items[start].Key != null)
|
||||||
|
{
|
||||||
|
start++;
|
||||||
|
}
|
||||||
|
else if (items[end].Key != null)
|
||||||
|
{
|
||||||
|
// Swap this item into start and advance
|
||||||
|
items[start] = items[end];
|
||||||
|
items[end] = default;
|
||||||
|
start++;
|
||||||
|
end--;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Both null, we need to hold on 'start' since we
|
||||||
|
// still need to fill it with something.
|
||||||
|
end--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RouteValueDictionary()
|
||||||
|
{
|
||||||
|
_arrayStorage = items,
|
||||||
|
_count = start,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an empty <see cref="RouteValueDictionary"/>.
|
||||||
|
/// </summary>
|
||||||
|
public RouteValueDictionary()
|
||||||
|
{
|
||||||
|
_arrayStorage = Array.Empty<KeyValuePair<string, object>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a <see cref="RouteValueDictionary"/> initialized with the specified <paramref name="values"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">An object to initialize the dictionary. The value can be of type
|
||||||
|
/// <see cref="IDictionary{TKey, TValue}"/> or <see cref="IReadOnlyDictionary{TKey, TValue}"/>
|
||||||
|
/// or an object with public properties as key-value pairs.
|
||||||
|
/// </param>
|
||||||
|
/// <remarks>
|
||||||
|
/// If the value is a dictionary or other <see cref="IEnumerable{T}"/> of <see cref="KeyValuePair{String, Object}"/>,
|
||||||
|
/// then its entries are copied. Otherwise the object is interpreted as a set of key-value pairs where the
|
||||||
|
/// property names are keys, and property values are the values, and copied into the dictionary.
|
||||||
|
/// Only public instance non-index properties are considered.
|
||||||
|
/// </remarks>
|
||||||
|
public RouteValueDictionary(object values)
|
||||||
|
: this()
|
||||||
|
{
|
||||||
|
if (values is RouteValueDictionary dictionary)
|
||||||
|
{
|
||||||
|
if (dictionary._propertyStorage != null)
|
||||||
|
{
|
||||||
|
// PropertyStorage is immutable so we can just copy it.
|
||||||
|
_propertyStorage = dictionary._propertyStorage;
|
||||||
|
_count = dictionary._count;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var other = dictionary._arrayStorage;
|
||||||
|
var storage = new KeyValuePair<string, object>[other.Length];
|
||||||
|
if (dictionary._count != 0)
|
||||||
|
{
|
||||||
|
Array.Copy(other, 0, storage, 0, dictionary._count);
|
||||||
|
}
|
||||||
|
|
||||||
|
_arrayStorage = storage;
|
||||||
|
_count = dictionary._count;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values is IEnumerable<KeyValuePair<string, object>> keyValueEnumerable)
|
||||||
|
{
|
||||||
|
foreach (var kvp in keyValueEnumerable)
|
||||||
|
{
|
||||||
|
Add(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values is IEnumerable<KeyValuePair<string, string>> stringValueEnumerable)
|
||||||
|
{
|
||||||
|
foreach (var kvp in stringValueEnumerable)
|
||||||
|
{
|
||||||
|
Add(kvp.Key, kvp.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values != null)
|
||||||
|
{
|
||||||
|
var storage = new PropertyStorage(values);
|
||||||
|
_propertyStorage = storage;
|
||||||
|
_count = storage.Properties.Length;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public object this[string key]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
object value;
|
||||||
|
TryGetValue(key, out value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're calling this here for the side-effect of converting from properties
|
||||||
|
// to array. We need to create the array even if we just set an existing value since
|
||||||
|
// property storage is immutable.
|
||||||
|
EnsureCapacity(_count);
|
||||||
|
|
||||||
|
var index = FindInArray(key);
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
EnsureCapacity(_count + 1);
|
||||||
|
_arrayStorage[_count++] = new KeyValuePair<string, object>(key, value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_arrayStorage[index] = new KeyValuePair<string, object>(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the comparer for this dictionary.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This will always be a reference to <see cref="StringComparer.OrdinalIgnoreCase"/>
|
||||||
|
/// </remarks>
|
||||||
|
public IEqualityComparer<string> Comparer => StringComparer.OrdinalIgnoreCase;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int Count => _count;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
bool ICollection<KeyValuePair<string, object>>.IsReadOnly => false;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICollection<string> Keys
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
EnsureCapacity(_count);
|
||||||
|
|
||||||
|
var array = _arrayStorage;
|
||||||
|
var keys = new string[_count];
|
||||||
|
for (var i = 0; i < keys.Length; i++)
|
||||||
|
{
|
||||||
|
keys[i] = array[i].Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable<string> IReadOnlyDictionary<string, object>.Keys => Keys;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICollection<object> Values
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
EnsureCapacity(_count);
|
||||||
|
|
||||||
|
var array = _arrayStorage;
|
||||||
|
var values = new object[_count];
|
||||||
|
for (var i = 0; i < values.Length; i++)
|
||||||
|
{
|
||||||
|
values[i] = array[i].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable<object> IReadOnlyDictionary<string, object>.Values => Values;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
|
||||||
|
{
|
||||||
|
Add(item.Key, item.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Add(string key, object value)
|
||||||
|
{
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureCapacity(_count + 1);
|
||||||
|
|
||||||
|
var index = FindInArray(key);
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary));
|
||||||
|
throw new ArgumentException(message, nameof(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
_arrayStorage[_count] = new KeyValuePair<string, object>(key, value);
|
||||||
|
_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
if (_count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_propertyStorage != null)
|
||||||
|
{
|
||||||
|
_arrayStorage = Array.Empty<KeyValuePair<string, object>>();
|
||||||
|
_propertyStorage = null;
|
||||||
|
_count = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.Clear(_arrayStorage, 0, _count);
|
||||||
|
_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
|
||||||
|
{
|
||||||
|
return TryGetValue(item.Key, out var value) && EqualityComparer<object>.Default.Equals(value, item.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool ContainsKey(string key)
|
||||||
|
{
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryGetValue(key, out var _);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
void ICollection<KeyValuePair<string, object>>.CopyTo(
|
||||||
|
KeyValuePair<string, object>[] array,
|
||||||
|
int arrayIndex)
|
||||||
|
{
|
||||||
|
if (array == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(array));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arrayIndex < 0 || arrayIndex > array.Length || array.Length - arrayIndex < this.Count)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(arrayIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureCapacity(Count);
|
||||||
|
|
||||||
|
var storage = _arrayStorage;
|
||||||
|
Array.Copy(storage, 0, array, arrayIndex, _count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Enumerator GetEnumerator()
|
||||||
|
{
|
||||||
|
return new Enumerator(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
|
||||||
|
{
|
||||||
|
if (Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureCapacity(Count);
|
||||||
|
|
||||||
|
var index = FindInArray(item.Key);
|
||||||
|
var array = _arrayStorage;
|
||||||
|
if (index >= 0 && EqualityComparer<object>.Default.Equals(array[index].Value, item.Value))
|
||||||
|
{
|
||||||
|
Array.Copy(array, index + 1, array, index, _count - index);
|
||||||
|
_count--;
|
||||||
|
array[_count] = default;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Remove(string key)
|
||||||
|
{
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnsureCapacity(Count);
|
||||||
|
|
||||||
|
var index = FindInArray(key);
|
||||||
|
if (index >= 0)
|
||||||
|
{
|
||||||
|
_count--;
|
||||||
|
var array = _arrayStorage;
|
||||||
|
Array.Copy(array, index + 1, array, index, _count - index);
|
||||||
|
array[_count] = default;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryGetValue(string key, out object value)
|
||||||
|
{
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_propertyStorage != null)
|
||||||
|
{
|
||||||
|
var storage = _propertyStorage;
|
||||||
|
for (var i = 0; i < storage.Properties.Length; i++)
|
||||||
|
{
|
||||||
|
if (string.Equals(storage.Properties[i].Name, key, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
value = storage.Properties[i].GetValue(storage.Value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var array = _arrayStorage;
|
||||||
|
for (var i = 0; i < _count; i++)
|
||||||
|
{
|
||||||
|
if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
value = array[i].Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void EnsureCapacity(int capacity)
|
||||||
|
{
|
||||||
|
if (_propertyStorage != null || _arrayStorage.Length < capacity)
|
||||||
|
{
|
||||||
|
EnsureCapacitySlow(capacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureCapacitySlow(int capacity)
|
||||||
|
{
|
||||||
|
if (_propertyStorage != null)
|
||||||
|
{
|
||||||
|
var storage = _propertyStorage;
|
||||||
|
capacity = Math.Max(storage.Properties.Length, capacity);
|
||||||
|
var array = new KeyValuePair<string, object>[capacity];
|
||||||
|
|
||||||
|
for (var i = 0; i < storage.Properties.Length; i++)
|
||||||
|
{
|
||||||
|
var property = storage.Properties[i];
|
||||||
|
array[i] = new KeyValuePair<string, object>(property.Name, property.GetValue(storage.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
_arrayStorage = array;
|
||||||
|
_propertyStorage = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_arrayStorage.Length < capacity)
|
||||||
|
{
|
||||||
|
capacity = _arrayStorage.Length == 0 ? DefaultCapacity : _arrayStorage.Length * 2;
|
||||||
|
var array = new KeyValuePair<string, object>[capacity];
|
||||||
|
if (_count > 0)
|
||||||
|
{
|
||||||
|
Array.Copy(_arrayStorage, 0, array, 0, _count);
|
||||||
|
}
|
||||||
|
|
||||||
|
_arrayStorage = array;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private int FindInArray(string key)
|
||||||
|
{
|
||||||
|
var array = _arrayStorage;
|
||||||
|
for (var i = 0; i < _count; i++)
|
||||||
|
{
|
||||||
|
if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Enumerator : IEnumerator<KeyValuePair<string, object>>
|
||||||
|
{
|
||||||
|
private RouteValueDictionary _dictionary;
|
||||||
|
private int _index;
|
||||||
|
|
||||||
|
public Enumerator(RouteValueDictionary dictionary)
|
||||||
|
{
|
||||||
|
if (dictionary == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException();
|
||||||
|
}
|
||||||
|
|
||||||
|
_dictionary = dictionary;
|
||||||
|
|
||||||
|
Current = default;
|
||||||
|
_index = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyValuePair<string, object> Current { get; private set; }
|
||||||
|
|
||||||
|
object IEnumerator.Current => Current;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
if (++_index < _dictionary.Count)
|
||||||
|
{
|
||||||
|
if (_dictionary._propertyStorage != null)
|
||||||
|
{
|
||||||
|
var storage = _dictionary._propertyStorage;
|
||||||
|
var property = storage.Properties[_index];
|
||||||
|
Current = new KeyValuePair<string, object>(property.Name, property.GetValue(storage.Value));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Current = _dictionary._arrayStorage[_index];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Current = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Current = default;
|
||||||
|
_index = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class PropertyStorage
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<Type, PropertyHelper[]> _propertyCache = new ConcurrentDictionary<Type, PropertyHelper[]>();
|
||||||
|
|
||||||
|
public readonly object Value;
|
||||||
|
public readonly PropertyHelper[] Properties;
|
||||||
|
|
||||||
|
public PropertyStorage(object value)
|
||||||
|
{
|
||||||
|
Debug.Assert(value != null);
|
||||||
|
Value = value;
|
||||||
|
|
||||||
|
// Cache the properties so we can know if we've already validated them for duplicates.
|
||||||
|
var type = Value.GetType();
|
||||||
|
if (!_propertyCache.TryGetValue(type, out Properties))
|
||||||
|
{
|
||||||
|
Properties = PropertyHelper.GetVisibleProperties(type);
|
||||||
|
ValidatePropertyNames(type, Properties);
|
||||||
|
_propertyCache.TryAdd(type, Properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidatePropertyNames(Type type, PropertyHelper[] properties)
|
||||||
|
{
|
||||||
|
var names = new Dictionary<string, PropertyHelper>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
for (var i = 0; i < properties.Length; i++)
|
||||||
|
{
|
||||||
|
var property = properties[i];
|
||||||
|
|
||||||
|
if (names.TryGetValue(property.Name, out var duplicate))
|
||||||
|
{
|
||||||
|
var message = Resources.FormatRouteValueDictionary_DuplicatePropertyName(
|
||||||
|
type.FullName,
|
||||||
|
property.Name,
|
||||||
|
duplicate.Name,
|
||||||
|
nameof(RouteValueDictionary));
|
||||||
|
throw new InvalidOperationException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
names.Add(property.Name, property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Routing
|
||||||
|
{
|
||||||
|
public class EndpointMetadataCollectionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_Enumeration_ContainsValues()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var metadata = new EndpointMetadataCollection(new List<object>
|
||||||
|
{
|
||||||
|
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_ParamsArray_ContainsValues()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var metadata = new EndpointMetadataCollection(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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue