diff --git a/src/Microsoft.AspNet.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs index 6daf047e22..f60691f581 100644 --- a/src/Microsoft.AspNet.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -4,7 +4,9 @@ using System; using System.Text.Encodings.Web; using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Routing.Internal; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.ObjectPool; namespace Microsoft.Extensions.DependencyInjection { @@ -25,6 +27,13 @@ namespace Microsoft.Extensions.DependencyInjection services.AddOptions(); services.TryAddTransient(); services.TryAddSingleton(UrlEncoder.Default); + services.TryAddSingleton(new DefaultObjectPoolProvider()); + services.TryAddSingleton>(s => + { + var provider = s.GetRequiredService(); + var encoder = s.GetRequiredService(); + return provider.Create(new UriBuilderContextPooledObjectPolicy(encoder)); + }); if (configureOptions != null) { diff --git a/src/Microsoft.AspNet.Routing/Internal/BufferValue.cs b/src/Microsoft.AspNet.Routing/Internal/BufferValue.cs new file mode 100644 index 0000000000..b6165be207 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Internal/BufferValue.cs @@ -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.AspNet.Routing.Internal +{ + public struct BufferValue + { + public BufferValue(string value, bool requiresEncoding) + { + Value = value; + RequiresEncoding = requiresEncoding; + } + + public bool RequiresEncoding { get; } + + public string Value { get; } + } +} diff --git a/src/Microsoft.AspNet.Routing/Internal/SegmentState.cs b/src/Microsoft.AspNet.Routing/Internal/SegmentState.cs new file mode 100644 index 0000000000..66a511f2bf --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Internal/SegmentState.cs @@ -0,0 +1,17 @@ +// 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.AspNet.Routing.Internal +{ + // Segments are treated as all-or-none. We should never output a partial segment. + // If we add any subsegment of this segment to the generated URI, we have to add + // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we + // used a value for {p1}, we have to output the entire segment up to the next "/". + // Otherwise we could end up with the partial segment "v1" instead of the entire + // segment "v1-v2.xml". + public enum SegmentState + { + Beginning, + Inside, + } +} diff --git a/src/Microsoft.AspNet.Routing/Internal/UriBuilderContextPooledObjectPolicy.cs b/src/Microsoft.AspNet.Routing/Internal/UriBuilderContextPooledObjectPolicy.cs new file mode 100644 index 0000000000..77460600b3 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Internal/UriBuilderContextPooledObjectPolicy.cs @@ -0,0 +1,35 @@ +// 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.Text.Encodings.Web; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNet.Routing.Internal +{ + public class UriBuilderContextPooledObjectPolicy : IPooledObjectPolicy + { + private readonly UrlEncoder _encoder; + + public UriBuilderContextPooledObjectPolicy(UrlEncoder encoder) + { + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); + } + + _encoder = encoder; + } + + public UriBuildingContext Create() + { + return new UriBuildingContext(_encoder); + } + + public bool Return(UriBuildingContext obj) + { + obj.Clear(); + return true; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Internal/UriBuildingContext.cs b/src/Microsoft.AspNet.Routing/Internal/UriBuildingContext.cs new file mode 100644 index 0000000000..04e278644a --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Internal/UriBuildingContext.cs @@ -0,0 +1,199 @@ +// 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; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Text.Encodings.Web; + +namespace Microsoft.AspNet.Routing.Internal +{ + [DebuggerDisplay("{DebuggerToString(),nq}")] + public class UriBuildingContext + { + // Holds the 'accepted' parts of the uri. + private readonly StringBuilder _uri; + + // Holds the 'optional' parts of the uri. We need a secondary buffer to handle cases where an optional + // segment is in the middle of the uri. We don't know if we need to write it out - if it's + // followed by other optional segments than we will just throw it away. + private readonly List _buffer; + private readonly UrlEncoder _urlEncoder; + + private bool _hasEmptySegment; + private int _lastValueOffset; + + public UriBuildingContext(UrlEncoder urlEncoder) + { + _urlEncoder = urlEncoder; + _uri = new StringBuilder(); + _buffer = new List(); + Writer = new StringWriter(_uri); + _lastValueOffset = -1; + + BufferState = SegmentState.Beginning; + UriState = SegmentState.Beginning; + } + + public SegmentState BufferState { get; private set; } + + public SegmentState UriState { get; private set; } + + public TextWriter Writer { get; } + + public bool Accept(string value) + { + if (string.IsNullOrEmpty(value)) + { + if (UriState == SegmentState.Inside || BufferState == SegmentState.Inside) + { + // We can't write an 'empty' part inside a segment + return false; + } + else + { + _hasEmptySegment = true; + return true; + } + } + else if (_hasEmptySegment) + { + // We're trying to write text after an empty segment - this is not allowed. + return false; + } + + for (var i = 0; i < _buffer.Count; i++) + { + if (_buffer[i].RequiresEncoding) + { + _urlEncoder.Encode(Writer, _buffer[i].Value); + } + else + { + _uri.Append(_buffer[i].Value); + } + } + _buffer.Clear(); + + if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) + { + if (_uri.Length != 0) + { + _uri.Append("/"); + } + } + + BufferState = SegmentState.Inside; + UriState = SegmentState.Inside; + + _lastValueOffset = _uri.Length; + // Allow the first segment to have a leading slash. + // This prevents the leading slash from PathString segments from being encoded. + if (_uri.Length == 0 && value.Length > 0 && value[0] == '/') + { + _uri.Append("/"); + _urlEncoder.Encode(Writer, value, 1, value.Length - 1); + } + else + { + _urlEncoder.Encode(Writer, value); + } + + return true; + } + + public void Remove(string literal) + { + Debug.Assert(_lastValueOffset != -1, "Cannot invoke Remove more than once."); + _uri.Length = _lastValueOffset; + _lastValueOffset = -1; + } + + public bool Buffer(string value) + { + if (string.IsNullOrEmpty(value)) + { + if (BufferState == SegmentState.Inside) + { + // We can't write an 'empty' part inside a segment + return false; + } + else + { + _hasEmptySegment = true; + return true; + } + } + else if (_hasEmptySegment) + { + // We're trying to write text after an empty segment - this is not allowed. + return false; + } + + if (UriState == SegmentState.Inside) + { + // We've already written part of this segment so there's no point in buffering, we need to + // write out the rest or give up. + var result = Accept(value); + + // We've already checked the conditions that could result in a rejected part, so this should + // always be true. + Debug.Assert(result); + + return result; + } + + if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) + { + if (_uri.Length != 0 || _buffer.Count != 0) + { + _buffer.Add(new BufferValue("/", requiresEncoding: false)); + } + + BufferState = SegmentState.Inside; + } + + _buffer.Add(new BufferValue(value, requiresEncoding: true)); + return true; + } + + public void EndSegment() + { + BufferState = SegmentState.Beginning; + UriState = SegmentState.Beginning; + } + + public void Clear() + { + _uri.Clear(); + if (_uri.Capacity > 128) + { + // We don't want to retain too much memory if this is getting pooled. + _uri.Capacity = 128; + } + + _buffer.Clear(); + if (_buffer.Capacity > 8) + { + _buffer.Capacity = 8; + } + + _hasEmptySegment = false; + _lastValueOffset = -1; + BufferState = SegmentState.Beginning; + UriState = SegmentState.Beginning; + } + + public override string ToString() + { + // We can ignore any currently buffered segments - they are are guaranteed to be 'defaults'. + return _uri.ToString(); + } + + private string DebuggerToString() + { + return string.Format("{{Accepted: '{0}' Buffered: '{1}'}}", _uri, string.Join("", _buffer)); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RouteBase.cs b/src/Microsoft.AspNet.Routing/RouteBase.cs index 0167483b63..c09647bd24 100644 --- a/src/Microsoft.AspNet.Routing/RouteBase.cs +++ b/src/Microsoft.AspNet.Routing/RouteBase.cs @@ -10,6 +10,7 @@ using Microsoft.AspNet.Routing.Internal; using Microsoft.AspNet.Routing.Template; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; namespace Microsoft.AspNet.Routing { @@ -243,7 +244,8 @@ namespace Microsoft.AspNet.Routing if (_binder == null) { var urlEncoder = context.RequestServices.GetRequiredService(); - _binder = new TemplateBinder(ParsedTemplate, urlEncoder, Defaults); + var pool = context.RequestServices.GetRequiredService>(); + _binder = new TemplateBinder(urlEncoder, pool, ParsedTemplate, Defaults); } } diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs index dd2db68b78..7b562ba542 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs @@ -2,40 +2,47 @@ // 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.Diagnostics; using System.Globalization; -using System.IO; -using System.Text; using System.Text.Encodings.Web; -using Microsoft.AspNet.Http.Extensions; +using Microsoft.AspNet.Routing.Internal; +using Microsoft.Extensions.ObjectPool; namespace Microsoft.AspNet.Routing.Template { public class TemplateBinder { + private readonly UrlEncoder _urlEncoder; + private readonly ObjectPool _pool; + private readonly RouteValueDictionary _defaults; private readonly RouteValueDictionary _filters; private readonly RouteTemplate _template; - private readonly UrlEncoder _urlEncoder; public TemplateBinder( - RouteTemplate template, UrlEncoder urlEncoder, + ObjectPool pool, + RouteTemplate template, RouteValueDictionary defaults) { - if (template == null) - { - throw new ArgumentNullException(nameof(template)); - } - if (urlEncoder == null) { throw new ArgumentNullException(nameof(urlEncoder)); } - _template = template; + if (pool == null) + { + throw new ArgumentNullException(nameof(pool)); + } + + if (template == null) + { + throw new ArgumentNullException(nameof(template)); + } + _urlEncoder = urlEncoder; + _pool = pool; + _template = template; _defaults = defaults; // Any default that doesn't have a corresponding parameter is a 'filter' and if a value @@ -189,8 +196,14 @@ namespace Microsoft.AspNet.Routing.Template // Step 2: If the route is a match generate the appropriate URI public string BindValues(RouteValueDictionary acceptedValues) { - var context = new UriBuildingContext(_urlEncoder); + var context = _pool.Get(); + var result = BindValues(context, acceptedValues); + _pool.Return(context); + return result; + } + private string BindValues(UriBuildingContext context, RouteValueDictionary acceptedValues) + { for (var i = 0; i < _template.Segments.Count; i++) { Debug.Assert(context.BufferState == SegmentState.Beginning); @@ -268,7 +281,7 @@ namespace Microsoft.AspNet.Routing.Template } // Generate the query string from the remaining values - var queryBuilder = new QueryBuilder(); + var wroteFirst = false; foreach (var kvp in acceptedValues) { if (_defaults != null && _defaults.ContainsKey(kvp.Key)) @@ -283,12 +296,22 @@ namespace Microsoft.AspNet.Routing.Template continue; } - queryBuilder.Add(kvp.Key, converted); + if (!wroteFirst) + { + context.Writer.Write('?'); + wroteFirst = true; + } + else + { + context.Writer.Write('&'); + } + + _urlEncoder.Encode(context.Writer, kvp.Key); + context.Writer.Write('='); + _urlEncoder.Encode(context.Writer, converted); } - var uri = context.GetUri(); - uri.Append(queryBuilder); - return uri.ToString(); + return context.ToString(); } private TemplatePart GetParameter(string name) @@ -350,7 +373,7 @@ namespace Microsoft.AspNet.Routing.Template } [DebuggerDisplay("{DebuggerToString(),nq}")] - private class TemplateBindingContext + private struct TemplateBindingContext { private readonly RouteValueDictionary _defaults; private readonly RouteValueDictionary _acceptedValues; @@ -396,196 +419,5 @@ namespace Microsoft.AspNet.Routing.Template return string.Format("{{Accepted: '{0}'}}", string.Join(", ", _acceptedValues.Keys)); } } - - [DebuggerDisplay("{DebuggerToString(),nq}")] - private class UriBuildingContext - { - // Holds the 'accepted' parts of the uri. - private readonly StringBuilder _uri; - - // Holds the 'optional' parts of the uri. We need a secondary buffer to handle cases where an optional - // segment is in the middle of the uri. We don't know if we need to write it out - if it's - // followed by other optional segments than we will just throw it away. - private readonly List _buffer; - private readonly UrlEncoder _urlEncoder; - private readonly StringWriter _uriWriter; - - private bool _hasEmptySegment; - private int _lastValueOffset; - - public UriBuildingContext(UrlEncoder urlEncoder) - { - _urlEncoder = urlEncoder; - _uri = new StringBuilder(); - _buffer = new List(); - _uriWriter = new StringWriter(_uri); - _lastValueOffset = -1; - - BufferState = SegmentState.Beginning; - UriState = SegmentState.Beginning; - } - - public SegmentState BufferState { get; private set; } - - public SegmentState UriState { get; private set; } - - public bool Accept(string value) - { - if (string.IsNullOrEmpty(value)) - { - if (UriState == SegmentState.Inside || BufferState == SegmentState.Inside) - { - // We can't write an 'empty' part inside a segment - return false; - } - else - { - _hasEmptySegment = true; - return true; - } - } - else if (_hasEmptySegment) - { - // We're trying to write text after an empty segment - this is not allowed. - return false; - } - - for (var i = 0; i < _buffer.Count; i++) - { - if (_buffer[i].RequiresEncoding) - { - _urlEncoder.Encode(_uriWriter, _buffer[i].Value); - } - else - { - _uri.Append(_buffer[i].Value); - } - } - _buffer.Clear(); - - if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) - { - if (_uri.Length != 0) - { - _uri.Append("/"); - } - } - - BufferState = SegmentState.Inside; - UriState = SegmentState.Inside; - - _lastValueOffset = _uri.Length; - // Allow the first segment to have a leading slash. - // This prevents the leading slash from PathString segments from being encoded. - if (_uri.Length == 0 && value.Length > 0 && value[0] == '/') - { - _uri.Append("/"); - _urlEncoder.Encode(_uriWriter, value, 1, value.Length - 1); - } - else - { - _urlEncoder.Encode(_uriWriter, value); - } - - return true; - } - - public void Remove(string literal) - { - Debug.Assert(_lastValueOffset != -1, "Cannot invoke Remove more than once."); - _uri.Length = _lastValueOffset; - _lastValueOffset = -1; - } - - public bool Buffer(string value) - { - if (string.IsNullOrEmpty(value)) - { - if (BufferState == SegmentState.Inside) - { - // We can't write an 'empty' part inside a segment - return false; - } - else - { - _hasEmptySegment = true; - return true; - } - } - else if (_hasEmptySegment) - { - // We're trying to write text after an empty segment - this is not allowed. - return false; - } - - if (UriState == SegmentState.Inside) - { - // We've already written part of this segment so there's no point in buffering, we need to - // write out the rest or give up. - var result = Accept(value); - - // We've already checked the conditions that could result in a rejected part, so this should - // always be true. - Debug.Assert(result); - - return result; - } - - if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) - { - if (_uri.Length != 0 || _buffer.Count != 0) - { - _buffer.Add(new BufferValue("/", requiresEncoding: false)); - } - - BufferState = SegmentState.Inside; - } - - _buffer.Add(new BufferValue(value, requiresEncoding: true)); - return true; - } - - internal void EndSegment() - { - BufferState = SegmentState.Beginning; - UriState = SegmentState.Beginning; - } - - internal StringBuilder GetUri() - { - // We can ignore any currently buffered segments - they are are guaranteed to be 'defaults'. - return _uri; - } - - private string DebuggerToString() - { - return string.Format("{{Accepted: '{0}' Buffered: '{1}'}}", _uri, string.Join("", _buffer)); - } - } - - // Segments are treated as all-or-none. We should never output a partial segment. - // If we add any subsegment of this segment to the generated URI, we have to add - // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we - // used a value for {p1}, we have to output the entire segment up to the next "/". - // Otherwise we could end up with the partial segment "v1" instead of the entire - // segment "v1-v2.xml". - private enum SegmentState - { - Beginning, - Inside, - } - - private struct BufferValue - { - public BufferValue(string value, bool requiresEncoding) - { - Value = value; - RequiresEncoding = requiresEncoding; - } - - public bool RequiresEncoding { get; } - - public string Value { get; } - } } } diff --git a/src/Microsoft.AspNet.Routing/project.json b/src/Microsoft.AspNet.Routing/project.json index f1f10a540a..e50fade45d 100644 --- a/src/Microsoft.AspNet.Routing/project.json +++ b/src/Microsoft.AspNet.Routing/project.json @@ -21,6 +21,7 @@ "version": "1.0.0-*" }, "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", + "Microsoft.Extensions.ObjectPool": "1.0.0-*", "Microsoft.Extensions.Options": "1.0.0-*", "Microsoft.Extensions.PropertyHelper.Sources": { "type": "build", diff --git a/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs b/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs index 0d30607050..bae0176a91 100644 --- a/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/RouteCollectionTest.cs @@ -288,7 +288,7 @@ namespace Microsoft.AspNet.Routing innerRouteCollection.Add(namedRoute); routeCollection.Add(innerRouteCollection); - var virtualPathContext = CreateVirtualPathContext("Ambiguous", options: new RouteOptions()); + var virtualPathContext = CreateVirtualPathContext("Ambiguous"); // Act & Assert var ex = Assert.Throws(() => routeCollection.GetVirtualPath(virtualPathContext)); @@ -519,31 +519,24 @@ namespace Microsoft.AspNet.Routing private static VirtualPathContext CreateVirtualPathContext( string routeName = null, ILoggerFactory loggerFactory = null, - RouteOptions options = null) + Action options = null) { if (loggerFactory == null) { loggerFactory = NullLoggerFactory.Instance; } - if (options == null) - { - options = new RouteOptions(); - } - var request = new Mock(MockBehavior.Strict); - var optionsAccessor = new Mock>(MockBehavior.Strict); - optionsAccessor.SetupGet(o => o.Value).Returns(options); - - var serviceProvider = new ServiceCollection() - .AddSingleton(loggerFactory) - .AddSingleton(UrlEncoder.Default) - .AddSingleton(optionsAccessor.Object) - .BuildServiceProvider(); + var services = new ServiceCollection(); + services.AddRouting(); + if (options != null) + { + services.Configure(options); + } var context = new Mock(MockBehavior.Strict); - context.SetupGet(m => m.RequestServices).Returns(serviceProvider); + context.SetupGet(m => m.RequestServices).Returns(services.BuildServiceProvider()); context.SetupGet(c => c.Request).Returns(request.Object); return new VirtualPathContext(context.Object, null, null, routeName); @@ -551,21 +544,20 @@ namespace Microsoft.AspNet.Routing private static VirtualPathContext CreateVirtualPathContext( RouteValueDictionary values, - RouteOptions options = null, + Action options = null, string routeName = null) { - var optionsAccessor = new Mock>(MockBehavior.Strict); - optionsAccessor.SetupGet(o => o.Value).Returns(options); - - var serviceProvider = new ServiceCollection() - .AddSingleton(NullLoggerFactory.Instance) - .AddSingleton(UrlEncoder.Default) - .AddSingleton(optionsAccessor.Object) - .BuildServiceProvider(); + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddRouting(); + if (options != null) + { + services.Configure(options); + } var context = new DefaultHttpContext { - RequestServices = serviceProvider + RequestServices = services.BuildServiceProvider(), }; return new VirtualPathContext( @@ -626,15 +618,15 @@ namespace Microsoft.AspNet.Routing return target; } - private static RouteOptions GetRouteOptions( + private static Action GetRouteOptions( bool lowerCaseUrls = false, bool appendTrailingSlash = false) { - var routeOptions = new RouteOptions(); - routeOptions.LowercaseUrls = lowerCaseUrls; - routeOptions.AppendTrailingSlash = appendTrailingSlash; - - return routeOptions; + return (options) => + { + options.LowercaseUrls = lowerCaseUrls; + options.AppendTrailingSlash = appendTrailingSlash; + }; } } } diff --git a/test/Microsoft.AspNet.Routing.Tests/RouteTest.cs b/test/Microsoft.AspNet.Routing.Tests/RouteTest.cs index 19892783cc..f4d3b422a5 100644 --- a/test/Microsoft.AspNet.Routing.Tests/RouteTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/RouteTest.cs @@ -1298,13 +1298,13 @@ namespace Microsoft.AspNet.Routing RouteValueDictionary values, RouteValueDictionary ambientValues) { - var serviceProvider = new ServiceCollection() - .AddSingleton(NullLoggerFactory.Instance) - .AddSingleton(UrlEncoder.Default) - .BuildServiceProvider(); + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddRouting(); + var context = new DefaultHttpContext { - RequestServices = serviceProvider + RequestServices = services.BuildServiceProvider(), }; return new VirtualPathContext(context, ambientValues, values); diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs index f5b4bc71fd..fe064a43fe 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs @@ -5,7 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text.Encodings.Web; +using Microsoft.AspNet.Routing.Internal; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Extensions.WebEncoders.Testing; using Xunit; @@ -114,9 +116,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests string expected) { // Arrange + var encoder = new UrlTestEncoder(); var binder = new TemplateBinder( + encoder, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy(encoder)), TemplateParser.Parse(template), - new UrlTestEncoder(), defaults); // Act & Assert @@ -247,7 +251,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2" }), new RouteValueDictionary(), new RouteValueDictionary(new {val3 = "someval3" }), - "UrlEncode[[Test]]/UrlEncode[[someval1]]UrlEncode[[.]]UrlEncode[[someval2]]?val3=someval3" + "UrlEncode[[Test]]/UrlEncode[[someval1]]UrlEncode[[.]]UrlEncode[[someval2]]?UrlEncode[[val3]]=UrlEncode[[someval3]]" }, }; @@ -261,9 +265,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests string expected) { // Arrange + var encoder = new UrlTestEncoder(); var binder = new TemplateBinder( + encoder, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy(encoder)), TemplateParser.Parse(template), - new UrlTestEncoder(), defaults); // Act & Assert @@ -602,7 +608,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests new RouteValueDictionary(new { controller = "Home" }), new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }), values, - "UrlEncode[[products]]UrlEncode[[.mvc]]/UrlEncode[[showcategory]]/UrlEncode[[123]]?so%3Frt=de%3Fsc&maxPrice=100"); + "UrlEncode[[products]]UrlEncode[[.mvc]]/UrlEncode[[showcategory]]/UrlEncode[[123]]" + + "?UrlEncode[[so?rt]]=UrlEncode[[de?sc]]&UrlEncode[[maxPrice]]=UrlEncode[[100]]"); } [Fact] @@ -622,7 +629,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests maxPrice = 100, custom = "customValue" }), - "UrlEncode[[products]]UrlEncode[[.mvc]]/UrlEncode[[showcategory]]/UrlEncode[[123]]?sort=desc&maxPrice=100"); + "UrlEncode[[products]]UrlEncode[[.mvc]]/UrlEncode[[showcategory]]/UrlEncode[[123]]" + + "?UrlEncode[[sort]]=UrlEncode[[desc]]&UrlEncode[[maxPrice]]=UrlEncode[[100]]"); } [Fact] @@ -1091,7 +1099,12 @@ namespace Microsoft.AspNet.Routing.Template.Tests { // Arrange encoder = encoder ?? new UrlTestEncoder(); - var binder = new TemplateBinder(TemplateParser.Parse(template), encoder, defaults); + + var binder = new TemplateBinder( + encoder, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy(encoder)), + TemplateParser.Parse(template), + defaults); // Act & Assert var result = binder.GetValues(ambientValues, values); diff --git a/test/Microsoft.AspNet.Routing.Tests/Tree/TreeRouterTest.cs b/test/Microsoft.AspNet.Routing.Tests/Tree/TreeRouterTest.cs index 2c1a781b09..2ef5102076 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Tree/TreeRouterTest.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Tree/TreeRouterTest.cs @@ -7,9 +7,11 @@ using System.Linq; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing.Internal; using Microsoft.AspNet.Routing.Template; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Extensions.WebEncoders.Testing; using Moq; @@ -21,6 +23,10 @@ namespace Microsoft.AspNet.Routing.Tree { private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0); + private static UrlEncoder Encoder = UrlTestEncoder.Default; + private static ObjectPool Pool = new DefaultObjectPoolProvider().Create( + new UriBuilderContextPooledObjectPolicy(Encoder)); + [Theory] [InlineData("template/5", "template/{parameter:int}")] [InlineData("template/5", "template/{parameter}")] @@ -1601,7 +1607,7 @@ namespace Microsoft.AspNet.Routing.Tree entry.Constraints = constraints; entry.Defaults = defaults; - entry.Binder = new TemplateBinder(entry.Template, UrlEncoder.Default, defaults); + entry.Binder = new TemplateBinder(Encoder, Pool, entry.Template, defaults); entry.Order = order; entry.GenerationPrecedence = RoutePrecedence.ComputeGenerated(entry.Template); entry.RequiredLinkValues = new RouteValueDictionary(requiredValues);