parent
f4fb178f55
commit
3fadca6a1b
|
|
@ -0,0 +1,52 @@
|
|||
// 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.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Dispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Constrains a dispatcher value by several child constraints.
|
||||
/// </summary>
|
||||
public class CompositeDispatcherValueConstraint : IDispatcherValueConstraint
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CompositeDispatcherValueConstraint" /> class.
|
||||
/// </summary>
|
||||
/// <param name="constraints">The child constraints that must match for this constraint to match.</param>
|
||||
public CompositeDispatcherValueConstraint(IEnumerable<IDispatcherValueConstraint> constraints)
|
||||
{
|
||||
if (constraints == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(constraints));
|
||||
}
|
||||
|
||||
Constraints = constraints;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the child constraints that must match for this constraint to match.
|
||||
/// </summary>
|
||||
public IEnumerable<IDispatcherValueConstraint> Constraints { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Match(DispatcherValueConstraintContext constraintContext)
|
||||
{
|
||||
if (constraintContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(constraintContext));
|
||||
}
|
||||
|
||||
foreach (var constraint in Constraints)
|
||||
{
|
||||
if (!constraint.Match(constraintContext))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
// 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.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Dispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// The default implementation of <see cref="IConstraintFactory"/>. Resolves constraints by parsing
|
||||
/// a constraint key and constraint arguments, using a map to resolve the constraint type, and calling an
|
||||
/// appropriate constructor for the constraint type.
|
||||
/// </summary>
|
||||
public class DefaultConstraintFactory : IConstraintFactory
|
||||
{
|
||||
private readonly IDictionary<string, Type> _constraintMap;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultConstraintFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dispatcherOptions">
|
||||
/// Accessor for <see cref="DispatcherOptions"/> containing the constraints of interest.
|
||||
/// </param>
|
||||
public DefaultConstraintFactory(IOptions<DispatcherOptions> dispatcherOptions)
|
||||
{
|
||||
_constraintMap = dispatcherOptions.Value.ConstraintMap;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <example>
|
||||
/// A typical constraint looks like the following
|
||||
/// "exampleConstraint(arg1, arg2, 12)".
|
||||
/// Here if the type registered for exampleConstraint has a single constructor with one argument,
|
||||
/// The entire string "arg1, arg2, 12" will be treated as a single argument.
|
||||
/// In all other cases arguments are split at comma.
|
||||
/// </example>
|
||||
public virtual IDispatcherValueConstraint ResolveConstraint(string constraint)
|
||||
{
|
||||
if (constraint == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(constraint));
|
||||
}
|
||||
|
||||
string constraintKey;
|
||||
string argumentString;
|
||||
var indexOfFirstOpenParens = constraint.IndexOf('(');
|
||||
if (indexOfFirstOpenParens >= 0 && constraint.EndsWith(")", StringComparison.Ordinal))
|
||||
{
|
||||
constraintKey = constraint.Substring(0, indexOfFirstOpenParens);
|
||||
argumentString = constraint.Substring(indexOfFirstOpenParens + 1,
|
||||
constraint.Length - indexOfFirstOpenParens - 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
constraintKey = constraint;
|
||||
argumentString = null;
|
||||
}
|
||||
|
||||
if (!_constraintMap.TryGetValue(constraintKey, out var constraintType))
|
||||
{
|
||||
// Cannot resolve the constraint key
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!typeof(IDispatcherValueConstraint).GetTypeInfo().IsAssignableFrom(constraintType.GetTypeInfo()))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
Resources.FormatDefaultConstraintResolver_TypeNotConstraint(
|
||||
constraintType, constraintKey, typeof(IDispatcherValueConstraint).Name));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return CreateConstraint(constraintType, argumentString);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"An error occurred while trying to create an instance of route constraint '{constraintType.FullName}'.",
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static IDispatcherValueConstraint CreateConstraint(Type constraintType, string argumentString)
|
||||
{
|
||||
// No arguments - call the default constructor
|
||||
if (argumentString == null)
|
||||
{
|
||||
return (IDispatcherValueConstraint)Activator.CreateInstance(constraintType);
|
||||
}
|
||||
|
||||
var constraintTypeInfo = constraintType.GetTypeInfo();
|
||||
ConstructorInfo activationConstructor = null;
|
||||
object[] parameters = null;
|
||||
var constructors = constraintTypeInfo.DeclaredConstructors.ToArray();
|
||||
|
||||
// If there is only one constructor and it has a single parameter, pass the argument string directly
|
||||
// This is necessary for the RegexDispatcherValueConstraint to ensure that patterns are not split on commas.
|
||||
if (constructors.Length == 1 && constructors[0].GetParameters().Length == 1)
|
||||
{
|
||||
activationConstructor = constructors[0];
|
||||
parameters = ConvertArguments(activationConstructor.GetParameters(), new string[] { argumentString });
|
||||
}
|
||||
else
|
||||
{
|
||||
var arguments = argumentString.Split(',').Select(argument => argument.Trim()).ToArray();
|
||||
|
||||
var matchingConstructors = constructors.Where(ci => ci.GetParameters().Length == arguments.Length)
|
||||
.ToArray();
|
||||
var constructorMatches = matchingConstructors.Length;
|
||||
|
||||
if (constructorMatches == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
Resources.FormatDefaultConstraintResolver_CouldNotFindCtor(
|
||||
constraintTypeInfo.Name, arguments.Length));
|
||||
}
|
||||
else if (constructorMatches == 1)
|
||||
{
|
||||
activationConstructor = matchingConstructors[0];
|
||||
parameters = ConvertArguments(activationConstructor.GetParameters(), arguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
Resources.FormatDefaultConstraintResolver_AmbiguousCtors(
|
||||
constraintTypeInfo.Name, arguments.Length));
|
||||
}
|
||||
}
|
||||
|
||||
return (IDispatcherValueConstraint)activationConstructor.Invoke(parameters);
|
||||
}
|
||||
|
||||
private static object[] ConvertArguments(ParameterInfo[] parameterInfos, string[] arguments)
|
||||
{
|
||||
var parameters = new object[parameterInfos.Length];
|
||||
for (var i = 0; i < parameterInfos.Length; i++)
|
||||
{
|
||||
var parameter = parameterInfos[i];
|
||||
var parameterType = parameter.ParameterType;
|
||||
parameters[i] = Convert.ChangeType(arguments[i], parameterType, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
// 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.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Dispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// A builder for producing a mapping of keys to <see cref="IDispatcherValueConstraint"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="DispatcherValueConstraintBuilder"/> allows iterative building a set of route constraints, and will
|
||||
/// merge multiple entries for the same key.
|
||||
/// </remarks>
|
||||
public class DispatcherValueConstraintBuilder
|
||||
{
|
||||
private readonly IConstraintFactory _constraintFactory;
|
||||
private readonly string _rawText;
|
||||
private readonly Dictionary<string, List<IDispatcherValueConstraint>> _constraints;
|
||||
private readonly HashSet<string> _optionalParameters;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="DispatcherValueConstraintBuilder"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="constraintFactory">The <see cref="IConstraintFactory"/>.</param>
|
||||
/// <param name="rawText">The display name (for use in error messages).</param>
|
||||
public DispatcherValueConstraintBuilder(
|
||||
IConstraintFactory constraintFactory,
|
||||
string rawText)
|
||||
{
|
||||
if (constraintFactory == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(constraintFactory));
|
||||
}
|
||||
|
||||
if (rawText == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(rawText));
|
||||
}
|
||||
|
||||
_constraintFactory = constraintFactory;
|
||||
_rawText = rawText;
|
||||
|
||||
_constraints = new Dictionary<string, List<IDispatcherValueConstraint>>(StringComparer.OrdinalIgnoreCase);
|
||||
_optionalParameters = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a mapping of constraints.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IDictionary{String, IDispatcherValueConstraint}"/> of the constraints.</returns>
|
||||
public IDictionary<string, IDispatcherValueConstraint> Build()
|
||||
{
|
||||
var constraints = new Dictionary<string, IDispatcherValueConstraint>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var kvp in _constraints)
|
||||
{
|
||||
IDispatcherValueConstraint constraint;
|
||||
if (kvp.Value.Count == 1)
|
||||
{
|
||||
constraint = kvp.Value[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
constraint = new CompositeDispatcherValueConstraint(kvp.Value.ToArray());
|
||||
}
|
||||
|
||||
if (_optionalParameters.Contains(kvp.Key))
|
||||
{
|
||||
var optionalConstraint = new OptionalDispatcherValueConstraint(constraint);
|
||||
constraints.Add(kvp.Key, optionalConstraint);
|
||||
}
|
||||
else
|
||||
{
|
||||
constraints.Add(kvp.Key, constraint);
|
||||
}
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a constraint instance for the given key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="value">
|
||||
/// The constraint instance. Must either be a string or an instance of <see cref="IDispatcherValueConstraint"/>.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// If the <paramref name="value"/> is a string, it will be converted to a <see cref="RegexDispatcherValueConstraint"/>.
|
||||
///
|
||||
/// For example, the string <code>Product[0-9]+</code> will be converted to the regular expression
|
||||
/// <code>^(Product[0-9]+)</code>. See <see cref="System.Text.RegularExpressions.Regex"/> for more details.
|
||||
/// </remarks>
|
||||
public void AddConstraint(string key, object value)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
var constraint = value as IDispatcherValueConstraint;
|
||||
if (constraint == null)
|
||||
{
|
||||
var regexPattern = value as string;
|
||||
if (regexPattern == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
Resources.FormatDispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint(
|
||||
key,
|
||||
value,
|
||||
_rawText,
|
||||
typeof(IDispatcherValueConstraint)));
|
||||
}
|
||||
|
||||
var constraintsRegEx = "^(" + regexPattern + ")$";
|
||||
constraint = new RegexDispatcherValueConstraint(constraintsRegEx);
|
||||
}
|
||||
|
||||
Add(key, constraint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a constraint for the given key, resolved by the <see cref="IConstraintFactory"/>.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="constraintText">The text to be resolved by <see cref="IConstraintFactory"/>.</param>
|
||||
/// <remarks>
|
||||
/// The <see cref="IConstraintFactory"/> can create <see cref="IDispatcherValueConstraint"/> instances
|
||||
/// based on <paramref name="constraintText"/>. See <see cref="DispatcherOptions.ConstraintMap"/> to register
|
||||
/// custom constraint types.
|
||||
/// </remarks>
|
||||
public void AddResolvedConstraint(string key, string constraintText)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
if (constraintText == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(constraintText));
|
||||
}
|
||||
|
||||
var constraint = _constraintFactory.ResolveConstraint(constraintText);
|
||||
if (constraint == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
Resources.FormatDispatcherValueConstraintBuilder_CouldNotResolveConstraint(
|
||||
key,
|
||||
constraintText,
|
||||
_rawText,
|
||||
_constraintFactory.GetType().Name));
|
||||
}
|
||||
|
||||
Add(key, constraint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the given key as optional.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
public void SetOptional(string key)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
_optionalParameters.Add(key);
|
||||
}
|
||||
|
||||
private void Add(string key, IDispatcherValueConstraint constraint)
|
||||
{
|
||||
if (!_constraints.TryGetValue(key, out var list))
|
||||
{
|
||||
list = new List<IDispatcherValueConstraint>();
|
||||
_constraints.Add(key, list);
|
||||
}
|
||||
|
||||
list.Add(constraint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Dispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines an abstraction for resolving constraints as instances of <see cref="IDispatcherValueConstraint"/>.
|
||||
/// </summary>
|
||||
public interface IConstraintFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the constraint.
|
||||
/// </summary>
|
||||
/// <param name="constraint">The constraint to resolve.</param>
|
||||
/// <returns>The <see cref="IDispatcherValueConstraint"/> the constraint was resolved to.</returns>
|
||||
IDispatcherValueConstraint ResolveConstraint(string constraint);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Dispatcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a constraint on an optional parameter. If the parameter is present, then it is constrained by InnerConstraint.
|
||||
/// </summary>
|
||||
public class OptionalDispatcherValueConstraint : IDispatcherValueConstraint
|
||||
{
|
||||
public OptionalDispatcherValueConstraint(IDispatcherValueConstraint innerConstraint)
|
||||
{
|
||||
if (innerConstraint == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(innerConstraint));
|
||||
}
|
||||
|
||||
InnerConstraint = innerConstraint;
|
||||
}
|
||||
|
||||
public IDispatcherValueConstraint InnerConstraint { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Match(DispatcherValueConstraintContext constraintContext)
|
||||
{
|
||||
if (constraintContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(constraintContext));
|
||||
}
|
||||
|
||||
if (constraintContext.Values.TryGetValue(constraintContext.Key, out var routeValue)
|
||||
&& routeValue != null)
|
||||
{
|
||||
return InnerConstraint.Match(constraintContext);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// 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.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Dispatcher
|
||||
{
|
||||
public class RegexDispatcherValueConstraint : IDispatcherValueConstraint
|
||||
{
|
||||
private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
public RegexDispatcherValueConstraint(Regex regex)
|
||||
{
|
||||
if (regex == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(regex));
|
||||
}
|
||||
|
||||
Constraint = regex;
|
||||
}
|
||||
|
||||
public RegexDispatcherValueConstraint(string regexPattern)
|
||||
{
|
||||
if (regexPattern == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(regexPattern));
|
||||
}
|
||||
|
||||
Constraint = new Regex(
|
||||
regexPattern,
|
||||
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
|
||||
RegexMatchTimeout);
|
||||
}
|
||||
|
||||
public Regex Constraint { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Match(DispatcherValueConstraintContext constraintContext)
|
||||
{
|
||||
if (constraintContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(constraintContext));
|
||||
}
|
||||
|
||||
if (constraintContext.Values.TryGetValue(constraintContext.Key, out var routeValue)
|
||||
&& routeValue != null)
|
||||
{
|
||||
var parameterValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
|
||||
|
||||
return Constraint.IsMatch(parameterValueString);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.AspNetCore.Dispatcher
|
||||
|
|
@ -8,5 +9,7 @@ namespace Microsoft.AspNetCore.Dispatcher
|
|||
public class DispatcherOptions
|
||||
{
|
||||
public MatcherCollection Matchers { get; } = new MatcherCollection();
|
||||
|
||||
public IDictionary<string, Type> ConstraintMap = new Dictionary<string, Type>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,21 @@ namespace Microsoft.AspNetCore.Dispatcher
|
|||
new EventId(3, "NoEndpointMatchedRequestMethod"),
|
||||
"No endpoint matched request method '{Method}'.");
|
||||
|
||||
// DispatcherValueConstraintMatcher
|
||||
private static readonly Action<ILogger, object, string, IDispatcherValueConstraint, Exception> _routeValueDoesNotMatchConstraint = LoggerMessage.Define<object, string, IDispatcherValueConstraint>(
|
||||
LogLevel.Debug,
|
||||
1,
|
||||
"Route value '{RouteValue}' with key '{RouteKey}' did not match the constraint '{RouteConstraint}'.");
|
||||
|
||||
public static void RouteValueDoesNotMatchConstraint(
|
||||
this ILogger logger,
|
||||
object routeValue,
|
||||
string routeKey,
|
||||
IDispatcherValueConstraint routeConstraint)
|
||||
{
|
||||
_routeValueDoesNotMatchConstraint(logger, routeValue, routeKey, routeConstraint, null);
|
||||
}
|
||||
|
||||
public static void AmbiguousEndpoints(this ILogger logger, string ambiguousEndpoints)
|
||||
{
|
||||
_ambiguousEndpoints(logger, ambiguousEndpoints, null);
|
||||
|
|
|
|||
|
|
@ -38,6 +38,76 @@ namespace Microsoft.AspNetCore.Dispatcher
|
|||
internal static string FormatArgument_NullOrEmpty()
|
||||
=> GetString("Argument_NullOrEmpty");
|
||||
|
||||
/// <summary>
|
||||
/// The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}.
|
||||
/// </summary>
|
||||
internal static string DefaultConstraintResolver_AmbiguousCtors
|
||||
{
|
||||
get => GetString("DefaultConstraintResolver_AmbiguousCtors");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}.
|
||||
/// </summary>
|
||||
internal static string FormatDefaultConstraintResolver_AmbiguousCtors(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("DefaultConstraintResolver_AmbiguousCtors"), p0, p1);
|
||||
|
||||
/// <summary>
|
||||
/// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.
|
||||
/// </summary>
|
||||
internal static string DefaultConstraintResolver_CouldNotFindCtor
|
||||
{
|
||||
get => GetString("DefaultConstraintResolver_CouldNotFindCtor");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.
|
||||
/// </summary>
|
||||
internal static string FormatDefaultConstraintResolver_CouldNotFindCtor(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("DefaultConstraintResolver_CouldNotFindCtor"), p0, p1);
|
||||
|
||||
/// <summary>
|
||||
/// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.
|
||||
/// </summary>
|
||||
internal static string DefaultConstraintResolver_TypeNotConstraint
|
||||
{
|
||||
get => GetString("DefaultConstraintResolver_TypeNotConstraint");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.
|
||||
/// </summary>
|
||||
internal static string FormatDefaultConstraintResolver_TypeNotConstraint(object p0, object p1, object p2)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("DefaultConstraintResolver_TypeNotConstraint"), p0, p1, p2);
|
||||
|
||||
/// <summary>
|
||||
/// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.
|
||||
/// </summary>
|
||||
internal static string DispatcherValueConstraintBuilder_CouldNotResolveConstraint
|
||||
{
|
||||
get => GetString("DispatcherValueConstraintBuilder_CouldNotResolveConstraint");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.
|
||||
/// </summary>
|
||||
internal static string FormatDispatcherValueConstraintBuilder_CouldNotResolveConstraint(object p0, object p1, object p2, object p3)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("DispatcherValueConstraintBuilder_CouldNotResolveConstraint"), p0, p1, p2, p3);
|
||||
|
||||
/// <summary>
|
||||
/// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.
|
||||
/// </summary>
|
||||
internal static string DispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint
|
||||
{
|
||||
get => GetString("DispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.
|
||||
/// </summary>
|
||||
internal static string FormatDispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint(object p0, object p1, object p2, object p3)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("DispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint"), p0, p1, p2, p3);
|
||||
|
||||
/// <summary>
|
||||
/// The collection cannot be empty.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -124,6 +124,21 @@
|
|||
<data name="Argument_NullOrEmpty" xml:space="preserve">
|
||||
<value>Value cannot be null or empty.</value>
|
||||
</data>
|
||||
<data name="DefaultConstraintResolver_AmbiguousCtors" xml:space="preserve">
|
||||
<value>The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}.</value>
|
||||
</data>
|
||||
<data name="DefaultConstraintResolver_CouldNotFindCtor" xml:space="preserve">
|
||||
<value>Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.</value>
|
||||
</data>
|
||||
<data name="DefaultConstraintResolver_TypeNotConstraint" xml:space="preserve">
|
||||
<value>The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.</value>
|
||||
</data>
|
||||
<data name="DispatcherValueConstraintBuilder_CouldNotResolveConstraint" xml:space="preserve">
|
||||
<value>The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.</value>
|
||||
</data>
|
||||
<data name="DispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint" xml:space="preserve">
|
||||
<value>The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.</value>
|
||||
</data>
|
||||
<data name="RoutePatternBuilder_CollectionCannotBeEmpty" xml:space="preserve">
|
||||
<value>The collection cannot be empty.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
// 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.Linq.Expressions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Dispatcher
|
||||
{
|
||||
public class CompositeDispatcherValueConstraintTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(true, true, true)]
|
||||
[InlineData(true, false, false)]
|
||||
[InlineData(false, true, false)]
|
||||
[InlineData(false, false, false)]
|
||||
public void CompositeRouteConstraint_Match_CallsMatchOnInnerConstraints(
|
||||
bool inner1Result,
|
||||
bool inner2Result,
|
||||
bool expected)
|
||||
{
|
||||
// Arrange
|
||||
var inner1 = MockConstraintWithResult(inner1Result);
|
||||
var inner2 = MockConstraintWithResult(inner2Result);
|
||||
|
||||
// Act
|
||||
var constraint = new CompositeDispatcherValueConstraint(new[] { inner1.Object, inner2.Object });
|
||||
var actual = TestConstraint(constraint, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
static Expression<Func<IDispatcherValueConstraint, bool>> ConstraintMatchMethodExpression =
|
||||
c => c.Match(
|
||||
It.IsAny<DispatcherValueConstraintContext>());
|
||||
|
||||
private static Mock<IDispatcherValueConstraint> MockConstraintWithResult(bool result)
|
||||
{
|
||||
var mock = new Mock<IDispatcherValueConstraint>();
|
||||
mock.Setup(ConstraintMatchMethodExpression)
|
||||
.Returns(result)
|
||||
.Verifiable();
|
||||
return mock;
|
||||
}
|
||||
|
||||
private static bool TestConstraint(IDispatcherValueConstraint constraint, object value, Action<IMatcher> routeConfig = null)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var values = new DispatcherValueCollection() { { "fake", value } };
|
||||
var constraintPurpose = ConstraintPurpose.IncomingRequest;
|
||||
|
||||
var dispatcherValueConstraintContext = new DispatcherValueConstraintContext(httpContext, values, constraintPurpose);
|
||||
|
||||
return constraint.Match(dispatcherValueConstraintContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
// 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.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Dispatcher
|
||||
{
|
||||
public class RegexDispatcherValueConstraintTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("abc", "abc", true)] // simple match
|
||||
[InlineData("Abc", "abc", true)] // case insensitive match
|
||||
[InlineData("Abc ", "abc", true)] // Extra space on input match (because we don't add ^({0})$
|
||||
[InlineData("Abcd", "abc", true)] // Extra char
|
||||
[InlineData("^Abcd", "abc", true)] // Extra special char
|
||||
[InlineData("Abc", " abc", false)] // Missing char
|
||||
[InlineData("123-456-2334", @"^\d{3}-\d{3}-\d{4}$", true)] // ssn
|
||||
[InlineData(@"12/4/2013", @"^\d{1,2}\/\d{1,2}\/\d{4}$", true)] // date
|
||||
[InlineData(@"abc@def.com", @"^\w+[\w\.]*\@\w+((-\w+)|(\w*))\.[a-z]{2,3}$", true)] // email
|
||||
public void RegexConstraintBuildRegexVerbatimFromInput(
|
||||
string routeValue,
|
||||
string constraintValue,
|
||||
bool shouldMatch)
|
||||
{
|
||||
// Arrange
|
||||
var constraint = new RegexDispatcherValueConstraint(constraintValue);
|
||||
var values = new DispatcherValueCollection(new { controller = routeValue });
|
||||
|
||||
// Act
|
||||
var match = TestConstraint(constraint, values, "controller");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(shouldMatch, match);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegexConstraint_TakesRegexAsInput_SimpleMatch()
|
||||
{
|
||||
// Arrange
|
||||
var constraint = new RegexDispatcherValueConstraint(new Regex("^abc$"));
|
||||
var values = new DispatcherValueCollection(new { controller = "abc" });
|
||||
|
||||
// Act
|
||||
var match = TestConstraint(constraint, values, "controller");
|
||||
|
||||
// Assert
|
||||
Assert.True(match);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegexConstraintConstructedWithRegex_SimpleFailedMatch()
|
||||
{
|
||||
// Arrange
|
||||
var constraint = new RegexDispatcherValueConstraint(new Regex("^abc$"));
|
||||
var values = new DispatcherValueCollection(new { controller = "Abc" });
|
||||
|
||||
// Act
|
||||
var match = TestConstraint(constraint, values, "controller");
|
||||
|
||||
// Assert
|
||||
Assert.False(match);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RegexConstraintFailsIfKeyIsNotFoundInRouteValues()
|
||||
{
|
||||
// Arrange
|
||||
var constraint = new RegexDispatcherValueConstraint(new Regex("^abc$"));
|
||||
var values = new DispatcherValueCollection(new { action = "abc" });
|
||||
|
||||
// Act
|
||||
var match = TestConstraint(constraint, values, "controller");
|
||||
|
||||
// Assert
|
||||
Assert.False(match);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("tr-TR")]
|
||||
[InlineData("en-US")]
|
||||
public void RegexConstraintIsCultureInsensitiveWhenConstructedWithString(string culture)
|
||||
{
|
||||
if (TestPlatformHelper.IsMono)
|
||||
{
|
||||
// The Regex in Mono returns true when matching the Turkish I for the a-z range which causes the test
|
||||
// to fail. Tracked via #100.
|
||||
return;
|
||||
}
|
||||
|
||||
// Arrange
|
||||
var constraint = new RegexDispatcherValueConstraint("^([a-z]+)$");
|
||||
var values = new DispatcherValueCollection(new { controller = "\u0130" }); // Turkish upper-case dotted I
|
||||
|
||||
using (new CultureReplacer(culture))
|
||||
{
|
||||
// Act
|
||||
var match = TestConstraint(constraint, values, "controller");
|
||||
|
||||
// Assert
|
||||
Assert.False(match);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TestConstraint(IDispatcherValueConstraint constraint, DispatcherValueCollection values, string routeKey)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var constraintPurpose = ConstraintPurpose.IncomingRequest;
|
||||
|
||||
var dispatcherValueConstraintContext = new DispatcherValueConstraintContext(httpContext, values, constraintPurpose)
|
||||
{
|
||||
Key = routeKey
|
||||
};
|
||||
|
||||
return constraint.Match(dispatcherValueConstraintContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue