Port TemplateBinder to dispatcher
This commit is contained in:
parent
eebc7db2ca
commit
2d661396df
|
|
@ -31,8 +31,8 @@ namespace Microsoft.AspNetCore.Dispatcher.Performance
|
||||||
var treeBuilder = new TreeRouteBuilder(
|
var treeBuilder = new TreeRouteBuilder(
|
||||||
NullLoggerFactory.Instance,
|
NullLoggerFactory.Instance,
|
||||||
UrlEncoder.Default,
|
UrlEncoder.Default,
|
||||||
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default)),
|
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
|
||||||
new DefaultInlineConstraintResolver(new OptionsManager<RouteOptions>(new OptionsFactory<RouteOptions>(Enumerable.Empty<IConfigureOptions<RouteOptions>>(), Enumerable.Empty<IPostConfigureOptions<RouteOptions>>()))));
|
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"), "default", 0);
|
||||||
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets/{id}"), "default", 0);
|
treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets/{id}"), "default", 0);
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ using System.Linq;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Dispatcher;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
|
||||||
using Microsoft.AspNetCore.Routing.Template;
|
using Microsoft.AspNetCore.Routing.Template;
|
||||||
using Microsoft.AspNetCore.Routing.Tree;
|
using Microsoft.AspNetCore.Routing.Tree;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
@ -31,8 +31,8 @@ namespace Microsoft.AspNetCore.Routing.Performance
|
||||||
var treeBuilder = new TreeRouteBuilder(
|
var treeBuilder = new TreeRouteBuilder(
|
||||||
NullLoggerFactory.Instance,
|
NullLoggerFactory.Instance,
|
||||||
UrlEncoder.Default,
|
UrlEncoder.Default,
|
||||||
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default)),
|
new DefaultObjectPool<UriBuildingContext>(new UriBuilderContextPooledObjectPolicy()),
|
||||||
new DefaultInlineConstraintResolver(new OptionsManager<RouteOptions>(new OptionsFactory<RouteOptions>(Enumerable.Empty<IConfigureOptions<RouteOptions>>(), Enumerable.Empty<IPostConfigureOptions<RouteOptions>>()))));
|
new DefaultInlineConstraintResolver(Options.Create(new RouteOptions())));
|
||||||
|
|
||||||
treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets"), "default", 0);
|
treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets"), "default", 0);
|
||||||
treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets/{id}"), "default", 0);
|
treeBuilder.MapInbound(handler, TemplateParser.Parse("api/Widgets/{id}"), "default", 0);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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
|
public struct BufferValue
|
||||||
{
|
{
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using Microsoft.AspNetCore.Dispatcher;
|
using Microsoft.AspNetCore.Dispatcher;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.ObjectPool;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.Extensions.DependencyInjection
|
namespace Microsoft.Extensions.DependencyInjection
|
||||||
|
|
@ -18,7 +19,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
throw new ArgumentNullException(nameof(services));
|
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>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupFilter, DispatcherEndpointStartupFilter>());
|
||||||
|
|
||||||
// Adds a default dispatcher which will collect all data sources and endpoint selectors from DI.
|
// 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<AddressTable, DefaultAddressTable>();
|
||||||
services.AddSingleton<TemplateAddressSelector>();
|
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>());
|
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHandlerFactory, TemplateEndpointHandlerFactory>());
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" />
|
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.Extensions.HashCodeCombiner.Sources" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.Extensions.HashCodeCombiner.Sources" PrivateAssets="All" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.ObjectPool" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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.
|
// 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
|
// If we add any subsegment of this segment to the generated URI, we have to add
|
||||||
|
|
@ -1,29 +1,15 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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;
|
using Microsoft.Extensions.ObjectPool;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Internal
|
namespace Microsoft.AspNetCore.Dispatcher
|
||||||
{
|
{
|
||||||
public class UriBuilderContextPooledObjectPolicy : IPooledObjectPolicy<UriBuildingContext>
|
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()
|
public UriBuildingContext Create()
|
||||||
{
|
{
|
||||||
return new UriBuildingContext(_encoder);
|
return new UriBuildingContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Return(UriBuildingContext obj)
|
public bool Return(UriBuildingContext obj)
|
||||||
|
|
@ -7,7 +7,7 @@ using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Internal
|
namespace Microsoft.AspNetCore.Dispatcher
|
||||||
{
|
{
|
||||||
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
[DebuggerDisplay("{DebuggerToString(),nq}")]
|
||||||
public class UriBuildingContext
|
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
|
// 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.
|
// followed by other optional segments than we will just throw it away.
|
||||||
private readonly List<BufferValue> _buffer;
|
private readonly List<BufferValue> _buffer;
|
||||||
private readonly UrlEncoder _urlEncoder;
|
|
||||||
|
|
||||||
private bool _hasEmptySegment;
|
private bool _hasEmptySegment;
|
||||||
private int _lastValueOffset;
|
private int _lastValueOffset;
|
||||||
|
|
||||||
public UriBuildingContext(UrlEncoder urlEncoder)
|
public UriBuildingContext()
|
||||||
{
|
{
|
||||||
_urlEncoder = urlEncoder;
|
|
||||||
_uri = new StringBuilder();
|
_uri = new StringBuilder();
|
||||||
_buffer = new List<BufferValue>();
|
_buffer = new List<BufferValue>();
|
||||||
Writer = new StringWriter(_uri);
|
Writer = new StringWriter(_uri);
|
||||||
|
|
@ -42,7 +40,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
||||||
|
|
||||||
public TextWriter Writer { get; }
|
public TextWriter Writer { get; }
|
||||||
|
|
||||||
public bool Accept(string value)
|
public bool Accept(UrlEncoder encoder, string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
{
|
{
|
||||||
|
|
@ -67,7 +65,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
||||||
{
|
{
|
||||||
if (_buffer[i].RequiresEncoding)
|
if (_buffer[i].RequiresEncoding)
|
||||||
{
|
{
|
||||||
_urlEncoder.Encode(Writer, _buffer[i].Value);
|
encoder.Encode(Writer, _buffer[i].Value);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -93,11 +91,11 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
||||||
if (_uri.Length == 0 && value.Length > 0 && value[0] == '/')
|
if (_uri.Length == 0 && value.Length > 0 && value[0] == '/')
|
||||||
{
|
{
|
||||||
_uri.Append("/");
|
_uri.Append("/");
|
||||||
_urlEncoder.Encode(Writer, value, 1, value.Length - 1);
|
encoder.Encode(Writer, value, 1, value.Length - 1);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_urlEncoder.Encode(Writer, value);
|
encoder.Encode(Writer, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -110,7 +108,7 @@ namespace Microsoft.AspNetCore.Routing.Internal
|
||||||
_lastValueOffset = -1;
|
_lastValueOffset = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Buffer(string value)
|
public bool Buffer(UrlEncoder encoder, string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(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
|
// 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.
|
// 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
|
// We've already checked the conditions that could result in a rejected part, so this should
|
||||||
// always be true.
|
// always be true.
|
||||||
|
|
@ -28,14 +28,11 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
throw new ArgumentNullException(nameof(services));
|
throw new ArgumentNullException(nameof(services));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Routing shares lots of infrastructure with the dispatcher.
|
||||||
|
services.AddDispatcher();
|
||||||
|
|
||||||
services.TryAddTransient<IInlineConstraintResolver, DefaultInlineConstraintResolver>();
|
services.TryAddTransient<IInlineConstraintResolver, DefaultInlineConstraintResolver>();
|
||||||
services.TryAddSingleton(UrlEncoder.Default);
|
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
|
// The TreeRouteBuilder is a builder for creating routes, it should stay transient because it's
|
||||||
// stateful.
|
// stateful.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Dispatcher;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
using Microsoft.AspNetCore.Routing.Logging;
|
using Microsoft.AspNetCore.Routing.Logging;
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,15 @@
|
||||||
// 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;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Dispatcher;
|
||||||
using Microsoft.Extensions.ObjectPool;
|
using Microsoft.Extensions.ObjectPool;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Routing.Template
|
namespace Microsoft.AspNetCore.Routing.Template
|
||||||
{
|
{
|
||||||
public class TemplateBinder
|
public class TemplateBinder
|
||||||
{
|
{
|
||||||
private readonly UrlEncoder _urlEncoder;
|
private readonly RoutePatternBinder _binder;
|
||||||
private readonly ObjectPool<UriBuildingContext> _pool;
|
|
||||||
|
|
||||||
private readonly RouteValueDictionary _defaults;
|
|
||||||
private readonly RouteValueDictionary _filters;
|
|
||||||
private readonly RouteTemplate _template;
|
|
||||||
|
|
||||||
public TemplateBinder(
|
public TemplateBinder(
|
||||||
UrlEncoder urlEncoder,
|
UrlEncoder urlEncoder,
|
||||||
|
|
@ -41,320 +33,29 @@ namespace Microsoft.AspNetCore.Routing.Template
|
||||||
throw new ArgumentNullException(nameof(template));
|
throw new ArgumentNullException(nameof(template));
|
||||||
}
|
}
|
||||||
|
|
||||||
_urlEncoder = urlEncoder;
|
_binder = new RoutePatternBinder(urlEncoder, pool, template.ToRoutePattern(), defaults);
|
||||||
_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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Get the list of values we're going to try to use to match and generate this URI
|
// 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)
|
public TemplateValuesResult GetValues(RouteValueDictionary ambientValues, RouteValueDictionary values)
|
||||||
{
|
{
|
||||||
var context = new TemplateBindingContext(_defaults);
|
(var acceptedValues, var combinedValues) = _binder.GetValues(ambientValues, values);
|
||||||
|
if (acceptedValues == null || combinedValues == null)
|
||||||
// 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 parameter = _template.Parameters[i];
|
return null;
|
||||||
|
|
||||||
// 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 new TemplateValuesResult()
|
return new TemplateValuesResult()
|
||||||
{
|
{
|
||||||
AcceptedValues = context.AcceptedValues,
|
AcceptedValues = acceptedValues.AsRouteValueDictionary(),
|
||||||
CombinedValues = combinedValues,
|
CombinedValues = combinedValues.AsRouteValueDictionary(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 = _pool.Get();
|
return _binder.BindValues(acceptedValues);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Dispatcher;
|
||||||
using Microsoft.AspNetCore.Routing.Template;
|
using Microsoft.AspNetCore.Routing.Template;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.ObjectPool;
|
using Microsoft.Extensions.ObjectPool;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Dispatcher;
|
||||||
using Microsoft.AspNetCore.Dispatcher.Internal;
|
using Microsoft.AspNetCore.Dispatcher.Internal;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
using Microsoft.AspNetCore.Routing.Logging;
|
using Microsoft.AspNetCore.Routing.Logging;
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http" />
|
<PackageReference Include="Microsoft.AspNetCore.Http" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" />
|
<PackageReference Include="Microsoft.Extensions.Logging" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Testing" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.WebEncoders" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -591,6 +591,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
|
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
|
||||||
services.AddOptions();
|
services.AddOptions();
|
||||||
services.AddRouting();
|
services.AddRouting();
|
||||||
|
services.AddDispatcher();
|
||||||
if (options != null)
|
if (options != null)
|
||||||
{
|
{
|
||||||
services.Configure(options);
|
services.Configure(options);
|
||||||
|
|
@ -613,6 +614,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
|
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
|
||||||
services.AddOptions();
|
services.AddOptions();
|
||||||
services.AddRouting();
|
services.AddRouting();
|
||||||
|
services.AddDispatcher();
|
||||||
if (options != null)
|
if (options != null)
|
||||||
{
|
{
|
||||||
services.Configure<RouteOptions>(options);
|
services.Configure<RouteOptions>(options);
|
||||||
|
|
|
||||||
|
|
@ -1487,6 +1487,7 @@ namespace Microsoft.AspNetCore.Routing
|
||||||
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
|
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
|
||||||
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
|
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
|
||||||
services.AddRouting();
|
services.AddRouting();
|
||||||
|
services.AddDispatcher();
|
||||||
|
|
||||||
var context = new DefaultHttpContext
|
var context = new DefaultHttpContext
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ 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.AspNetCore.Dispatcher;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Routing.Internal;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.ObjectPool;
|
using Microsoft.Extensions.ObjectPool;
|
||||||
|
|
@ -119,7 +120,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
|
||||||
var encoder = new UrlTestEncoder();
|
var encoder = new UrlTestEncoder();
|
||||||
var binder = new TemplateBinder(
|
var binder = new TemplateBinder(
|
||||||
encoder,
|
encoder,
|
||||||
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy(encoder)),
|
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
|
||||||
TemplateParser.Parse(template),
|
TemplateParser.Parse(template),
|
||||||
defaults);
|
defaults);
|
||||||
|
|
||||||
|
|
@ -269,7 +270,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
|
||||||
var encoder = new UrlTestEncoder();
|
var encoder = new UrlTestEncoder();
|
||||||
var binder = new TemplateBinder(
|
var binder = new TemplateBinder(
|
||||||
encoder,
|
encoder,
|
||||||
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy(encoder)),
|
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
|
||||||
TemplateParser.Parse(template),
|
TemplateParser.Parse(template),
|
||||||
defaults);
|
defaults);
|
||||||
|
|
||||||
|
|
@ -699,7 +700,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
|
||||||
var encoder = new UrlTestEncoder();
|
var encoder = new UrlTestEncoder();
|
||||||
var binder = new TemplateBinder(
|
var binder = new TemplateBinder(
|
||||||
new UrlTestEncoder(),
|
new UrlTestEncoder(),
|
||||||
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy(encoder)),
|
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
|
||||||
TemplateParser.Parse(template),
|
TemplateParser.Parse(template),
|
||||||
defaults: null);
|
defaults: null);
|
||||||
var ambientValues = new RouteValueDictionary();
|
var ambientValues = new RouteValueDictionary();
|
||||||
|
|
@ -1131,7 +1132,7 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests
|
||||||
|
|
||||||
var binder = new TemplateBinder(
|
var binder = new TemplateBinder(
|
||||||
encoder,
|
encoder,
|
||||||
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy(encoder)),
|
new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()),
|
||||||
TemplateParser.Parse(template),
|
TemplateParser.Parse(template),
|
||||||
defaults);
|
defaults);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// 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.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
using Microsoft.AspNetCore.Dispatcher;
|
||||||
using Microsoft.AspNetCore.Routing.Template;
|
using Microsoft.AspNetCore.Routing.Template;
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
@ -244,7 +244,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
||||||
private static TreeRouteBuilder CreateBuilder()
|
private static TreeRouteBuilder CreateBuilder()
|
||||||
{
|
{
|
||||||
var objectPoolProvider = new DefaultObjectPoolProvider();
|
var objectPoolProvider = new DefaultObjectPoolProvider();
|
||||||
var objectPolicy = new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default);
|
var objectPolicy = new UriBuilderContextPooledObjectPolicy();
|
||||||
var objectPool = objectPoolProvider.Create(objectPolicy);
|
var objectPool = objectPoolProvider.Create(objectPolicy);
|
||||||
|
|
||||||
var constraintResolver = GetInlineConstraintResolver();
|
var constraintResolver = GetInlineConstraintResolver();
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Dispatcher;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing.Internal;
|
|
||||||
using Microsoft.AspNetCore.Routing.Template;
|
using Microsoft.AspNetCore.Routing.Template;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
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 readonly RequestDelegate NullHandler = (c) => Task.FromResult(0);
|
||||||
|
|
||||||
private static UrlEncoder Encoder = UrlTestEncoder.Default;
|
private static UrlEncoder Encoder = UrlTestEncoder.Default;
|
||||||
private static ObjectPool<UriBuildingContext> Pool = new DefaultObjectPoolProvider().Create(
|
private static ObjectPool<UriBuildingContext> Pool = new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy());
|
||||||
new UriBuilderContextPooledObjectPolicy(Encoder));
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("template/5", "template/{parameter:int}")]
|
[InlineData("template/5", "template/{parameter:int}")]
|
||||||
|
|
@ -1990,7 +1989,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
|
||||||
private static TreeRouteBuilder CreateBuilder()
|
private static TreeRouteBuilder CreateBuilder()
|
||||||
{
|
{
|
||||||
var objectPoolProvider = new DefaultObjectPoolProvider();
|
var objectPoolProvider = new DefaultObjectPoolProvider();
|
||||||
var objectPolicy = new UriBuilderContextPooledObjectPolicy(UrlEncoder.Default);
|
var objectPolicy = new UriBuilderContextPooledObjectPolicy();
|
||||||
var objectPool = objectPoolProvider.Create<UriBuildingContext>(objectPolicy);
|
var objectPool = objectPoolProvider.Create<UriBuildingContext>(objectPolicy);
|
||||||
|
|
||||||
var constraintResolver = CreateConstraintResolver();
|
var constraintResolver = CreateConstraintResolver();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue