Pool state used to generate URL

This commit is contained in:
Ryan Nowak 2015-12-18 12:44:47 -08:00
parent a956fe53c1
commit 33f9bdadef
12 changed files with 379 additions and 255 deletions

View File

@ -4,7 +4,9 @@
using System; using System;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Internal;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.ObjectPool;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
{ {
@ -25,6 +27,13 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddOptions(); services.AddOptions();
services.TryAddTransient<IInlineConstraintResolver, DefaultInlineConstraintResolver>(); services.TryAddTransient<IInlineConstraintResolver, DefaultInlineConstraintResolver>();
services.TryAddSingleton(UrlEncoder.Default); services.TryAddSingleton(UrlEncoder.Default);
services.TryAddSingleton<ObjectPoolProvider>(new DefaultObjectPoolProvider());
services.TryAddSingleton<ObjectPool<UriBuildingContext>>(s =>
{
var provider = s.GetRequiredService<ObjectPoolProvider>();
var encoder = s.GetRequiredService<UrlEncoder>();
return provider.Create<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy(encoder));
});
if (configureOptions != null) if (configureOptions != null)
{ {

View File

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

View File

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

View File

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

View File

@ -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<BufferValue> _buffer;
private readonly UrlEncoder _urlEncoder;
private bool _hasEmptySegment;
private int _lastValueOffset;
public UriBuildingContext(UrlEncoder urlEncoder)
{
_urlEncoder = urlEncoder;
_uri = new StringBuilder();
_buffer = new List<BufferValue>();
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));
}
}
}

View File

@ -10,6 +10,7 @@ using Microsoft.AspNet.Routing.Internal;
using Microsoft.AspNet.Routing.Template; using Microsoft.AspNet.Routing.Template;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;
namespace Microsoft.AspNet.Routing namespace Microsoft.AspNet.Routing
{ {
@ -243,7 +244,8 @@ namespace Microsoft.AspNet.Routing
if (_binder == null) if (_binder == null)
{ {
var urlEncoder = context.RequestServices.GetRequiredService<UrlEncoder>(); var urlEncoder = context.RequestServices.GetRequiredService<UrlEncoder>();
_binder = new TemplateBinder(ParsedTemplate, urlEncoder, Defaults); var pool = context.RequestServices.GetRequiredService<ObjectPool<UriBuildingContext>>();
_binder = new TemplateBinder(urlEncoder, pool, ParsedTemplate, Defaults);
} }
} }

View File

