Skip to content

Commit 3fadca6

Browse files
authored
Add IConstraintFactory (#487)
Addresses part of #472
1 parent f4fb178 commit 3fadca6

14 files changed

+794
-0
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
7+
namespace Microsoft.AspNetCore.Dispatcher
8+
{
9+
/// <summary>
10+
/// Constrains a dispatcher value by several child constraints.
11+
/// </summary>
12+
public class CompositeDispatcherValueConstraint : IDispatcherValueConstraint
13+
{
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="CompositeDispatcherValueConstraint" /> class.
16+
/// </summary>
17+
/// <param name="constraints">The child constraints that must match for this constraint to match.</param>
18+
public CompositeDispatcherValueConstraint(IEnumerable<IDispatcherValueConstraint> constraints)
19+
{
20+
if (constraints == null)
21+
{
22+
throw new ArgumentNullException(nameof(constraints));
23+
}
24+
25+
Constraints = constraints;
26+
}
27+
28+
/// <summary>
29+
/// Gets the child constraints that must match for this constraint to match.
30+
/// </summary>
31+
public IEnumerable<IDispatcherValueConstraint> Constraints { get; private set; }
32+
33+
/// <inheritdoc />
34+
public bool Match(DispatcherValueConstraintContext constraintContext)
35+
{
36+
if (constraintContext == null)
37+
{
38+
throw new ArgumentNullException(nameof(constraintContext));
39+
}
40+
41+
foreach (var constraint in Constraints)
42+
{
43+
if (!constraint.Match(constraintContext))
44+
{
45+
return false;
46+
}
47+
}
48+
49+
return true;
50+
}
51+
}
52+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Globalization;
7+
using System.Linq;
8+
using System.Reflection;
9+
using Microsoft.Extensions.Options;
10+
11+
namespace Microsoft.AspNetCore.Dispatcher
12+
{
13+
/// <summary>
14+
/// The default implementation of <see cref="IConstraintFactory"/>. Resolves constraints by parsing
15+
/// a constraint key and constraint arguments, using a map to resolve the constraint type, and calling an
16+
/// appropriate constructor for the constraint type.
17+
/// </summary>
18+
public class DefaultConstraintFactory : IConstraintFactory
19+
{
20+
private readonly IDictionary<string, Type> _constraintMap;
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="DefaultConstraintFactory"/> class.
24+
/// </summary>
25+
/// <param name="dispatcherOptions">
26+
/// Accessor for <see cref="DispatcherOptions"/> containing the constraints of interest.
27+
/// </param>
28+
public DefaultConstraintFactory(IOptions<DispatcherOptions> dispatcherOptions)
29+
{
30+
_constraintMap = dispatcherOptions.Value.ConstraintMap;
31+
}
32+
33+
/// <inheritdoc />
34+
/// <example>
35+
/// A typical constraint looks like the following
36+
/// "exampleConstraint(arg1, arg2, 12)".
37+
/// Here if the type registered for exampleConstraint has a single constructor with one argument,
38+
/// The entire string "arg1, arg2, 12" will be treated as a single argument.
39+
/// In all other cases arguments are split at comma.
40+
/// </example>
41+
public virtual IDispatcherValueConstraint ResolveConstraint(string constraint)
42+
{
43+
if (constraint == null)
44+
{
45+
throw new ArgumentNullException(nameof(constraint));
46+
}
47+
48+
string constraintKey;
49+
string argumentString;
50+
var indexOfFirstOpenParens = constraint.IndexOf('(');
51+
if (indexOfFirstOpenParens >= 0 && constraint.EndsWith(")", StringComparison.Ordinal))
52+
{
53+
constraintKey = constraint.Substring(0, indexOfFirstOpenParens);
54+
argumentString = constraint.Substring(indexOfFirstOpenParens + 1,
55+
constraint.Length - indexOfFirstOpenParens - 2);
56+
}
57+
else
58+
{
59+
constraintKey = constraint;
60+
argumentString = null;
61+
}
62+
63+
if (!_constraintMap.TryGetValue(constraintKey, out var constraintType))
64+
{
65+
// Cannot resolve the constraint key
66+
return null;
67+
}
68+
69+
if (!typeof(IDispatcherValueConstraint).GetTypeInfo().IsAssignableFrom(constraintType.GetTypeInfo()))
70+
{
71+
throw new InvalidOperationException(
72+
Resources.FormatDefaultConstraintResolver_TypeNotConstraint(
73+
constraintType, constraintKey, typeof(IDispatcherValueConstraint).Name));
74+
}
75+
76+
try
77+
{
78+
return CreateConstraint(constraintType, argumentString);
79+
}
80+
catch (Exception exception)
81+
{
82+
throw new InvalidOperationException(
83+
$"An error occurred while trying to create an instance of route constraint '{constraintType.FullName}'.",
84+
exception);
85+
}
86+
}
87+
88+
private static IDispatcherValueConstraint CreateConstraint(Type constraintType, string argumentString)
89+
{
90+
// No arguments - call the default constructor
91+
if (argumentString == null)
92+
{
93+
return (IDispatcherValueConstraint)Activator.CreateInstance(constraintType);
94+
}
95+
96+
var constraintTypeInfo = constraintType.GetTypeInfo();
97+
ConstructorInfo activationConstructor = null;
98+
object[] parameters = null;
99+
var constructors = constraintTypeInfo.DeclaredConstructors.ToArray();
100+
101+
// If there is only one constructor and it has a single parameter, pass the argument string directly
102+
// This is necessary for the RegexDispatcherValueConstraint to ensure that patterns are not split on commas.
103+
if (constructors.Length == 1 && constructors[0].GetParameters().Length == 1)
104+
{
105+
activationConstructor = constructors[0];
106+
parameters = ConvertArguments(activationConstructor.GetParameters(), new string[] { argumentString });
107+
}
108+
else
109+
{
110+
var arguments = argumentString.Split(',').Select(argument => argument.Trim()).ToArray();
111+
112+
var matchingConstructors = constructors.Where(ci => ci.GetParameters().Length == arguments.Length)
113+
.ToArray();
114+
var constructorMatches = matchingConstructors.Length;
115+
116+
if (constructorMatches == 0)
117+
{
118+
throw new InvalidOperationException(
119+
Resources.FormatDefaultConstraintResolver_CouldNotFindCtor(
120+
constraintTypeInfo.Name, arguments.Length));
121+
}
122+
else if (constructorMatches == 1)
123+
{
124+
activationConstructor = matchingConstructors[0];
125+
parameters = ConvertArguments(activationConstructor.GetParameters(), arguments);
126+
}
127+
else
128+
{
129+
throw new InvalidOperationException(
130+
Resources.FormatDefaultConstraintResolver_AmbiguousCtors(
131+
constraintTypeInfo.Name, arguments.Length));
132+
}
133+
}
134+
135+
return (IDispatcherValueConstraint)activationConstructor.Invoke(parameters);
136+
}
137+
138+
private static object[] ConvertArguments(ParameterInfo[] parameterInfos, string[] arguments)
139+
{
140+
var parameters = new object[parameterInfos.Length];
141+
for (var i = 0; i < parameterInfos.Length; i++)
142+
{
143+
var parameter = parameterInfos[i];
144+
var parameterType = parameter.ParameterType;
145+
parameters[i] = Convert.ChangeType(arguments[i], parameterType, CultureInfo.InvariantCulture);
146+
}
147+
148+
return parameters;
149+
}
150+
}
151+
}
152+
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
7+
namespace Microsoft.AspNetCore.Dispatcher
8+
{
9+
/// <summary>
10+
/// A builder for producing a mapping of keys to <see cref="IDispatcherValueConstraint"/>.
11+
/// </summary>
12+
/// <remarks>
13+
/// <see cref="DispatcherValueConstraintBuilder"/> allows iterative building a set of route constraints, and will
14+
/// merge multiple entries for the same key.
15+
/// </remarks>
16+
public class DispatcherValueConstraintBuilder
17+
{
18+
private readonly IConstraintFactory _constraintFactory;
19+
private readonly string _rawText;
20+
private readonly Dictionary<string, List<IDispatcherValueConstraint>> _constraints;
21+
private readonly HashSet<string> _optionalParameters;
22+
23+
/// <summary>
24+
/// Creates a new <see cref="DispatcherValueConstraintBuilder"/> instance.
25+
/// </summary>
26+
/// <param name="constraintFactory">The <see cref="IConstraintFactory"/>.</param>
27+
/// <param name="rawText">The display name (for use in error messages).</param>
28+
public DispatcherValueConstraintBuilder(
29+
IConstraintFactory constraintFactory,
30+
string rawText)
31+
{
32+
if (constraintFactory == null)
33+
{
34+
throw new ArgumentNullException(nameof(constraintFactory));
35+
}
36+
37+
if (rawText == null)
38+
{
39+
throw new ArgumentNullException(nameof(rawText));
40+
}
41+
42+
_constraintFactory = constraintFactory;
43+
_rawText = rawText;
44+
45+
_constraints = new Dictionary<string, List<IDispatcherValueConstraint>>(StringComparer.OrdinalIgnoreCase);
46+
_optionalParameters = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
47+
}
48+
49+
/// <summary>
50+
/// Builds a mapping of constraints.
51+
/// </summary>
52+
/// <returns>An <see cref="IDictionary{String, IDispatcherValueConstraint}"/> of the constraints.</returns>
53+
public IDictionary<string, IDispatcherValueConstraint> Build()
54+
{
55+
var constraints = new Dictionary<string, IDispatcherValueConstraint>(StringComparer.OrdinalIgnoreCase);
56+
foreach (var kvp in _constraints)
57+
{
58+
IDispatcherValueConstraint constraint;
59+
if (kvp.Value.Count == 1)
60+
{
61+
constraint = kvp.Value[0];
62+
}
63+
else
64+
{
65+
constraint = new CompositeDispatcherValueConstraint(kvp.Value.ToArray());
66+
}
67+
68+
if (_optionalParameters.Contains(kvp.Key))
69+
{
70+
var optionalConstraint = new OptionalDispatcherValueConstraint(constraint);
71+
constraints.Add(kvp.Key, optionalConstraint);
72+
}
73+
else
74+
{
75+
constraints.Add(kvp.Key, constraint);
76+
}
77+
}
78+
79+
return constraints;
80+
}
81+
82+
/// <summary>
83+
/// Adds a constraint instance for the given key.
84+
/// </summary>
85+
/// <param name="key">The key.</param>
86+
/// <param name="value">
87+
/// The constraint instance. Must either be a string or an instance of <see cref="IDispatcherValueConstraint"/>.
88+
/// </param>
89+
/// <remarks>
90+
/// If the <paramref name="value"/> is a string, it will be converted to a <see cref="RegexDispatcherValueConstraint"/>.
91+
///
92+
/// For example, the string <code>Product[0-9]+</code> will be converted to the regular expression
93+
/// <code>^(Product[0-9]+)</code>. See <see cref="System.Text.RegularExpressions.Regex"/> for more details.
94+
/// </remarks>
95+
public void AddConstraint(string key, object value)
96+
{
97+
if (key == null)
98+
{
99+
throw new ArgumentNullException(nameof(key));
100+
}
101+
102+
if (value == null)
103+
{
104+
throw new ArgumentNullException(nameof(value));
105+
}
106+
107+
var constraint = value as IDispatcherValueConstraint;
108+
if (constraint == null)
109+
{
110+
var regexPattern = value as string;
111+
if (regexPattern == null)
112+
{
113+
throw new InvalidOperationException(
114+
Resources.FormatDispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint(
115+
key,
116+
value,
117+
_rawText,
118+
typeof(IDispatcherValueConstraint)));
119+
}
120+
121+
var constraintsRegEx = "^(" + regexPattern + ")$";
122+
constraint = new RegexDispatcherValueConstraint(constraintsRegEx);
123+
}
124+
125+
Add(key, constraint);
126+
}
127+
128+
/// <summary>
129+
/// Adds a constraint for the given key, resolved by the <see cref="IConstraintFactory"/>.
130+
/// </summary>
131+
/// <param name="key">The key.</param>
132+
/// <param name="constraintText">The text to be resolved by <see cref="IConstraintFactory"/>.</param>
133+
/// <remarks>
134+
/// The <see cref="IConstraintFactory"/> can create <see cref="IDispatcherValueConstraint"/> instances
135+
/// based on <paramref name="constraintText"/>. See <see cref="DispatcherOptions.ConstraintMap"/> to register
136+
/// custom constraint types.
137+
/// </remarks>
138+
public void AddResolvedConstraint(string key, string constraintText)
139+
{
140+
if (key == null)
141+
{
142+
throw new ArgumentNullException(nameof(key));
143+
}
144+
145+
if (constraintText == null)
146+
{
147+
throw new ArgumentNullException(nameof(constraintText));
148+
}
149+
150+
var constraint = _constraintFactory.ResolveConstraint(constraintText);
151+
if (constraint == null)
152+
{
153+
throw new InvalidOperationException(
154+
Resources.FormatDispatcherValueConstraintBuilder_CouldNotResolveConstraint(
155+
key,
156+
constraintText,
157+
_rawText,
158+
_constraintFactory.GetType().Name));
159+
}
160+
161+
Add(key, constraint);
162+
}
163+
164+
/// <summary>
165+
/// Sets the given key as optional.
166+
/// </summary>
167+
/// <param name="key">The key.</param>
168+
public void SetOptional(string key)
169+
{
170+
if (key == null)
171+
{
172+
throw new ArgumentNullException(nameof(key));
173+
}
174+
175+
_optionalParameters.Add(key);
176+
}
177+
178+
private void Add(string key, IDispatcherValueConstraint constraint)
179+
{
180+
if (!_constraints.TryGetValue(key, out var list))
181+
{
182+
list = new List<IDispatcherValueConstraint>();
183+
_constraints.Add(key, list);
184+
}
185+
186+
list.Add(constraint);
187+
}
188+
}
189+
}

0 commit comments

Comments
 (0)