diff --git a/src/Microsoft.AspNet.Routing/AttributeRouting/TreeRouter.cs b/src/Microsoft.AspNet.Routing/AttributeRouting/TreeRouter.cs index 2449303da1..70163d2280 100644 --- a/src/Microsoft.AspNet.Routing/AttributeRouting/TreeRouter.cs +++ b/src/Microsoft.AspNet.Routing/AttributeRouting/TreeRouter.cs @@ -311,8 +311,8 @@ namespace Microsoft.AspNet.Routing.Tree } private static void MergeValues( - IDictionary destination, - IDictionary values) + RouteValueDictionary destination, + RouteValueDictionary values) { foreach (var kvp in values) { @@ -328,7 +328,7 @@ namespace Microsoft.AspNet.Routing.Tree private struct TemplateMatch : IEquatable { - public TemplateMatch(TreeRouteMatchingEntry entry, IDictionary values) + public TemplateMatch(TreeRouteMatchingEntry entry, RouteValueDictionary values) { Entry = entry; Values = values; @@ -336,7 +336,7 @@ namespace Microsoft.AspNet.Routing.Tree public TreeRouteMatchingEntry Entry { get; } - public IDictionary Values { get; } + public RouteValueDictionary Values { get; } public override bool Equals(object obj) { @@ -403,7 +403,7 @@ namespace Microsoft.AspNet.Routing.Tree // required values: { id = "5", action = "Buy", Controller = "CoolProducts" } // // result: { id = "5", action = "Buy" } - var inputValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + var inputValues = new RouteValueDictionary(); foreach (var kvp in context.Values) { if (entry.RequiredLinkValues.ContainsKey(kvp.Key)) diff --git a/src/Microsoft.AspNet.Routing/RouteData.cs b/src/Microsoft.AspNet.Routing/RouteData.cs index dbc4da9d6b..89104497e1 100644 --- a/src/Microsoft.AspNet.Routing/RouteData.cs +++ b/src/Microsoft.AspNet.Routing/RouteData.cs @@ -72,7 +72,7 @@ namespace Microsoft.AspNet.Routing /// /// Gets the set of values produced by routes on the current routing path. /// - public IDictionary Values + public RouteValueDictionary Values { get { diff --git a/src/Microsoft.AspNet.Routing/RouteValueDictionary.cs b/src/Microsoft.AspNet.Routing/RouteValueDictionary.cs index 203bcc13b0..53b55cbfad 100644 --- a/src/Microsoft.AspNet.Routing/RouteValueDictionary.cs +++ b/src/Microsoft.AspNet.Routing/RouteValueDictionary.cs @@ -4,8 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Reflection; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNet.Routing { @@ -19,14 +18,11 @@ namespace Microsoft.AspNet.Routing /// internal static readonly IReadOnlyDictionary Empty = new RouteValueDictionary(); - private readonly Dictionary _dictionary; - /// /// Creates an empty . /// public RouteValueDictionary() { - _dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); } /// @@ -43,46 +39,63 @@ namespace Microsoft.AspNet.Routing /// Only public instance non-index properties are considered. /// public RouteValueDictionary(object values) - : this() { - if (values != null) + var otherDictionary = values as RouteValueDictionary; + if (otherDictionary != null) { - var keyValuePairCollection = values as IEnumerable>; - if (keyValuePairCollection != null) + if (otherDictionary.InnerDictionary != null) { - foreach (var kvp in keyValuePairCollection) + InnerDictionary = new Dictionary( + otherDictionary.InnerDictionary.Count, + StringComparer.OrdinalIgnoreCase); + + foreach (var kvp in otherDictionary.InnerDictionary) { - Add(kvp.Key, kvp.Value); + InnerDictionary[kvp.Key] = kvp.Value; } + return; } - - var type = values.GetType(); - var allProperties = type.GetRuntimeProperties(); - - // This is done to support 'new' properties that hide a property on a base class - var orderedByDeclaringType = allProperties.OrderBy(p => p.DeclaringType == type ? 0 : 1); - foreach (var property in orderedByDeclaringType) + else if (otherDictionary.Properties != null) { - if (property.GetMethod != null && - property.GetMethod.IsPublic && - !property.GetMethod.IsStatic && - property.GetIndexParameters().Length == 0) - { - var value = property.GetValue(values); - if (ContainsKey(property.Name) && property.DeclaringType != type) - { - // This is a hidden property, ignore it. - } - else - { - Add(property.Name, value); - } - } + Properties = otherDictionary.Properties; + Value = otherDictionary.Value; + return; + } + else + { + return; } } + + var keyValuePairCollection = values as IEnumerable>; + if (keyValuePairCollection != null) + { + InnerDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var kvp in keyValuePairCollection) + { + InnerDictionary[kvp.Key] = kvp.Value; + } + + return; + } + + if (values != null) + { + Properties = PropertyHelper.GetVisibleProperties(values); + Value = values; + + return; + } } + private Dictionary InnerDictionary { get; set; } + + private PropertyHelper[] Properties { get; } + + private object Value { get; } + /// public object this[string key] { @@ -94,7 +107,7 @@ namespace Microsoft.AspNet.Routing } object value; - _dictionary.TryGetValue(key, out value); + TryGetValue(key, out value); return value; } @@ -105,7 +118,8 @@ namespace Microsoft.AspNet.Routing throw new ArgumentNullException(nameof(key)); } - _dictionary[key] = value; + EnsureWritable(); + InnerDictionary[key] = value; } } @@ -115,47 +129,21 @@ namespace Microsoft.AspNet.Routing /// /// This will always be a reference to /// - public IEqualityComparer Comparer - { - get - { - return _dictionary.Comparer; - } - } + public IEqualityComparer Comparer => StringComparer.OrdinalIgnoreCase; /// - public int Count - { - get - { - return _dictionary.Count; - } - } + public int Count => InnerDictionary?.Count ?? Properties?.Length ?? 0; /// - bool ICollection>.IsReadOnly - { - get - { - return ((ICollection>)_dictionary).IsReadOnly; - } - } + bool ICollection>.IsReadOnly => false; /// - public Dictionary.KeyCollection Keys + public ICollection Keys { get { - return _dictionary.Keys; - } - } - - /// - ICollection IDictionary.Keys - { - get - { - return _dictionary.Keys; + EnsureWritable(); + return InnerDictionary.Keys; } } @@ -163,25 +151,18 @@ namespace Microsoft.AspNet.Routing { get { - return _dictionary.Keys; + EnsureWritable(); + return InnerDictionary.Keys; } } /// - public Dictionary.ValueCollection Values + public ICollection Values { get { - return _dictionary.Values; - } - } - - /// - ICollection IDictionary.Values - { - get - { - return _dictionary.Values; + EnsureWritable(); + return InnerDictionary.Values; } } @@ -189,14 +170,16 @@ namespace Microsoft.AspNet.Routing { get { - return _dictionary.Values; + EnsureWritable(); + return InnerDictionary.Values; } } /// void ICollection>.Add(KeyValuePair item) { - ((ICollection>)_dictionary).Add(item); + EnsureWritable(); + ((ICollection>)InnerDictionary).Add(item); } /// @@ -207,19 +190,22 @@ namespace Microsoft.AspNet.Routing throw new ArgumentNullException(nameof(key)); } - _dictionary.Add(key, value); + EnsureWritable(); + InnerDictionary.Add(key, value); } /// public void Clear() { - _dictionary.Clear(); + EnsureWritable(); + InnerDictionary.Clear(); } /// bool ICollection>.Contains(KeyValuePair item) { - return ((ICollection>)_dictionary).Contains(item); + EnsureWritable(); + return ((ICollection>)InnerDictionary).Contains(item); } /// @@ -230,7 +216,27 @@ namespace Microsoft.AspNet.Routing throw new ArgumentNullException(nameof(key)); } - return _dictionary.ContainsKey(key); + if (InnerDictionary != null) + { + return InnerDictionary.ContainsKey(key); + } + else if (Properties != null) + { + for (var i = 0; i < Properties.Length; i++) + { + var property = Properties[i]; + if (Comparer.Equals(property.Name, key)) + { + return true; + } + } + + return false; + } + else + { + return false; + } } /// @@ -243,31 +249,33 @@ namespace Microsoft.AspNet.Routing throw new ArgumentNullException(nameof(array)); } - ((ICollection>)_dictionary).CopyTo(array, arrayIndex); + EnsureWritable(); + ((ICollection>)InnerDictionary).CopyTo(array, arrayIndex); } /// - public Dictionary.Enumerator GetEnumerator() + public Enumerator GetEnumerator() { - return _dictionary.GetEnumerator(); + return new Enumerator(this); } /// IEnumerator> IEnumerable>.GetEnumerator() { - return _dictionary.GetEnumerator(); + return GetEnumerator(); } /// IEnumerator IEnumerable.GetEnumerator() { - return _dictionary.GetEnumerator(); + return GetEnumerator(); } /// bool ICollection>.Remove(KeyValuePair item) { - return ((ICollection>)_dictionary).Remove(item); + EnsureWritable(); + return ((ICollection>)InnerDictionary).Remove(item); } /// @@ -278,7 +286,8 @@ namespace Microsoft.AspNet.Routing throw new ArgumentNullException(nameof(key)); } - return _dictionary.Remove(key); + EnsureWritable(); + return InnerDictionary.Remove(key); } /// @@ -289,7 +298,115 @@ namespace Microsoft.AspNet.Routing throw new ArgumentNullException(nameof(key)); } - return _dictionary.TryGetValue(key, out value); + if (InnerDictionary != null) + { + return InnerDictionary.TryGetValue(key, out value); + } + else if (Properties != null) + { + for (var i = 0; i < Properties.Length; i++) + { + var property = Properties[i]; + if (Comparer.Equals(property.Name, key)) + { + value = property.ValueGetter(Value); + return true; + } + } + + value = null; + return false; + } + else + { + value = null; + return false; + } + } + + private void EnsureWritable() + { + if (InnerDictionary == null && Properties == null) + { + InnerDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + else if (InnerDictionary == null) + { + InnerDictionary = new Dictionary(Properties.Length, StringComparer.OrdinalIgnoreCase); + + for (var i = 0; i < Properties.Length; i++) + { + var property = Properties[i]; + InnerDictionary.Add(property.Property.Name, property.ValueGetter(Value)); + } + } + } + + public struct Enumerator : IEnumerator> + { + private readonly RouteValueDictionary _dictionary; + + private int _index; + private Dictionary.Enumerator _enumerator; + + public Enumerator(RouteValueDictionary dictionary) + { + if (dictionary == null) + { + throw new ArgumentNullException(); + } + + _dictionary = dictionary; + + Current = default(KeyValuePair); + _index = -1; + _enumerator = _dictionary.InnerDictionary == null ? + default(Dictionary.Enumerator) : + _dictionary.InnerDictionary.GetEnumerator(); + } + + public KeyValuePair Current { get; private set; } + + object IEnumerator.Current => Current; + + public void Dispose() + { + } + + public bool MoveNext() + { + if (_dictionary?.InnerDictionary != null) + { + if (_enumerator.MoveNext()) + { + Current = _enumerator.Current; + return true; + } + } + else if (_dictionary?.Properties != null) + { + _index++; + if (_index < _dictionary.Properties.Length) + { + var property = _dictionary.Properties[_index]; + var value = property.ValueGetter(_dictionary.Value); + Current = new KeyValuePair(property.Name, value); + return true; + } + } + + Current = default(KeyValuePair); + return false; + } + + public void Reset() + { + Current = default(KeyValuePair); + _index = -1; + _enumerator = _dictionary?.InnerDictionary == null ? + default(Dictionary.Enumerator) : + _dictionary.InnerDictionary.GetEnumerator(); + } } } } diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs index ce64b9959f..183effd021 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs @@ -28,10 +28,9 @@ namespace Microsoft.AspNet.Routing.Template } // Step 1: Get the list of values we're going to try to use to match and generate this URI - public TemplateValuesResult GetValues(IDictionary ambientValues, - IDictionary values) + public TemplateValuesResult GetValues(RouteValueDictionary ambientValues, RouteValueDictionary values) { - var context = new TemplateBindingContext(_defaults, values); + var context = new TemplateBindingContext(_defaults); // Find out which entries in the URI are valid for the URI we want to generate. // If the URI had ordered parameters a="1", b="2", c="3" and the new values @@ -171,7 +170,7 @@ namespace Microsoft.AspNet.Routing.Template } // Step 2: If the route is a match generate the appropriate URI - public string BindValues(IDictionary acceptedValues) + public string BindValues(RouteValueDictionary acceptedValues) { var context = new UriBuildingContext(); @@ -355,15 +354,8 @@ namespace Microsoft.AspNet.Routing.Template private readonly RouteValueDictionary _acceptedValues; private readonly RouteValueDictionary _filters; - public TemplateBindingContext( - IReadOnlyDictionary defaults, - IDictionary values) + public TemplateBindingContext(IReadOnlyDictionary defaults) { - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - _defaults = defaults; _acceptedValues = new RouteValueDictionary(); diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs b/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs index cc32a00f04..e20bc5316c 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs @@ -33,7 +33,7 @@ namespace Microsoft.AspNet.Routing.Template public RouteTemplate Template { get; private set; } - public IDictionary Match(PathString path) + public RouteValueDictionary Match(PathString path) { var i = 0; var pathTokenizer = new PathTokenizer(path); diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index 48acbe6d7d..6a4c546355 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -317,8 +317,8 @@ namespace Microsoft.AspNet.Routing.Template } private static void MergeValues( - IDictionary destination, - IDictionary values) + RouteValueDictionary destination, + RouteValueDictionary values) { foreach (var kvp in values) { diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateValuesResult.cs b/src/Microsoft.AspNet.Routing/Template/TemplateValuesResult.cs index 767b04fcd7..6aeb85d515 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateValuesResult.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateValuesResult.cs @@ -1,8 +1,6 @@ // 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.AspNet.Routing.Template { /// @@ -13,7 +11,7 @@ namespace Microsoft.AspNet.Routing.Template /// /// The set of values that will appear in the URL. /// - public IDictionary AcceptedValues { get; set; } + public RouteValueDictionary AcceptedValues { get; set; } /// /// The set of values that that were supplied for URL generation. @@ -26,6 +24,6 @@ namespace Microsoft.AspNet.Routing.Template /// Implicit (ambient) values which are invalidated due to changes in values lexically earlier in the /// route template are excluded from this set. /// - public IDictionary CombinedValues { get; set; } + public RouteValueDictionary CombinedValues { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/VirtualPathContext.cs b/src/Microsoft.AspNet.Routing/VirtualPathContext.cs index 8134f2fca6..ac67152c52 100644 --- a/src/Microsoft.AspNet.Routing/VirtualPathContext.cs +++ b/src/Microsoft.AspNet.Routing/VirtualPathContext.cs @@ -8,17 +8,19 @@ namespace Microsoft.AspNet.Routing { public class VirtualPathContext { - public VirtualPathContext(HttpContext httpContext, - IDictionary ambientValues, - IDictionary values) + public VirtualPathContext( + HttpContext httpContext, + RouteValueDictionary ambientValues, + RouteValueDictionary values) : this(httpContext, ambientValues, values, null) { } - public VirtualPathContext(HttpContext context, - IDictionary ambientValues, - IDictionary values, - string routeName) + public VirtualPathContext( + HttpContext context, + RouteValueDictionary ambientValues, + RouteValueDictionary values, + string routeName) { Context = context; AmbientValues = ambientValues; @@ -26,16 +28,16 @@ namespace Microsoft.AspNet.Routing RouteName = routeName; } - public string RouteName { get; private set; } + public string RouteName { get; } public IDictionary ProvidedValues { get; set; } - public IDictionary AmbientValues { get; private set; } + public RouteValueDictionary AmbientValues { get; } - public HttpContext Context { get; private set; } + public HttpContext Context { get; } public bool IsBound { get; set; } - public IDictionary Values { get; private set; } + public RouteValueDictionary Values { get; } } } diff --git a/src/Microsoft.AspNet.Routing/VirtualPathData.cs b/src/Microsoft.AspNet.Routing/VirtualPathData.cs index 326afac34d..4b056b40b8 100644 --- a/src/Microsoft.AspNet.Routing/VirtualPathData.cs +++ b/src/Microsoft.AspNet.Routing/VirtualPathData.cs @@ -2,7 +2,6 @@ // 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 Microsoft.AspNet.Http; namespace Microsoft.AspNet.Routing @@ -13,7 +12,7 @@ namespace Microsoft.AspNet.Routing /// public class VirtualPathData { - private readonly IDictionary _dataTokens; + private RouteValueDictionary _dataTokens; /// /// Initializes a new instance of the class. @@ -21,7 +20,7 @@ namespace Microsoft.AspNet.Routing /// The object that is used to generate the URL. /// The generated URL. public VirtualPathData(IRouter router, string virtualPath) - : this(router, virtualPath, dataTokens: new RouteValueDictionary()) + : this(router, virtualPath, dataTokens: null) { } @@ -34,7 +33,7 @@ namespace Microsoft.AspNet.Routing public VirtualPathData( IRouter router, string virtualPath, - IDictionary dataTokens) + RouteValueDictionary dataTokens) : this(router, CreatePathString(virtualPath), dataTokens) { } @@ -48,7 +47,7 @@ namespace Microsoft.AspNet.Routing public VirtualPathData( IRouter router, PathString virtualPath, - IDictionary dataTokens) + RouteValueDictionary dataTokens) { if (router == null) { @@ -57,23 +56,23 @@ namespace Microsoft.AspNet.Routing Router = router; VirtualPath = virtualPath; - - _dataTokens = new RouteValueDictionary(); - if (dataTokens != null) - { - foreach (var dataToken in dataTokens) - { - _dataTokens.Add(dataToken.Key, dataToken.Value); - } - } + _dataTokens = dataTokens == null ? null : new RouteValueDictionary(dataTokens); } /// /// Gets the collection of custom values for the . /// - public IDictionary DataTokens + public RouteValueDictionary DataTokens { - get { return _dataTokens; } + get + { + if (_dataTokens == null) + { + _dataTokens = new RouteValueDictionary(); + } + + return _dataTokens; + } } /// diff --git a/src/Microsoft.AspNet.Routing/project.json b/src/Microsoft.AspNet.Routing/project.json index 1cdf02f18f..1164f91961 100644 --- a/src/Microsoft.AspNet.Routing/project.json +++ b/src/Microsoft.AspNet.Routing/project.json @@ -9,21 +9,25 @@ "warningsAsErrors": true, "keyFile": "../../tools/Key.snk" }, - "dependencies": { - "Microsoft.AspNet.Http.Extensions": "1.0.0-*", - "Microsoft.AspNet.Routing.DecisionTree.Sources": { - "type": "build", - "version": "1.0.0-*" - }, - "Microsoft.Extensions.HashCodeCombiner.Sources": { - "type": "build", - "version": "1.0.0-*" - }, - "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", - "Microsoft.Extensions.OptionsModel": "1.0.0-*" + "dependencies": { + "Microsoft.AspNet.Http.Extensions": "1.0.0-*", + "Microsoft.AspNet.Routing.DecisionTree.Sources": { + "type": "build", + "version": "1.0.0-*" }, + "Microsoft.Extensions.HashCodeCombiner.Sources": { + "type": "build", + "version": "1.0.0-*" + }, + "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", + "Microsoft.Extensions.OptionsModel": "1.0.0-*", + "Microsoft.Extensions.PropertyHelper.Sources": { + "type": "build", + "version": "1.0.0-*" + } + }, "frameworks": { - "net451": {}, + "net451": { }, "dotnet5.4": { "dependencies": { "System.Text.RegularExpressions": "4.0.11-*" diff --git a/test/Microsoft.AspNet.Routing.Tests/RouteValueDictionaryTests.cs b/test/Microsoft.AspNet.Routing.Tests/RouteValueDictionaryTests.cs index 88dd3ac4f0..34e805c88d 100644 --- a/test/Microsoft.AspNet.Routing.Tests/RouteValueDictionaryTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/RouteValueDictionaryTests.cs @@ -165,7 +165,11 @@ namespace Microsoft.AspNet.Routing.Tests // Act & Assert ExceptionAssert.Throws( - () => new RouteValueDictionary(obj), + () => + { + var dictionary = new RouteValueDictionary(obj); + dictionary.Add("Hi", "There"); + }, expected); } diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs index 0be677f6df..b97b307800 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs @@ -127,7 +127,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests public void Binding_WithEmptyAndNull_DefaultValues( string template, IReadOnlyDictionary defaults, - IDictionary values, + RouteValueDictionary values, string expected) { // Arrange @@ -284,8 +284,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests public void GetVirtualPathWithMultiSegmentWithOptionalParam( string template, IReadOnlyDictionary defaults, - IDictionary ambientValues, - IDictionary values, + RouteValueDictionary ambientValues, + RouteValueDictionary values, string expected) { // Arrange @@ -1086,8 +1086,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests private static void RunTest( string template, IReadOnlyDictionary defaults, - IDictionary ambientValues, - IDictionary values, + RouteValueDictionary ambientValues, + RouteValueDictionary values, string expected) { // Arrange diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs index f9a34e6d24..323a1a77ca 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTest.cs @@ -1658,8 +1658,8 @@ namespace Microsoft.AspNet.Routing.Template } private static VirtualPathContext CreateVirtualPathContext( - IDictionary values, - IDictionary ambientValues) + RouteValueDictionary values, + RouteValueDictionary ambientValues) { var context = new Mock(MockBehavior.Strict); context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory)))