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.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<IInlineConstraintResolver, DefaultInlineConstraintResolver>();
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)
{

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.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<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.
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<UriBuildingContext> _pool;
private readonly RouteValueDictionary _defaults;
private readonly RouteValueDictionary _filters;
private readonly RouteTemplate _template;
private readonly UrlEncoder _urlEncoder;
public TemplateBinder(
RouteTemplate template,
UrlEncoder urlEncoder,
ObjectPool<UriBuildingContext> 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<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-*"
},
"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",

View File

@ -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<InvalidOperationException>(() => 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<RouteOptions> options = null)
{
if (loggerFactory == null)
{
loggerFactory = NullLoggerFactory.Instance;
}
if (options == null)
{
options = new RouteOptions();
}
var request = new Mock<HttpRequest>(MockBehavior.Strict);
var optionsAccessor = new Mock<IOptions<RouteOptions>>(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<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);
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<RouteOptions> options = null,
string routeName = null)
{
var optionsAccessor = new Mock<IOptions<RouteOptions>>(MockBehavior.Strict);
optionsAccessor.SetupGet(o => o.Value).Returns(options);
var serviceProvider = new ServiceCollection()
.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance)
.AddSingleton(UrlEncoder.Default)
.AddSingleton(optionsAccessor.Object)
.BuildServiceProvider();
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
services.AddRouting();
if (options != null)
{
services.Configure<RouteOptions>(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<RouteOptions> 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;
};
}
}
}

View File

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

View File

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

View File

@ -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<UriBuildingContext> 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);