Port TemplateBinder to dispatcher

This commit is contained in:
Ryan Nowak 2017-10-20 18:36:11 -07:00
parent eebc7db2ca
commit 2d661396df
22 changed files with 1761 additions and 420 deletions

View File

@ -31,8 +31,8 @@ namespace Microsoft.AspNetCore.Dispatcher.Performance
var treeBuilder = new TreeRouteBuilder(
NullLoggerFactory.Instance,
UrlEncoder.Default,
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default)),
new DefaultInlineConstraintResolver(new OptionsManager<RouteOptions>(new OptionsFactory<RouteOptions>(Enumerable.Empty<IConfigureOptions<RouteOptions>>(), Enumerable.Empty<IPostConfigureOptions<RouteOptions>>()))));
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions())));
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets"), "default", 0);
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets/{id}"), "default", 0);

View File

@ -6,8 +6,8 @@ using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Dispatcher;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Routing.Tree;
using Microsoft.Extensions.Logging.Abstractions;
@ -31,8 +31,8 @@ namespace Microsoft.AspNetCore.Routing.Performance
var treeBuilder = new TreeRouteBuilder(
NullLoggerFactory.Instance,
UrlEncoder.Default,
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default)),
new DefaultInlineConstraintResolver(new OptionsManager<RouteOptions>(new OptionsFactory<RouteOptions>(Enumerable.Empty<IConfigureOptions<RouteOptions>>(), Enumerable.Empty<IPostConfigureOptions<RouteOptions>>()))));
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions())));
treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets"), "default", 0);
treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets/{id}"), "default", 0);

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Routing.Internal
namespace Microsoft.AspNetCore.Dispatcher
{
public struct BufferValue
{

View File

@ -5,6 +5,7 @@ using System;
using Microsoft.AspNetCore.Dispatcher;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
namespace Microsoft.Extensions.DependencyInjection
@ -18,7 +19,7 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentNullException(nameof(services));
}
// Adds the EndpointMiddleare at the end of the pipeline if the DispatcherMiddleware is in use.
// Adds the EndpointMiddleware at the end of the pipeline if the DispatcherMiddleware is in use.
services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupFilter, DispatcherEndpointStartupFilter>());
// Adds a default dispatcher which will collect all data sources and endpoint selectors from DI.
@ -27,6 +28,15 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddSingleton<AddressTable, DefaultAddressTable>();
services.AddSingleton<TemplateAddressSelector>();
//
// Infrastructure
//
services.AddSingleton<ObjectPool<UriBuildingContext>>(s =>
{
var provider = s.GetRequiredService<ObjectPoolProvider>();
return provider.Create<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy());
});
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHandlerFactory, TemplateEndpointHandlerFactory>());
return services;

View File

@ -16,6 +16,7 @@
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.HashCodeCombiner.Sources" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" />
<PackageReference Include="Microsoft.Extensions.Options" />
</ItemGroup>

View File

