| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911 |
- commit 3fadca6a1b96c69ecfc217f20c69146d584cb3fe
- Author: Jass Bagga <[email protected]>
- Date: Thu Nov 2 10:57:37 2017 -0700
- Add IConstraintFactory (#487)
-
- Addresses part of #472
- diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/CompositeDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/CompositeDispatcherValueConstraint.cs
- new file mode 100644
- index 00000000000..8e7ea40209a
- --- /dev/null
- +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/CompositeDispatcherValueConstraint.cs
- @@ -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;
- + }
- + }
- +}
- \ No newline at end of file
- diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/DefaultConstraintFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DefaultConstraintFactory.cs
- new file mode 100644
- index 00000000000..687dc84afdc
- --- /dev/null
- +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DefaultConstraintFactory.cs
- @@ -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;
- + }
- + }
- +}
- +
- diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs
- new file mode 100644
- index 00000000000..ed93324b98b
- --- /dev/null
- +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs
- @@ -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);
- + }
- + }
- +}
- diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherValueConstraintContext.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintContext.cs
- similarity index 100%
- rename from src/Microsoft.AspNetCore.Dispatcher/DispatcherValueConstraintContext.cs
- rename to src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintContext.cs
- diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/IConstraintFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/IConstraintFactory.cs
- new file mode 100644
- index 00000000000..ca5acbbc74b
- --- /dev/null
- +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/IConstraintFactory.cs
- @@ -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);
- + }
- +}
- \ No newline at end of file
- diff --git a/src/Microsoft.AspNetCore.Dispatcher/IDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/IDispatcherValueConstraint.cs
- similarity index 100%
- rename from src/Microsoft.AspNetCore.Dispatcher/IDispatcherValueConstraint.cs
- rename to src/Microsoft.AspNetCore.Dispatcher/Constraints/IDispatcherValueConstraint.cs
- diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/OptionalDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/OptionalDispatcherValueConstraint.cs
- new file mode 100644
- index 00000000000..f7fccc2a65d
- --- /dev/null
- +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/OptionalDispatcherValueConstraint.cs
- @@ -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;
- + }
- + }
- +}
- \ No newline at end of file
- diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexDispatcherValueConstraint.cs
- new file mode 100644
- index 00000000000..99d0a10cfea
- --- /dev/null
- +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexDispatcherValueConstraint.cs
- @@ -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;
- + }
- + }
- +}
- diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs
- index 1df9f50cc9d..a4a26f662f1 100644
- --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs
- +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs
- @@ -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>();
- }
- }
- diff --git a/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs b/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs
- index b9d91cba8e8..89ff7048b6a 100644
- --- a/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs
- +++ b/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs
- @@ -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);
- diff --git a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs
- index 7cfa53c69e8..ff22f3d26c5 100644
- --- a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs
- +++ b/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs
- @@ -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>
- diff --git a/src/Microsoft.AspNetCore.Dispatcher/Resources.resx b/src/Microsoft.AspNetCore.Dispatcher/Resources.resx
- index 23f44055ad9..cb7814278cd 100644
- --- a/src/Microsoft.AspNetCore.Dispatcher/Resources.resx
- +++ b/src/Microsoft.AspNetCore.Dispatcher/Resources.resx
- @@ -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>
- diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/CompositeDispatcherValueConstraintTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/CompositeDispatcherValueConstraintTest.cs
- new file mode 100644
- index 00000000000..2c24ab60f07
- --- /dev/null
- +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/CompositeDispatcherValueConstraintTest.cs
- @@ -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);
- + }
- + }
- +}
- diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/RegexDispatcherValueConstraintTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/RegexDispatcherValueConstraintTest.cs
- new file mode 100644
- index 00000000000..6b6f23bc625
- --- /dev/null
- +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/RegexDispatcherValueConstraintTest.cs
- @@ -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(@"[email protected]", @"^\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);
- + }
- + }
- +}
|