Add IConstraintFactory (#487)

Addresses part of #472
This commit is contained in:
Jass Bagga 2017-11-02 10:57:37 -07:00 committed by GitHub
parent f4fb178f55
commit 3fadca6a1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 794 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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