@ -0,0 +1,441 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Diagnostics;
using System.Globalization;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Dispatcher.Patterns;
using Microsoft.Extensions.ObjectPool;
namespace Microsoft.AspNetCore.Dispatcher
{
public class RoutePatternBinder
{
private readonly UrlEncoder _urlEncoder;
private readonly ObjectPool<UriBuildingContext> _pool;
private readonly DispatcherValueCollection _defaults;
private readonly DispatcherValueCollection _filters;
private readonly RoutePattern _pattern;
public RoutePatternBinder(
UrlEncoder urlEncoder,
ObjectPool<UriBuildingContext> pool,
RoutePattern template,
DispatcherValueCollection defaults)
{
if (urlEncoder == null)
{
throw new ArgumentNullException(nameof(urlEncoder));
}
if (pool == null)
{
throw new ArgumentNullException(nameof(pool));
}
if (template == null)
{
throw new ArgumentNullException(nameof(template));
}
_urlEncoder = urlEncoder;
_pool = pool;
_pattern = template;
_defaults = defaults;
// Any default that doesn't have a corresponding parameter is a 'filter' and if a value
// is provided for that 'filter' it must match the value in defaults.
_filters = new DispatcherValueCollection(_defaults);
foreach (var parameter in _pattern.Parameters)
{
_filters.Remove(parameter.Name);
}
}
// Step 1: Get the list of values we're going to try to use to match and generate this URI
public (DispatcherValueCollection acceptedValues, DispatcherValueCollection combinedValues) GetValues(DispatcherValueCollection ambientValues, DispatcherValueCollection 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
// specified that b="9", then we need to invalidate everything after it. The new
// values should then be a="1", b="9", c=<no value>.
//
// We also handle the case where a parameter is optional but has no value - we shouldn't
// accept additional parameters that appear *after* that parameter.
for (var i = 0; i < _pattern.Parameters.Count; i++)
{
var parameter = _pattern.Parameters[i];
// If it's a parameter subsegment, examine the current value to see if it matches the new value
var parameterName = parameter.Name;
object newParameterValue;
var hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue);
object currentParameterValue = null;
var hasCurrentParameterValue = ambientValues != null &&
ambientValues.TryGetValue(parameterName, out currentParameterValue);
if (hasNewParameterValue && hasCurrentParameterValue)
{
if (!RoutePartsEqual(currentParameterValue, newParameterValue))
{
// Stop copying current values when we find one that doesn't match
break;
}
}
if (!hasNewParameterValue &&
!hasCurrentParameterValue &&
_defaults?.ContainsKey(parameter.Name) != true)
{
// This is an unsatisfied parameter value and there are no defaults. We might still
// be able to generate a URL but we should stop 'accepting' ambient values.
//
// This might be a case like:
// template: a/{b?}/{c?}
// ambient: { c = 17 }
// values: { }
//
// We can still generate a URL from this ("/a") but we shouldn't accept 'c' because
// we can't use it.
//
// In the example above we should fall into this block for 'b'.
break;
}
// If the parameter is a match, add it to the list of values we will use for URI generation
if (hasNewParameterValue)
{
if (IsRoutePartNonEmpty(newParameterValue))
{
context.Accept(parameterName, newParameterValue);
}
}
else
{
if (hasCurrentParameterValue)
{
context.Accept(parameterName, currentParameterValue);
}
}
}
// Add all remaining new values to the list of values we will use for URI generation
foreach (var kvp in values)
{
if (IsRoutePartNonEmpty(kvp.Value))
{
context.Accept(kvp.Key, kvp.Value);
}
}
// Accept all remaining default values if they match a required parameter
for (var i = 0; i < _pattern.Parameters.Count; i++)
{
var parameter = _pattern.Parameters[i];
if (parameter.IsOptional || parameter.IsCatchAll)
{
continue;
}
if (context.NeedsValue(parameter.Name))
{
// Add the default value only if there isn't already a new value for it and
// only if it actually has a default value, which we determine based on whether
// the parameter value is required.
context.AcceptDefault(parameter.Name);
}
}
// Validate that all required parameters have a value.
for (var i = 0; i < _pattern.Parameters.Count; i++)
{
var parameter = _pattern.Parameters[i];
if (parameter.IsOptional || parameter.IsCatchAll)
{
continue;
}
if (!context.AcceptedValues.ContainsKey(parameter.Name))
{
// We don't have a value for this parameter, so we can't generate a url.
return (null, null);
}
}
// Any default values that don't appear as parameters are treated like filters. Any new values
// provided must match these defaults.
foreach (var filter in _filters)
{
var parameter = _pattern.GetParameter(filter.Key);
if (parameter != null)
{
continue;
}
object value;
if (values.TryGetValue(filter.Key, out value))
{
if (!RoutePartsEqual(value, filter.Value))
{
// If there is a non-parameterized value in the route and there is a
// new value for it and it doesn't match, this route won't match.
return (null, null);
}
}
}
// Add any ambient values that don't match parameters - they need to be visible to constraints
// but they will ignored by link generation.
var combinedValues = new DispatcherValueCollection(context.AcceptedValues);
if (ambientValues != null)
{
foreach (var kvp in ambientValues)
{
if (IsRoutePartNonEmpty(kvp.Value))
{
var parameter = _pattern.GetParameter(kvp.Key);
if (parameter == null && !context.AcceptedValues.ContainsKey(kvp.Key))
{
combinedValues.Add(kvp.Key, kvp.Value);
}
}
}
}
return (context.AcceptedValues, combinedValues);
}
// Step 2: If the route is a match generate the appropriate URI
public string BindValues(DispatcherValueCollection acceptedValues)
{
var context = _pool.Get();
var result = BindValues(context, acceptedValues);
_pool.Return(context);
return result;
}
private string BindValues(UriBuildingContext context, DispatcherValueCollection acceptedValues)
{
for (var i = 0; i < _pattern.PathSegments.Count; i++)
{
Debug.Assert(context.BufferState == SegmentState.Beginning);
Debug.Assert(context.UriState == SegmentState.Beginning);
var segment = _pattern.PathSegments[i];
for (var j = 0; j < segment.Parts.Count; j++)
{
var part = segment.Parts[j];
if (part.IsLiteral)
{
if (!context.Accept(_urlEncoder, ((RoutePatternLiteral)part).Content))
{
return null;
}
}
else if (part.IsSeparator)
{
if (!context.Accept(_urlEncoder, ((RoutePatternSeparator)part).Content))
{
return null;
}
}
else if (part.IsParameter && part is RoutePatternParameter parameter)
{
// If it's a parameter, get its value
object value;
var hasValue = acceptedValues.TryGetValue(parameter.Name, out value);
if (hasValue)
{
acceptedValues.Remove(parameter.Name);
}
var isSameAsDefault = false;
object defaultValue;
if (_defaults != null && _defaults.TryGetValue(parameter.Name, out defaultValue))
{
if (RoutePartsEqual(value, defaultValue))
{
isSameAsDefault = true;
}
}
var converted = Convert.ToString(value, CultureInfo.InvariantCulture);
if (isSameAsDefault)
{
// If the accepted value is the same as the default value buffer it since
// we won't necessarily add it to the URI we generate.
if (!context.Buffer(_urlEncoder, converted))
{
return null;
}
}
else
{
// If the value is not accepted, it is null or empty value in the
// middle of the segment. We accept this if the parameter is an
// optional parameter and it is preceded by an optional seperator.
// I this case, we need to remove the optional seperator that we
// have added to the URI
// Example: template = {id}.{format?}. parameters: id=5
// In this case after we have generated "5.", we wont find any value
// for format, so we remove '.' and generate 5.
if (!context.Accept(_urlEncoder, converted))
{
if (j != 0 && parameter.IsOptional && segment.Parts[j - 1].IsSeparator)
{
context.Remove(((RoutePatternSeparator)segment.Parts[j - 1]).Content);
}
else
{
return null;
}
}
}
}
}
context.EndSegment();
}
// Generate the query string from the remaining values
var wroteFirst = false;
foreach (var kvp in acceptedValues)
{
if (_defaults != null && _defaults.ContainsKey(kvp.Key))
{
// This value is a 'filter' we don't need to put it in the query string.
continue;
}
var values = kvp.Value as IEnumerable;
if (values != null && !(values is string))
{
foreach (var value in values)
{
wroteFirst |= AddParameterToContext(context, kvp.Key, value, wroteFirst);
}
}
else
{
wroteFirst |= AddParameterToContext(context, kvp.Key, kvp.Value, wroteFirst);
}
}
return context.ToString();
}
private bool AddParameterToContext(UriBuildingContext context, string key, object value, bool wroteFirst)
{
var converted = Convert.ToString(value, CultureInfo.InvariantCulture);
if (!string.IsNullOrEmpty(converted))
{
context.Writer.Write(wroteFirst ? '&' : '?');
_urlEncoder.Encode(context.Writer, key);
context.Writer.Write('=');
_urlEncoder.Encode(context.Writer, converted);
return true;
}
return false;
}
/// <summary>
/// Compares two objects for equality as parts of a case-insensitive path.
/// </summary>
/// <param name="a">An object to compare.</param>
/// <param name="b">An object to compare.</param>
/// <returns>True if the object are equal, otherwise false.</returns>
public static bool RoutePartsEqual(object a, object b)
{
var sa = a as string;
var sb = b as string;
if (sa != null && sb != null)
{
// For strings do a case-insensitive comparison
return string.Equals(sa, sb, StringComparison.OrdinalIgnoreCase);
}
else
{
if (a != null && b != null)
{
// Explicitly call .Equals() in case it is overridden in the type
return a.Equals(b);
}
else
{
// At least one of them is null. Return true if they both are
return a == b;
}
}
}
private static bool IsRoutePartNonEmpty(object routePart)
{
var routePartString = routePart as string;
if (routePartString == null)
{
return routePart != null;
}
else
{
return routePartString.Length > 0;
}
}
[DebuggerDisplay("{DebuggerToString(),nq}")]
private struct TemplateBindingContext
{
private readonly DispatcherValueCollection _defaults;
private readonly DispatcherValueCollection _acceptedValues;
public TemplateBindingContext(DispatcherValueCollection defaults)
{
_defaults = defaults;
_acceptedValues = new DispatcherValueCollection();
}
public DispatcherValueCollection AcceptedValues
{
get { return _acceptedValues; }
}
public void Accept(string key, object value)
{
if (!_acceptedValues.ContainsKey(key))
{
_acceptedValues.Add(key, value);
}
}
public void AcceptDefault(string key)
{
Debug.Assert(!_acceptedValues.ContainsKey(key));
object value;
if (_defaults != null && _defaults.TryGetValue(key, out value))
{
_acceptedValues.Add(key, value);
}
}
public bool NeedsValue(string key)
{
return !_acceptedValues.ContainsKey(key);
}
private string DebuggerToString()
{
return string.Format("{{Accepted: '{0}'}}", string.Join(", ", _acceptedValues.Keys));
}
}
}
}

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Routing.Internal
namespace Microsoft.AspNetCore.Dispatcher
{
// 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

View File

@ -1,29 +1,15 @@
// 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.AspNetCore.Routing.Internal
namespace Microsoft.AspNetCore.Dispatcher
{
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);
return new UriBuildingContext();
}
public bool Return(UriBuildingContext obj)

View File

@ -7,7 +7,7 @@ using System.IO;
using System.Text;
using System.Text.Encodings.Web;
namespace Microsoft.AspNetCore.Routing.Internal
namespace Microsoft.AspNetCore.Dispatcher
{
[DebuggerDisplay("{DebuggerToString(),nq}")]
public class UriBuildingContext
@ -19,14 +19,12 @@ namespace Microsoft.AspNetCore.Routing.Internal
// 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)
public UriBuildingContext()
{
_urlEncoder = urlEncoder;
_uri = new StringBuilder();
_buffer = new List<BufferValue>();
Writer = new StringWriter(_uri);
@ -42,7 +40,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
public TextWriter Writer { get; }
public bool Accept(string value)
public bool Accept(UrlEncoder encoder, string value)
{
if (string.IsNullOrEmpty(value))
{
@ -67,7 +65,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
{
if (_buffer[i].RequiresEncoding)
{
_urlEncoder.Encode(Writer, _buffer[i].Value);
encoder.Encode(Writer, _buffer[i].Value);
}
else
{
@ -93,11 +91,11 @@ namespace Microsoft.AspNetCore.Routing.Internal
if (_uri.Length == 0 && value.Length > 0 && value[0] == '/')
{
_uri.Append("/");
_urlEncoder.Encode(Writer, value, 1, value.Length - 1);
encoder.Encode(Writer, value, 1, value.Length - 1);
}
else
{
_urlEncoder.Encode(Writer, value);
encoder.Encode(Writer, value);
}
return true;
@ -110,7 +108,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
_lastValueOffset = -1;
}
public bool Buffer(string value)
public bool Buffer(UrlEncoder encoder, string value)
{
if (string.IsNullOrEmpty(value))
{
@ -135,7 +133,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
{
// 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);
var result = Accept(encoder, value);
// We've already checked the conditions that could result in a rejected part, so this should
// always be true.

View File

@ -28,14 +28,11 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentNullException(nameof(services));
}
// Routing shares lots of infrastructure with the dispatcher.
services.AddDispatcher();
services.TryAddTransient<IInlineConstraintResolver, DefaultInlineConstraintResolver>();
services.TryAddSingleton(UrlEncoder.Default);
services.TryAddSingleton<ObjectPool<UriBuildingContext>>(s =>
{
var provider = s.GetRequiredService<ObjectPoolProvider>();
var encoder = s.GetRequiredService<UrlEncoder>();
return provider.Create<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy(encoder));
});
// The TreeRouteBuilder is a builder for creating routes, it should stay transient because it's
// stateful.

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Dispatcher;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Logging;

View File

@ -2,23 +2,15 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Diagnostics;
using System.Globalization;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Dispatcher;
using Microsoft.Extensions.ObjectPool;
namespace Microsoft.AspNetCore.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 RoutePatternBinder _binder;
public TemplateBinder(
UrlEncoder urlEncoder,
@ -41,320 +33,29 @@ namespace Microsoft.AspNetCore.Routing.Template
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
// is provided for that 'filter' it must match the value in defaults.
_filters = new RouteValueDictionary(_defaults);
foreach (var parameter in _template.Parameters)
{
_filters.Remove(parameter.Name);
}
_binder = new RoutePatternBinder(urlEncoder, pool, template.ToRoutePattern(), defaults);
}
// Step 1: Get the list of values we're going to try to use to match and generate this URI
public TemplateValuesResult GetValues(RouteValueDictionary ambientValues, RouteValueDictionary 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
// specified that b="9", then we need to invalidate everything after it. The new
// values should then be a="1", b="9", c=<no value>.
//
// We also handle the case where a parameter is optional but has no value - we shouldn't
// accept additional parameters that appear *after* that parameter.
for (var i = 0; i < _template.Parameters.Count; i++)
(var acceptedValues, var combinedValues) = _binder.GetValues(ambientValues, values);
if (acceptedValues == null || combinedValues == null)
{
var parameter = _template.Parameters[i];
// If it's a parameter subsegment, examine the current value to see if it matches the new value
var parameterName = parameter.Name;
object newParameterValue;
var hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue);
object currentParameterValue = null;
var hasCurrentParameterValue = ambientValues != null &&
ambientValues.TryGetValue(parameterName, out currentParameterValue);
if (hasNewParameterValue && hasCurrentParameterValue)
{
if (!RoutePartsEqual(currentParameterValue, newParameterValue))
{
// Stop copying current values when we find one that doesn't match
break;
}
}
if (!hasNewParameterValue &&
!hasCurrentParameterValue &&
_defaults?.ContainsKey(parameter.Name) != true)
{
// This is an unsatisfied parameter value and there are no defaults. We might still
// be able to generate a URL but we should stop 'accepting' ambient values.
//
// This might be a case like:
// template: a/{b?}/{c?}
// ambient: { c = 17 }
// values: { }
//
// We can still generate a URL from this ("/a") but we shouldn't accept 'c' because
// we can't use it.
//
// In the example above we should fall into this block for 'b'.
break;
}
// If the parameter is a match, add it to the list of values we will use for URI generation
if (hasNewParameterValue)
{
if (IsRoutePartNonEmpty(newParameterValue))
{
context.Accept(parameterName, newParameterValue);
}
}
else
{
if (hasCurrentParameterValue)
{
context.Accept(parameterName, currentParameterValue);
}
}
}
// Add all remaining new values to the list of values we will use for URI generation
foreach (var kvp in values)
{
if (IsRoutePartNonEmpty(kvp.Value))
{
context.Accept(kvp.Key, kvp.Value);
}
}
// Accept all remaining default values if they match a required parameter
for (var i = 0; i < _template.Parameters.Count; i++)
{
var parameter = _template.Parameters[i];
if (parameter.IsOptional || parameter.IsCatchAll)
{
continue;
}
if (context.NeedsValue(parameter.Name))
{
// Add the default value only if there isn't already a new value for it and
// only if it actually has a default value, which we determine based on whether
// the parameter value is required.
context.AcceptDefault(parameter.Name);
}
}
// Validate that all required parameters have a value.
for (var i = 0; i < _template.Parameters.Count; i++)
{
var parameter = _template.Parameters[i];
if (parameter.IsOptional || parameter.IsCatchAll)
{
continue;
}
if (!context.AcceptedValues.ContainsKey(parameter.Name))
{
// We don't have a value for this parameter, so we can't generate a url.
return null;
}
}
// Any default values that don't appear as parameters are treated like filters. Any new values
// provided must match these defaults.
foreach (var filter in _filters)
{
var parameter = GetParameter(filter.Key);
if (parameter != null)
{
continue;
}
object value;
if (values.TryGetValue(filter.Key, out value))
{
if (!RoutePartsEqual(value, filter.Value))
{
// If there is a non-parameterized value in the route and there is a
// new value for it and it doesn't match, this route won't match.
return null;
}
}
}
// Add any ambient values that don't match parameters - they need to be visible to constraints
// but they will ignored by link generation.
var combinedValues = new RouteValueDictionary(context.AcceptedValues);
if (ambientValues != null)
{
foreach (var kvp in ambientValues)
{
if (IsRoutePartNonEmpty(kvp.Value))
{
var parameter = GetParameter(kvp.Key);
if (parameter == null && !context.AcceptedValues.ContainsKey(kvp.Key))
{
combinedValues.Add(kvp.Key, kvp.Value);
}
}
}
return null;
}
return new TemplateValuesResult()
{
AcceptedValues = context.AcceptedValues,
CombinedValues = combinedValues,
AcceptedValues = acceptedValues.AsRouteValueDictionary(),
CombinedValues = combinedValues.AsRouteValueDictionary(),
};
}
// Step 2: If the route is a match generate the appropriate URI
public string BindValues(RouteValueDictionary acceptedValues)
{
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);
Debug.Assert(context.UriState == SegmentState.Beginning);
var segment = _template.Segments[i];
for (var j = 0; j < segment.Parts.Count; j++)
{
var part = segment.Parts[j];
if (part.IsLiteral)
{
if (!context.Accept(part.Text))
{
return null;
}
}
else if (part.IsParameter)
{
// If it's a parameter, get its value
object value;
var hasValue = acceptedValues.TryGetValue(part.Name, out value);
if (hasValue)
{
acceptedValues.Remove(part.Name);
}
var isSameAsDefault = false;
object defaultValue;
if (_defaults != null && _defaults.TryGetValue(part.Name, out defaultValue))
{
if (RoutePartsEqual(value, defaultValue))
{
isSameAsDefault = true;
}
}
var converted = Convert.ToString(value, CultureInfo.InvariantCulture);
if (isSameAsDefault)
{
// If the accepted value is the same as the default value buffer it since
// we won't necessarily add it to the URI we generate.
if (!context.Buffer(converted))
{
return null;
}
}
else
{
// If the value is not accepted, it is null or empty value in the
// middle of the segment. We accept this if the parameter is an
// optional parameter and it is preceded by an optional seperator.
// I this case, we need to remove the optional seperator that we
// have added to the URI
// Example: template = {id}.{format?}. parameters: id=5
// In this case after we have generated "5.", we wont find any value
// for format, so we remove '.' and generate 5.
if (!context.Accept(converted))
{
if (j != 0 && part.IsOptional && segment.Parts[j - 1].IsOptionalSeperator)
{
context.Remove(segment.Parts[j - 1].Text);
}
else
{
return null;
}
}
}
}
}
context.EndSegment();
}
// Generate the query string from the remaining values
var wroteFirst = false;
foreach (var kvp in acceptedValues)
{
if (_defaults != null && _defaults.ContainsKey(kvp.Key))
{
// This value is a 'filter' we don't need to put it in the query string.
continue;
}
var values = kvp.Value as IEnumerable;
if (values != null && !(values is string))
{
foreach (var value in values)
{
wroteFirst |= AddParameterToContext(context, kvp.Key, value, wroteFirst);
}
}
else
{
wroteFirst |= AddParameterToContext(context, kvp.Key, kvp.Value, wroteFirst);
}
}
return context.ToString();
}
private bool AddParameterToContext(UriBuildingContext context, string key, object value, bool wroteFirst)
{
var converted = Convert.ToString(value, CultureInfo.InvariantCulture);
if (!string.IsNullOrEmpty(converted))
{
context.Writer.Write(wroteFirst ? '&' : '?');
_urlEncoder.Encode(context.Writer, key);
context.Writer.Write('=');
_urlEncoder.Encode(context.Writer, converted);
return true;
}
return false;
}
private TemplatePart GetParameter(string name)
{
for (var i = 0; i < _template.Parameters.Count; i++)
{
var parameter = _template.Parameters[i];
if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase))
{
return parameter;
}
}
return null;
return _binder.BindValues(acceptedValues);
}
/// <summary>
@ -387,66 +88,5 @@ namespace Microsoft.AspNetCore.Routing.Template
}
}
}
private static bool IsRoutePartNonEmpty(object routePart)
{
var routePartString = routePart as string;
if (routePartString == null)
{
return routePart != null;
}
else
{
return routePartString.Length > 0;
}
}
[DebuggerDisplay("{DebuggerToString(),nq}")]
private struct TemplateBindingContext
{
private readonly RouteValueDictionary _defaults;
private readonly RouteValueDictionary _acceptedValues;
public TemplateBindingContext(RouteValueDictionary defaults)
{
_defaults = defaults;
_acceptedValues = new RouteValueDictionary();
}
public RouteValueDictionary AcceptedValues
{
get { return _acceptedValues; }
}
public void Accept(string key, object value)
{
if (!_acceptedValues.ContainsKey(key))
{
_acceptedValues.Add(key, value);
}
}
public void AcceptDefault(string key)
{
Debug.Assert(!_acceptedValues.ContainsKey(key));
object value;
if (_defaults != null && _defaults.TryGetValue(key, out value))
{
_acceptedValues.Add(key, value);
}
}
public bool NeedsValue(string key)
{
return !_acceptedValues.ContainsKey(key);
}
private string DebuggerToString()
{
return string.Format("{{Accepted: '{0}'}}", string.Join(", ", _acceptedValues.Keys));
}
}
}
}

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Dispatcher;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;

View File

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Dispatcher;
using Microsoft.AspNetCore.Dispatcher.Internal;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Logging;

View File

@ -0,0 +1,17 @@
[
{
"TypeId": "public class Microsoft.AspNetCore.Routing.Tree.TreeRouter : Microsoft.AspNetCore.Routing.IRouter",
"MemberId": "public .ctor(Microsoft.AspNetCore.Routing.Tree.UrlMatchingTree[] trees, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Routing.Tree.OutboundRouteEntry> linkGenerationEntries, System.Text.Encodings.Web.UrlEncoder urlEncoder, Microsoft.Extensions.ObjectPool.ObjectPool<Microsoft.AspNetCore.Routing.Internal.UriBuildingContext> objectPool, Microsoft.Extensions.Logging.ILogger routeLogger, Microsoft.Extensions.Logging.ILogger constraintLogger, System.Int32 version)",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.AspNetCore.Routing.Tree.TreeRouteBuilder",
"MemberId": "public .ctor(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, System.Text.Encodings.Web.UrlEncoder urlEncoder, Microsoft.Extensions.ObjectPool.ObjectPool<Microsoft.AspNetCore.Routing.Internal.UriBuildingContext> objectPool, Microsoft.AspNetCore.Routing.IInlineConstraintResolver constraintResolver)",
"Kind": "Removal"
},
{
"TypeId": "public class Microsoft.AspNetCore.Routing.Template.TemplateBinder",
"MemberId": "public .ctor(System.Text.Encodings.Web.UrlEncoder urlEncoder, Microsoft.Extensions.ObjectPool.ObjectPool<Microsoft.AspNetCore.Routing.Internal.UriBuildingContext> pool, Microsoft.AspNetCore.Routing.Template.RouteTemplate template, Microsoft.AspNetCore.Routing.RouteValueDictionary defaults)",
"Kind": "Removal"
}
]