@ -2,40 +2,47 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNet.Http.Extensions; using Microsoft.AspNet.Routing.Internal;
using Microsoft.Extensions.ObjectPool;
namespace Microsoft.AspNet.Routing.Template namespace Microsoft.AspNet.Routing.Template
{ {
public class TemplateBinder public class TemplateBinder
{ {
private readonly UrlEncoder _urlEncoder;
private readonly ObjectPool<UriBuildingContext> _pool;
private readonly RouteValueDictionary _defaults; private readonly RouteValueDictionary _defaults;
private readonly RouteValueDictionary _filters; private readonly RouteValueDictionary _filters;
private readonly RouteTemplate _template; private readonly RouteTemplate _template;
private readonly UrlEncoder _urlEncoder;
public TemplateBinder( public TemplateBinder(
RouteTemplate template,
UrlEncoder urlEncoder, UrlEncoder urlEncoder,
ObjectPool<UriBuildingContext> pool,
RouteTemplate template,
RouteValueDictionary defaults) RouteValueDictionary defaults)
{ {
if (template == null)
{
throw new ArgumentNullException(nameof(template));
}
if (urlEncoder == null) if (urlEncoder == null)
{ {
throw new ArgumentNullException(nameof(urlEncoder)); 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; _urlEncoder = urlEncoder;
_pool = pool;
_template = template;
_defaults = defaults; _defaults = defaults;
// Any default that doesn't have a corresponding parameter is a 'filter' and if a value // 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 // Step 2: If the route is a match generate the appropriate URI
public string BindValues(RouteValueDictionary acceptedValues) 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++) for (var i = 0; i < _template.Segments.Count; i++)
{ {
Debug.Assert(context.BufferState == SegmentState.Beginning); Debug.Assert(context.BufferState == SegmentState.Beginning);
@ -268,7 +281,7 @@ namespace Microsoft.AspNet.Routing.Template
} }
// Generate the query string from the remaining values // Generate the query string from the remaining values
var queryBuilder = new QueryBuilder(); var wroteFirst = false;
foreach (var kvp in acceptedValues) foreach (var kvp in acceptedValues)
{ {
if (_defaults != null && _defaults.ContainsKey(kvp.Key)) if (_defaults != null && _defaults.ContainsKey(kvp.Key))
@ -283,12 +296,22 @@ namespace Microsoft.AspNet.Routing.Template
continue; 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(); return context.ToString();
uri.Append(queryBuilder);
return uri.ToString();
} }
private TemplatePart GetParameter(string name) private TemplatePart GetParameter(string name)
@ -350,7 +373,7 @@ namespace Microsoft.AspNet.Routing.Template
} }
[DebuggerDisplay("{DebuggerToString(),nq}")] [DebuggerDisplay("{DebuggerToString(),nq}")]
private class TemplateBindingContext private struct TemplateBindingContext
{ {
private readonly RouteValueDictionary _defaults; private readonly RouteValueDictionary _defaults;
private readonly RouteValueDictionary _acceptedValues; private readonly RouteValueDictionary _acceptedValues;
@ -396,196 +419,5 @@ namespace Microsoft.AspNet.Routing.Template
return string.Format("{{Accepted: '{0}'}}", string.Join(", ", _acceptedValues.Keys)); 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<BufferValue> _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<BufferValue>();
_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; }
}
} }
} }

View File

@ -21,6 +21,7 @@
"version": "1.0.0-*" "version": "1.0.0-*"
}, },
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*",
"Microsoft.Extensions.ObjectPool": "1.0.0-*",
"Microsoft.Extensions.Options": "1.0.0-*", "Microsoft.Extensions.Options": "1.0.0-*",
"Microsoft.Extensions.PropertyHelper.Sources": { "Microsoft.Extensions.PropertyHelper.Sources": {
"type": "build", "type": "build",

View File

@ -288,7 +288,7 @@ namespace Microsoft.AspNet.Routing
innerRouteCollection.Add(namedRoute); innerRouteCollection.Add(namedRoute);
routeCollection.Add(innerRouteCollection); routeCollection.Add(innerRouteCollection);
var virtualPathContext = CreateVirtualPathContext("Ambiguous", options: new RouteOptions()); var virtualPathContext = CreateVirtualPathContext("Ambiguous");
// Act & Assert // Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => routeCollection.GetVirtualPath(virtualPathContext)); var ex = Assert.Throws<InvalidOperationException>(() => routeCollection.GetVirtualPath(virtualPathContext));
@ -519,31 +519,24 @@ namespace Microsoft.AspNet.Routing
private static VirtualPathContext CreateVirtualPathContext( private static VirtualPathContext CreateVirtualPathContext(
string routeName = null, string routeName = null,
ILoggerFactory loggerFactory = null, ILoggerFactory loggerFactory = null,
RouteOptions options = null) Action<RouteOptions> options = null)
{ {
if (loggerFactory == null) if (loggerFactory == null)
{ {
loggerFactory = NullLoggerFactory.Instance; loggerFactory = NullLoggerFactory.Instance;
} }
if (options == null)
{
options = new RouteOptions();
}
var request = new Mock<HttpRequest>(MockBehavior.Strict); var request = new Mock<HttpRequest>(MockBehavior.Strict);
var optionsAccessor = new Mock<IOptions<RouteOptions>>(MockBehavior.Strict); var services = new ServiceCollection();
optionsAccessor.SetupGet(o => o.Value).Returns(options); services.AddRouting();
if (options != null)
var serviceProvider = new ServiceCollection() {
.AddSingleton(loggerFactory) services.Configure(options);
.AddSingleton(UrlEncoder.Default) }
.AddSingleton(optionsAccessor.Object)
.BuildServiceProvider();
var context = new Mock<HttpContext>(MockBehavior.Strict); var context = new Mock<HttpContext>(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); context.SetupGet(c => c.Request).Returns(request.Object);
return new VirtualPathContext(context.Object, null, null, routeName); return new VirtualPathContext(context.Object, null, null, routeName);
@ -551,21 +544,20 @@ namespace Microsoft.AspNet.Routing
private static VirtualPathContext CreateVirtualPathContext( private static VirtualPathContext CreateVirtualPathContext(
RouteValueDictionary values, RouteValueDictionary values,
RouteOptions options = null, Action<RouteOptions> options = null,
string routeName = null) string routeName = null)
{ {
var optionsAccessor = new Mock<IOptions<RouteOptions>>(MockBehavior.Strict); var services = new ServiceCollection();
optionsAccessor.SetupGet(o => o.Value).Returns(options); services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
services.AddRouting();
var serviceProvider = new ServiceCollection() if (options != null)
.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance) {
.AddSingleton(UrlEncoder.Default) services.Configure<RouteOptions>(options);
.AddSingleton(optionsAccessor.Object) }
.BuildServiceProvider();
var context = new DefaultHttpContext var context = new DefaultHttpContext
{ {
RequestServices = serviceProvider RequestServices = services.BuildServiceProvider(),
}; };
return new VirtualPathContext( return new VirtualPathContext(
@ -626,15 +618,15 @@ namespace Microsoft.AspNet.Routing
return target; return target;
} }
private static RouteOptions GetRouteOptions( private static Action<RouteOptions> GetRouteOptions(
bool lowerCaseUrls = false, bool lowerCaseUrls = false,
bool appendTrailingSlash = false) bool appendTrailingSlash = false)
{ {
var routeOptions = new RouteOptions(); return (options) =>
routeOptions.LowercaseUrls = lowerCaseUrls; {
routeOptions.AppendTrailingSlash = appendTrailingSlash; options.LowercaseUrls = lowerCaseUrls;
options.AppendTrailingSlash = appendTrailingSlash;
return routeOptions; };
} }
} }
} }

View File

@ -1298,13 +1298,13 @@ namespace Microsoft.AspNet.Routing
RouteValueDictionary values, RouteValueDictionary values,
RouteValueDictionary ambientValues) RouteValueDictionary ambientValues)
{ {
var serviceProvider = new ServiceCollection() var services = new ServiceCollection();
.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance) services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
.AddSingleton(UrlEncoder.Default) services.AddRouting();
.BuildServiceProvider();
var context = new DefaultHttpContext var context = new DefaultHttpContext
{ {
RequestServices = serviceProvider RequestServices = services.BuildServiceProvider(),
}; };
return new VirtualPathContext(context, ambientValues, values); return new VirtualPathContext(context, ambientValues, values);

View File

@ -5,7 +5,9 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNet.Routing.Internal;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.WebEncoders.Testing; using Microsoft.Extensions.WebEncoders.Testing;
using Xunit; using Xunit;
@ -114,9 +116,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests
string expected) string expected)
{ {
// Arrange // Arrange
var encoder = new UrlTestEncoder();
var binder = new TemplateBinder( var binder = new TemplateBinder(
encoder,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy(encoder)),
TemplateParser.Parse(template), TemplateParser.Parse(template),
new UrlTestEncoder(),
defaults); defaults);
// Act & Assert // Act & Assert
@ -247,7 +251,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests
new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2" }), new RouteValueDictionary(new {val1 = "someval1", val2 = "someval2" }),
new RouteValueDictionary(), new RouteValueDictionary(),
new RouteValueDictionary(new {val3 = "someval3" }), 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) string expected)
{ {
// Arrange // Arrange
var encoder = new UrlTestEncoder();
var binder = new TemplateBinder( var binder = new TemplateBinder(
encoder,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy(encoder)),
TemplateParser.Parse(template), TemplateParser.Parse(template),
new UrlTestEncoder(),
defaults); defaults);
// Act & Assert // Act & Assert
@ -602,7 +608,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
new RouteValueDictionary(new { controller = "Home" }), new RouteValueDictionary(new { controller = "Home" }),
new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }), new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }),
values, 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] [Fact]
@ -622,7 +629,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests
maxPrice = 100, maxPrice = 100,
custom = "customValue" 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] [Fact]
@ -1091,7 +1099,12 @@ namespace Microsoft.AspNet.Routing.Template.Tests
{ {
// Arrange // Arrange
encoder = encoder ?? new UrlTestEncoder(); 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 // Act & Assert
var result = binder.GetValues(ambientValues, values); var result = binder.GetValues(ambientValues, values);

View File

@ -7,9 +7,11 @@ using System.Linq;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing.Internal;
using Microsoft.AspNet.Routing.Template; using Microsoft.AspNet.Routing.Template;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.WebEncoders.Testing; using Microsoft.Extensions.WebEncoders.Testing;
using Moq; using Moq;
@ -21,6 +23,10 @@ namespace Microsoft.AspNet.Routing.Tree
{ {
private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0); private static readonly RequestDelegate NullHandler = (c) => Task.FromResult(0);
private static UrlEncoder Encoder = UrlTestEncoder.Default;
private static ObjectPool<UriBuildingContext> Pool = new DefaultObjectPoolProvider().Create(
new UriBuilderContextPooledObjectPolicy(Encoder));
[Theory] [Theory]
[InlineData("template/5", "template/{parameter:int}")] [InlineData("template/5", "template/{parameter:int}")]
[InlineData("template/5", "template/{parameter}")] [InlineData("template/5", "template/{parameter}")]
@ -1601,7 +1607,7 @@ namespace Microsoft.AspNet.Routing.Tree
entry.Constraints = constraints; entry.Constraints = constraints;
entry.Defaults = defaults; 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.Order = order;
entry.GenerationPrecedence = RoutePrecedence.ComputeGenerated(entry.Template); entry.GenerationPrecedence = RoutePrecedence.ComputeGenerated(entry.Template);
entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); entry.RequiredLinkValues = new RouteValueDictionary(requiredValues);