View File

@ -13,6 +13,8 @@
<PackageReference Include="Microsoft.AspNetCore.Http" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Testing" />
<PackageReference Include="Microsoft.Extensions.WebEncoders" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

View File

@ -591,6 +591,7 @@ namespace Microsoft.AspNetCore.Routing
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.AddOptions();
services.AddRouting();
services.AddDispatcher();
if (options != null)
{
services.Configure(options);
@ -613,6 +614,7 @@ namespace Microsoft.AspNetCore.Routing
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.AddOptions();
services.AddRouting();
services.AddDispatcher();
if (options != null)
{
services.Configure<RouteOptions>(options);

View File

@ -1487,6 +1487,7 @@ namespace Microsoft.AspNetCore.Routing
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.AddRouting();
services.AddDispatcher();
var context = new DefaultHttpContext
{

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Dispatcher;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.ObjectPool;
@ -119,7 +120,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
var encoder = new UrlTestEncoder();
var binder = new TemplateBinder(
encoder,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy(encoder)),
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
TemplateParser.Parse(template),
defaults);
@ -269,7 +270,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
var encoder = new UrlTestEncoder();
var binder = new TemplateBinder(
encoder,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy(encoder)),
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
TemplateParser.Parse(template),
defaults);
@ -699,7 +700,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
var encoder = new UrlTestEncoder();
var binder = new TemplateBinder(
new UrlTestEncoder(),
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy(encoder)),
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
TemplateParser.Parse(template),
defaults: null);
var ambientValues = new RouteValueDictionary();
@ -1131,7 +1132,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
var binder = new TemplateBinder(
encoder,
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy(encoder)),
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
TemplateParser.Parse(template),
defaults);

View File

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Dispatcher;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
@ -244,7 +244,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
private static TreeRouteBuilder CreateBuilder()
{
var objectPoolProvider = new DefaultObjectPoolProvider();
var objectPolicy = new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default);
var objectPolicy = new UriBuilderContextPooledObjectPolicy();
var objectPool = objectPoolProvider.Create(objectPolicy);
var constraintResolver = GetInlineConstraintResolver();

View File

@ -6,8 +6,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Dispatcher;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@ -24,8 +24,7 @@ namespace Microsoft.AspNetCore.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));
private static ObjectPool<UriBuildingContext> Pool = new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy());
[Theory]
[InlineData("template/5", "template/{parameter:int}")]
@ -1990,7 +1989,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
private static TreeRouteBuilder CreateBuilder()
{
var objectPoolProvider = new DefaultObjectPoolProvider();
var objectPolicy = new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default);
var objectPolicy = new UriBuilderContextPooledObjectPolicy();
var objectPool = objectPoolProvider.Create<UriBuildingContext>(objectPolicy);
var constraintResolver = CreateConstraintResolver();