Routing 738 B

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911
  1. commit 3fadca6a1b96c69ecfc217f20c69146d584cb3fe
  2. Author: Jass Bagga <[email protected]>
  3. Date: Thu Nov 2 10:57:37 2017 -0700
  4. Add IConstraintFactory (#487)
  5. Addresses part of #472
  6. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/CompositeDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/CompositeDispatcherValueConstraint.cs
  7. new file mode 100644
  8. index 00000000000..8e7ea40209a
  9. --- /dev/null
  10. +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/CompositeDispatcherValueConstraint.cs
  11. @@ -0,0 +1,52 @@
  12. +// Copyright (c) .NET Foundation. All rights reserved.
  13. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  14. +
  15. +using System;
  16. +using System.Collections.Generic;
  17. +
  18. +namespace Microsoft.AspNetCore.Dispatcher
  19. +{
  20. + /// <summary>
  21. + /// Constrains a dispatcher value by several child constraints.
  22. + /// </summary>
  23. + public class CompositeDispatcherValueConstraint : IDispatcherValueConstraint
  24. + {
  25. + /// <summary>
  26. + /// Initializes a new instance of the <see cref="CompositeDispatcherValueConstraint" /> class.
  27. + /// </summary>
  28. + /// <param name="constraints">The child constraints that must match for this constraint to match.</param>
  29. + public CompositeDispatcherValueConstraint(IEnumerable<IDispatcherValueConstraint> constraints)
  30. + {
  31. + if (constraints == null)
  32. + {
  33. + throw new ArgumentNullException(nameof(constraints));
  34. + }
  35. +
  36. + Constraints = constraints;
  37. + }
  38. +
  39. + /// <summary>
  40. + /// Gets the child constraints that must match for this constraint to match.
  41. + /// </summary>
  42. + public IEnumerable<IDispatcherValueConstraint> Constraints { get; private set; }
  43. +
  44. + /// <inheritdoc />
  45. + public bool Match(DispatcherValueConstraintContext constraintContext)
  46. + {
  47. + if (constraintContext == null)
  48. + {
  49. + throw new ArgumentNullException(nameof(constraintContext));
  50. + }
  51. +
  52. + foreach (var constraint in Constraints)
  53. + {
  54. + if (!constraint.Match(constraintContext))
  55. + {
  56. + return false;
  57. + }
  58. + }
  59. +
  60. + return true;
  61. + }
  62. + }
  63. +}
  64. \ No newline at end of file
  65. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/DefaultConstraintFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DefaultConstraintFactory.cs
  66. new file mode 100644
  67. index 00000000000..687dc84afdc
  68. --- /dev/null
  69. +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DefaultConstraintFactory.cs
  70. @@ -0,0 +1,152 @@
  71. +// Copyright (c) .NET Foundation. All rights reserved.
  72. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  73. +
  74. +using System;
  75. +using System.Collections.Generic;
  76. +using System.Globalization;
  77. +using System.Linq;
  78. +using System.Reflection;
  79. +using Microsoft.Extensions.Options;
  80. +
  81. +namespace Microsoft.AspNetCore.Dispatcher
  82. +{
  83. + /// <summary>
  84. + /// The default implementation of <see cref="IConstraintFactory"/>. Resolves constraints by parsing
  85. + /// a constraint key and constraint arguments, using a map to resolve the constraint type, and calling an
  86. + /// appropriate constructor for the constraint type.
  87. + /// </summary>
  88. + public class DefaultConstraintFactory : IConstraintFactory
  89. + {
  90. + private readonly IDictionary<string, Type> _constraintMap;
  91. +
  92. + /// <summary>
  93. + /// Initializes a new instance of the <see cref="DefaultConstraintFactory"/> class.
  94. + /// </summary>
  95. + /// <param name="dispatcherOptions">
  96. + /// Accessor for <see cref="DispatcherOptions"/> containing the constraints of interest.
  97. + /// </param>
  98. + public DefaultConstraintFactory(IOptions<DispatcherOptions> dispatcherOptions)
  99. + {
  100. + _constraintMap = dispatcherOptions.Value.ConstraintMap;
  101. + }
  102. +
  103. + /// <inheritdoc />
  104. + /// <example>
  105. + /// A typical constraint looks like the following
  106. + /// "exampleConstraint(arg1, arg2, 12)".
  107. + /// Here if the type registered for exampleConstraint has a single constructor with one argument,
  108. + /// The entire string "arg1, arg2, 12" will be treated as a single argument.
  109. + /// In all other cases arguments are split at comma.
  110. + /// </example>
  111. + public virtual IDispatcherValueConstraint ResolveConstraint(string constraint)
  112. + {
  113. + if (constraint == null)
  114. + {
  115. + throw new ArgumentNullException(nameof(constraint));
  116. + }
  117. +
  118. + string constraintKey;
  119. + string argumentString;
  120. + var indexOfFirstOpenParens = constraint.IndexOf('(');
  121. + if (indexOfFirstOpenParens >= 0 && constraint.EndsWith(")", StringComparison.Ordinal))
  122. + {
  123. + constraintKey = constraint.Substring(0, indexOfFirstOpenParens);
  124. + argumentString = constraint.Substring(indexOfFirstOpenParens + 1,
  125. + constraint.Length - indexOfFirstOpenParens - 2);
  126. + }
  127. + else
  128. + {
  129. + constraintKey = constraint;
  130. + argumentString = null;
  131. + }
  132. +
  133. + if (!_constraintMap.TryGetValue(constraintKey, out var constraintType))
  134. + {
  135. + // Cannot resolve the constraint key
  136. + return null;
  137. + }
  138. +
  139. + if (!typeof(IDispatcherValueConstraint).GetTypeInfo().IsAssignableFrom(constraintType.GetTypeInfo()))
  140. + {
  141. + throw new InvalidOperationException(
  142. + Resources.FormatDefaultConstraintResolver_TypeNotConstraint(
  143. + constraintType, constraintKey, typeof(IDispatcherValueConstraint).Name));
  144. + }
  145. +
  146. + try
  147. + {
  148. + return CreateConstraint(constraintType, argumentString);
  149. + }
  150. + catch (Exception exception)
  151. + {
  152. + throw new InvalidOperationException(
  153. + $"An error occurred while trying to create an instance of route constraint '{constraintType.FullName}'.",
  154. + exception);
  155. + }
  156. + }
  157. +
  158. + private static IDispatcherValueConstraint CreateConstraint(Type constraintType, string argumentString)
  159. + {
  160. + // No arguments - call the default constructor
  161. + if (argumentString == null)
  162. + {
  163. + return (IDispatcherValueConstraint)Activator.CreateInstance(constraintType);
  164. + }
  165. +
  166. + var constraintTypeInfo = constraintType.GetTypeInfo();
  167. + ConstructorInfo activationConstructor = null;
  168. + object[] parameters = null;
  169. + var constructors = constraintTypeInfo.DeclaredConstructors.ToArray();
  170. +
  171. + // If there is only one constructor and it has a single parameter, pass the argument string directly
  172. + // This is necessary for the RegexDispatcherValueConstraint to ensure that patterns are not split on commas.
  173. + if (constructors.Length == 1 && constructors[0].GetParameters().Length == 1)
  174. + {
  175. + activationConstructor = constructors[0];
  176. + parameters = ConvertArguments(activationConstructor.GetParameters(), new string[] { argumentString });
  177. + }
  178. + else
  179. + {
  180. + var arguments = argumentString.Split(',').Select(argument => argument.Trim()).ToArray();
  181. +
  182. + var matchingConstructors = constructors.Where(ci => ci.GetParameters().Length == arguments.Length)
  183. + .ToArray();
  184. + var constructorMatches = matchingConstructors.Length;
  185. +
  186. + if (constructorMatches == 0)
  187. + {
  188. + throw new InvalidOperationException(
  189. + Resources.FormatDefaultConstraintResolver_CouldNotFindCtor(
  190. + constraintTypeInfo.Name, arguments.Length));
  191. + }
  192. + else if (constructorMatches == 1)
  193. + {
  194. + activationConstructor = matchingConstructors[0];
  195. + parameters = ConvertArguments(activationConstructor.GetParameters(), arguments);
  196. + }
  197. + else
  198. + {
  199. + throw new InvalidOperationException(
  200. + Resources.FormatDefaultConstraintResolver_AmbiguousCtors(
  201. + constraintTypeInfo.Name, arguments.Length));
  202. + }
  203. + }
  204. +
  205. + return (IDispatcherValueConstraint)activationConstructor.Invoke(parameters);
  206. + }
  207. +
  208. + private static object[] ConvertArguments(ParameterInfo[] parameterInfos, string[] arguments)
  209. + {
  210. + var parameters = new object[parameterInfos.Length];
  211. + for (var i = 0; i < parameterInfos.Length; i++)
  212. + {
  213. + var parameter = parameterInfos[i];
  214. + var parameterType = parameter.ParameterType;
  215. + parameters[i] = Convert.ChangeType(arguments[i], parameterType, CultureInfo.InvariantCulture);
  216. + }
  217. +
  218. + return parameters;
  219. + }
  220. + }
  221. +}
  222. +
  223. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs
  224. new file mode 100644
  225. index 00000000000..ed93324b98b
  226. --- /dev/null
  227. +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs
  228. @@ -0,0 +1,189 @@
  229. +// Copyright (c) .NET Foundation. All rights reserved.
  230. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  231. +
  232. +using System;
  233. +using System.Collections.Generic;
  234. +
  235. +namespace Microsoft.AspNetCore.Dispatcher
  236. +{
  237. + /// <summary>
  238. + /// A builder for producing a mapping of keys to <see cref="IDispatcherValueConstraint"/>.
  239. + /// </summary>
  240. + /// <remarks>
  241. + /// <see cref="DispatcherValueConstraintBuilder"/> allows iterative building a set of route constraints, and will
  242. + /// merge multiple entries for the same key.
  243. + /// </remarks>
  244. + public class DispatcherValueConstraintBuilder
  245. + {
  246. + private readonly IConstraintFactory _constraintFactory;
  247. + private readonly string _rawText;
  248. + private readonly Dictionary<string, List<IDispatcherValueConstraint>> _constraints;
  249. + private readonly HashSet<string> _optionalParameters;
  250. +
  251. + /// <summary>
  252. + /// Creates a new <see cref="DispatcherValueConstraintBuilder"/> instance.
  253. + /// </summary>
  254. + /// <param name="constraintFactory">The <see cref="IConstraintFactory"/>.</param>
  255. + /// <param name="rawText">The display name (for use in error messages).</param>
  256. + public DispatcherValueConstraintBuilder(
  257. + IConstraintFactory constraintFactory,
  258. + string rawText)
  259. + {
  260. + if (constraintFactory == null)
  261. + {
  262. + throw new ArgumentNullException(nameof(constraintFactory));
  263. + }
  264. +
  265. + if (rawText == null)
  266. + {
  267. + throw new ArgumentNullException(nameof(rawText));
  268. + }
  269. +
  270. + _constraintFactory = constraintFactory;
  271. + _rawText = rawText;
  272. +
  273. + _constraints = new Dictionary<string, List<IDispatcherValueConstraint>>(StringComparer.OrdinalIgnoreCase);
  274. + _optionalParameters = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  275. + }
  276. +
  277. + /// <summary>
  278. + /// Builds a mapping of constraints.
  279. + /// </summary>
  280. + /// <returns>An <see cref="IDictionary{String, IDispatcherValueConstraint}"/> of the constraints.</returns>
  281. + public IDictionary<string, IDispatcherValueConstraint> Build()
  282. + {
  283. + var constraints = new Dictionary<string, IDispatcherValueConstraint>(StringComparer.OrdinalIgnoreCase);
  284. + foreach (var kvp in _constraints)
  285. + {
  286. + IDispatcherValueConstraint constraint;
  287. + if (kvp.Value.Count == 1)
  288. + {
  289. + constraint = kvp.Value[0];
  290. + }
  291. + else
  292. + {
  293. + constraint = new CompositeDispatcherValueConstraint(kvp.Value.ToArray());
  294. + }
  295. +
  296. + if (_optionalParameters.Contains(kvp.Key))
  297. + {
  298. + var optionalConstraint = new OptionalDispatcherValueConstraint(constraint);
  299. + constraints.Add(kvp.Key, optionalConstraint);
  300. + }
  301. + else
  302. + {
  303. + constraints.Add(kvp.Key, constraint);
  304. + }
  305. + }
  306. +
  307. + return constraints;
  308. + }
  309. +
  310. + /// <summary>
  311. + /// Adds a constraint instance for the given key.
  312. + /// </summary>
  313. + /// <param name="key">The key.</param>
  314. + /// <param name="value">
  315. + /// The constraint instance. Must either be a string or an instance of <see cref="IDispatcherValueConstraint"/>.
  316. + /// </param>
  317. + /// <remarks>
  318. + /// If the <paramref name="value"/> is a string, it will be converted to a <see cref="RegexDispatcherValueConstraint"/>.
  319. + ///
  320. + /// For example, the string <code>Product[0-9]+</code> will be converted to the regular expression
  321. + /// <code>^(Product[0-9]+)</code>. See <see cref="System.Text.RegularExpressions.Regex"/> for more details.
  322. + /// </remarks>
  323. + public void AddConstraint(string key, object value)
  324. + {
  325. + if (key == null)
  326. + {
  327. + throw new ArgumentNullException(nameof(key));
  328. + }
  329. +
  330. + if (value == null)
  331. + {
  332. + throw new ArgumentNullException(nameof(value));
  333. + }
  334. +
  335. + var constraint = value as IDispatcherValueConstraint;
  336. + if (constraint == null)
  337. + {
  338. + var regexPattern = value as string;
  339. + if (regexPattern == null)
  340. + {
  341. + throw new InvalidOperationException(
  342. + Resources.FormatDispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint(
  343. + key,
  344. + value,
  345. + _rawText,
  346. + typeof(IDispatcherValueConstraint)));
  347. + }
  348. +
  349. + var constraintsRegEx = "^(" + regexPattern + ")$";
  350. + constraint = new RegexDispatcherValueConstraint(constraintsRegEx);
  351. + }
  352. +
  353. + Add(key, constraint);
  354. + }
  355. +
  356. + /// <summary>
  357. + /// Adds a constraint for the given key, resolved by the <see cref="IConstraintFactory"/>.
  358. + /// </summary>
  359. + /// <param name="key">The key.</param>
  360. + /// <param name="constraintText">The text to be resolved by <see cref="IConstraintFactory"/>.</param>
  361. + /// <remarks>
  362. + /// The <see cref="IConstraintFactory"/> can create <see cref="IDispatcherValueConstraint"/> instances
  363. + /// based on <paramref name="constraintText"/>. See <see cref="DispatcherOptions.ConstraintMap"/> to register
  364. + /// custom constraint types.
  365. + /// </remarks>
  366. + public void AddResolvedConstraint(string key, string constraintText)
  367. + {
  368. + if (key == null)
  369. + {
  370. + throw new ArgumentNullException(nameof(key));
  371. + }
  372. +
  373. + if (constraintText == null)
  374. + {
  375. + throw new ArgumentNullException(nameof(constraintText));
  376. + }
  377. +
  378. + var constraint = _constraintFactory.ResolveConstraint(constraintText);
  379. + if (constraint == null)
  380. + {
  381. + throw new InvalidOperationException(
  382. + Resources.FormatDispatcherValueConstraintBuilder_CouldNotResolveConstraint(
  383. + key,
  384. + constraintText,
  385. + _rawText,
  386. + _constraintFactory.GetType().Name));
  387. + }
  388. +
  389. + Add(key, constraint);
  390. + }
  391. +
  392. + /// <summary>
  393. + /// Sets the given key as optional.
  394. + /// </summary>
  395. + /// <param name="key">The key.</param>
  396. + public void SetOptional(string key)
  397. + {
  398. + if (key == null)
  399. + {
  400. + throw new ArgumentNullException(nameof(key));
  401. + }
  402. +
  403. + _optionalParameters.Add(key);
  404. + }
  405. +
  406. + private void Add(string key, IDispatcherValueConstraint constraint)
  407. + {
  408. + if (!_constraints.TryGetValue(key, out var list))
  409. + {
  410. + list = new List<IDispatcherValueConstraint>();
  411. + _constraints.Add(key, list);
  412. + }
  413. +
  414. + list.Add(constraint);
  415. + }
  416. + }
  417. +}
  418. diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherValueConstraintContext.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintContext.cs
  419. similarity index 100%
  420. rename from src/Microsoft.AspNetCore.Dispatcher/DispatcherValueConstraintContext.cs
  421. rename to src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintContext.cs
  422. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/IConstraintFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/IConstraintFactory.cs
  423. new file mode 100644
  424. index 00000000000..ca5acbbc74b
  425. --- /dev/null
  426. +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/IConstraintFactory.cs
  427. @@ -0,0 +1,18 @@
  428. +// Copyright (c) .NET Foundation. All rights reserved.
  429. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  430. +
  431. +namespace Microsoft.AspNetCore.Dispatcher
  432. +{
  433. + /// <summary>
  434. + /// Defines an abstraction for resolving constraints as instances of <see cref="IDispatcherValueConstraint"/>.
  435. + /// </summary>
  436. + public interface IConstraintFactory
  437. + {
  438. + /// <summary>
  439. + /// Resolves the constraint.
  440. + /// </summary>
  441. + /// <param name="constraint">The constraint to resolve.</param>
  442. + /// <returns>The <see cref="IDispatcherValueConstraint"/> the constraint was resolved to.</returns>
  443. + IDispatcherValueConstraint ResolveConstraint(string constraint);
  444. + }
  445. +}
  446. \ No newline at end of file
  447. diff --git a/src/Microsoft.AspNetCore.Dispatcher/IDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/IDispatcherValueConstraint.cs
  448. similarity index 100%
  449. rename from src/Microsoft.AspNetCore.Dispatcher/IDispatcherValueConstraint.cs
  450. rename to src/Microsoft.AspNetCore.Dispatcher/Constraints/IDispatcherValueConstraint.cs
  451. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/OptionalDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/OptionalDispatcherValueConstraint.cs
  452. new file mode 100644
  453. index 00000000000..f7fccc2a65d
  454. --- /dev/null
  455. +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/OptionalDispatcherValueConstraint.cs
  456. @@ -0,0 +1,42 @@
  457. +// Copyright (c) .NET Foundation. All rights reserved.
  458. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  459. +
  460. +using System;
  461. +
  462. +namespace Microsoft.AspNetCore.Dispatcher
  463. +{
  464. + /// <summary>
  465. + /// Defines a constraint on an optional parameter. If the parameter is present, then it is constrained by InnerConstraint.
  466. + /// </summary>
  467. + public class OptionalDispatcherValueConstraint : IDispatcherValueConstraint
  468. + {
  469. + public OptionalDispatcherValueConstraint(IDispatcherValueConstraint innerConstraint)
  470. + {
  471. + if (innerConstraint == null)
  472. + {
  473. + throw new ArgumentNullException(nameof(innerConstraint));
  474. + }
  475. +
  476. + InnerConstraint = innerConstraint;
  477. + }
  478. +
  479. + public IDispatcherValueConstraint InnerConstraint { get; }
  480. +
  481. + /// <inheritdoc />
  482. + public bool Match(DispatcherValueConstraintContext constraintContext)
  483. + {
  484. + if (constraintContext == null)
  485. + {
  486. + throw new ArgumentNullException(nameof(constraintContext));
  487. + }
  488. +
  489. + if (constraintContext.Values.TryGetValue(constraintContext.Key, out var routeValue)
  490. + && routeValue != null)
  491. + {
  492. + return InnerConstraint.Match(constraintContext);
  493. + }
  494. +
  495. + return true;
  496. + }
  497. + }
  498. +}
  499. \ No newline at end of file
  500. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexDispatcherValueConstraint.cs
  501. new file mode 100644
  502. index 00000000000..99d0a10cfea
  503. --- /dev/null
  504. +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexDispatcherValueConstraint.cs
  505. @@ -0,0 +1,58 @@
  506. +// Copyright (c) .NET Foundation. All rights reserved.
  507. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  508. +
  509. +using System;
  510. +using System.Globalization;
  511. +using System.Text.RegularExpressions;
  512. +
  513. +namespace Microsoft.AspNetCore.Dispatcher
  514. +{
  515. + public class RegexDispatcherValueConstraint : IDispatcherValueConstraint
  516. + {
  517. + private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10);
  518. +
  519. + public RegexDispatcherValueConstraint(Regex regex)
  520. + {
  521. + if (regex == null)
  522. + {
  523. + throw new ArgumentNullException(nameof(regex));
  524. + }
  525. +
  526. + Constraint = regex;
  527. + }
  528. +
  529. + public RegexDispatcherValueConstraint(string regexPattern)
  530. + {
  531. + if (regexPattern == null)
  532. + {
  533. + throw new ArgumentNullException(nameof(regexPattern));
  534. + }
  535. +
  536. + Constraint = new Regex(
  537. + regexPattern,
  538. + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
  539. + RegexMatchTimeout);
  540. + }
  541. +
  542. + public Regex Constraint { get; private set; }
  543. +
  544. + /// <inheritdoc />
  545. + public bool Match(DispatcherValueConstraintContext constraintContext)
  546. + {
  547. + if (constraintContext == null)
  548. + {
  549. + throw new ArgumentNullException(nameof(constraintContext));
  550. + }
  551. +
  552. + if (constraintContext.Values.TryGetValue(constraintContext.Key, out var routeValue)
  553. + && routeValue != null)
  554. + {
  555. + var parameterValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
  556. +
  557. + return Constraint.IsMatch(parameterValueString);
  558. + }
  559. +
  560. + return false;
  561. + }
  562. + }
  563. +}
  564. diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs
  565. index 1df9f50cc9d..a4a26f662f1 100644
  566. --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs
  567. +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs
  568. @@ -1,6 +1,7 @@
  569. // Copyright (c) .NET Foundation. All rights reserved.
  570. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  571. +using System;
  572. using System.Collections.Generic;
  573. namespace Microsoft.AspNetCore.Dispatcher
  574. @@ -8,5 +9,7 @@ namespace Microsoft.AspNetCore.Dispatcher
  575. public class DispatcherOptions
  576. {
  577. public MatcherCollection Matchers { get; } = new MatcherCollection();
  578. +
  579. + public IDictionary<string, Type> ConstraintMap = new Dictionary<string, Type>();
  580. }
  581. }
  582. diff --git a/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs b/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs
  583. index b9d91cba8e8..89ff7048b6a 100644
  584. --- a/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs
  585. +++ b/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs
  586. @@ -83,6 +83,21 @@ namespace Microsoft.AspNetCore.Dispatcher
  587. new EventId(3, "NoEndpointMatchedRequestMethod"),
  588. "No endpoint matched request method '{Method}'.");
  589. + // DispatcherValueConstraintMatcher
  590. + private static readonly Action<ILogger, object, string, IDispatcherValueConstraint, Exception> _routeValueDoesNotMatchConstraint = LoggerMessage.Define<object, string, IDispatcherValueConstraint>(
  591. + LogLevel.Debug,
  592. + 1,
  593. + "Route value '{RouteValue}' with key '{RouteKey}' did not match the constraint '{RouteConstraint}'.");
  594. +
  595. + public static void RouteValueDoesNotMatchConstraint(
  596. + this ILogger logger,
  597. + object routeValue,
  598. + string routeKey,
  599. + IDispatcherValueConstraint routeConstraint)
  600. + {
  601. + _routeValueDoesNotMatchConstraint(logger, routeValue, routeKey, routeConstraint, null);
  602. + }
  603. +
  604. public static void AmbiguousEndpoints(this ILogger logger, string ambiguousEndpoints)
  605. {
  606. _ambiguousEndpoints(logger, ambiguousEndpoints, null);
  607. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs
  608. index 7cfa53c69e8..ff22f3d26c5 100644
  609. --- a/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs
  610. +++ b/src/Microsoft.AspNetCore.Dispatcher/Properties/Resources.Designer.cs
  611. @@ -38,6 +38,76 @@ namespace Microsoft.AspNetCore.Dispatcher
  612. internal static string FormatArgument_NullOrEmpty()
  613. => GetString("Argument_NullOrEmpty");
  614. + /// <summary>
  615. + /// The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}.
  616. + /// </summary>
  617. + internal static string DefaultConstraintResolver_AmbiguousCtors
  618. + {
  619. + get => GetString("DefaultConstraintResolver_AmbiguousCtors");
  620. + }
  621. +
  622. + /// <summary>
  623. + /// The constructor to use for activating the constraint type '{0}' is ambiguous. Multiple constructors were found with the following number of parameters: {1}.
  624. + /// </summary>
  625. + internal static string FormatDefaultConstraintResolver_AmbiguousCtors(object p0, object p1)
  626. + => string.Format(CultureInfo.CurrentCulture, GetString("DefaultConstraintResolver_AmbiguousCtors"), p0, p1);
  627. +
  628. + /// <summary>
  629. + /// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.
  630. + /// </summary>
  631. + internal static string DefaultConstraintResolver_CouldNotFindCtor
  632. + {
  633. + get => GetString("DefaultConstraintResolver_CouldNotFindCtor");
  634. + }
  635. +
  636. + /// <summary>
  637. + /// Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.
  638. + /// </summary>
  639. + internal static string FormatDefaultConstraintResolver_CouldNotFindCtor(object p0, object p1)
  640. + => string.Format(CultureInfo.CurrentCulture, GetString("DefaultConstraintResolver_CouldNotFindCtor"), p0, p1);
  641. +
  642. + /// <summary>
  643. + /// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.
  644. + /// </summary>
  645. + internal static string DefaultConstraintResolver_TypeNotConstraint
  646. + {
  647. + get => GetString("DefaultConstraintResolver_TypeNotConstraint");
  648. + }
  649. +
  650. + /// <summary>
  651. + /// The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.
  652. + /// </summary>
  653. + internal static string FormatDefaultConstraintResolver_TypeNotConstraint(object p0, object p1, object p2)
  654. + => string.Format(CultureInfo.CurrentCulture, GetString("DefaultConstraintResolver_TypeNotConstraint"), p0, p1, p2);
  655. +
  656. + /// <summary>
  657. + /// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.
  658. + /// </summary>
  659. + internal static string DispatcherValueConstraintBuilder_CouldNotResolveConstraint
  660. + {
  661. + get => GetString("DispatcherValueConstraintBuilder_CouldNotResolveConstraint");
  662. + }
  663. +
  664. + /// <summary>
  665. + /// The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.
  666. + /// </summary>
  667. + internal static string FormatDispatcherValueConstraintBuilder_CouldNotResolveConstraint(object p0, object p1, object p2, object p3)
  668. + => string.Format(CultureInfo.CurrentCulture, GetString("DispatcherValueConstraintBuilder_CouldNotResolveConstraint"), p0, p1, p2, p3);
  669. +
  670. + /// <summary>
  671. + /// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.
  672. + /// </summary>
  673. + internal static string DispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint
  674. + {
  675. + get => GetString("DispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint");
  676. + }
  677. +
  678. + /// <summary>
  679. + /// The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.
  680. + /// </summary>
  681. + internal static string FormatDispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint(object p0, object p1, object p2, object p3)
  682. + => string.Format(CultureInfo.CurrentCulture, GetString("DispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint"), p0, p1, p2, p3);
  683. +
  684. /// <summary>
  685. /// The collection cannot be empty.
  686. /// </summary>
  687. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Resources.resx b/src/Microsoft.AspNetCore.Dispatcher/Resources.resx
  688. index 23f44055ad9..cb7814278cd 100644
  689. --- a/src/Microsoft.AspNetCore.Dispatcher/Resources.resx
  690. +++ b/src/Microsoft.AspNetCore.Dispatcher/Resources.resx
  691. @@ -124,6 +124,21 @@
  692. <data name="Argument_NullOrEmpty" xml:space="preserve">
  693. <value>Value cannot be null or empty.</value>
  694. </data>
  695. + <data name="DefaultConstraintResolver_AmbiguousCtors" xml:space="preserve">
  696. + <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>
  697. + </data>
  698. + <data name="DefaultConstraintResolver_CouldNotFindCtor" xml:space="preserve">
  699. + <value>Could not find a constructor for constraint type '{0}' with the following number of parameters: {1}.</value>
  700. + </data>
  701. + <data name="DefaultConstraintResolver_TypeNotConstraint" xml:space="preserve">
  702. + <value>The constraint type '{0}' which is mapped to constraint key '{1}' must implement the '{2}' interface.</value>
  703. + </data>
  704. + <data name="DispatcherValueConstraintBuilder_CouldNotResolveConstraint" xml:space="preserve">
  705. + <value>The constraint entry '{0}' - '{1}' on the route '{2}' could not be resolved by the constraint resolver of type '{3}'.</value>
  706. + </data>
  707. + <data name="DispatcherValueConstraintBuilder_ValidationMustBeStringOrCustomConstraint" xml:space="preserve">
  708. + <value>The constraint entry '{0}' - '{1}' on the route '{2}' must have a string value or be of a type which implements '{3}'.</value>
  709. + </data>
  710. <data name="RoutePatternBuilder_CollectionCannotBeEmpty" xml:space="preserve">
  711. <value>The collection cannot be empty.</value>
  712. </data>
  713. diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/CompositeDispatcherValueConstraintTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/CompositeDispatcherValueConstraintTest.cs
  714. new file mode 100644
  715. index 00000000000..2c24ab60f07
  716. --- /dev/null
  717. +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/CompositeDispatcherValueConstraintTest.cs
  718. @@ -0,0 +1,60 @@
  719. +// Copyright (c) .NET Foundation. All rights reserved.
  720. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  721. +
  722. +using System;
  723. +using System.Linq.Expressions;
  724. +using Microsoft.AspNetCore.Http;
  725. +using Moq;
  726. +using Xunit;
  727. +
  728. +namespace Microsoft.AspNetCore.Dispatcher
  729. +{
  730. + public class CompositeDispatcherValueConstraintTest
  731. + {
  732. + [Theory]
  733. + [InlineData(true, true, true)]
  734. + [InlineData(true, false, false)]
  735. + [InlineData(false, true, false)]
  736. + [InlineData(false, false, false)]
  737. + public void CompositeRouteConstraint_Match_CallsMatchOnInnerConstraints(
  738. + bool inner1Result,
  739. + bool inner2Result,
  740. + bool expected)
  741. + {
  742. + // Arrange
  743. + var inner1 = MockConstraintWithResult(inner1Result);
  744. + var inner2 = MockConstraintWithResult(inner2Result);
  745. +
  746. + // Act
  747. + var constraint = new CompositeDispatcherValueConstraint(new[] { inner1.Object, inner2.Object });
  748. + var actual = TestConstraint(constraint, null);
  749. +
  750. + // Assert
  751. + Assert.Equal(expected, actual);
  752. + }
  753. +
  754. + static Expression<Func<IDispatcherValueConstraint, bool>> ConstraintMatchMethodExpression =
  755. + c => c.Match(
  756. + It.IsAny<DispatcherValueConstraintContext>());
  757. +
  758. + private static Mock<IDispatcherValueConstraint> MockConstraintWithResult(bool result)
  759. + {
  760. + var mock = new Mock<IDispatcherValueConstraint>();
  761. + mock.Setup(ConstraintMatchMethodExpression)
  762. + .Returns(result)
  763. + .Verifiable();
  764. + return mock;
  765. + }
  766. +
  767. + private static bool TestConstraint(IDispatcherValueConstraint constraint, object value, Action<IMatcher> routeConfig = null)
  768. + {
  769. + var httpContext = new DefaultHttpContext();
  770. + var values = new DispatcherValueCollection() { { "fake", value } };
  771. + var constraintPurpose = ConstraintPurpose.IncomingRequest;
  772. +
  773. + var dispatcherValueConstraintContext = new DispatcherValueConstraintContext(httpContext, values, constraintPurpose);
  774. +
  775. + return constraint.Match(dispatcherValueConstraintContext);
  776. + }
  777. + }
  778. +}
  779. diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/RegexDispatcherValueConstraintTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/RegexDispatcherValueConstraintTest.cs
  780. new file mode 100644
  781. index 00000000000..6b6f23bc625
  782. --- /dev/null
  783. +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/Constraints/RegexDispatcherValueConstraintTest.cs
  784. @@ -0,0 +1,120 @@
  785. +// Copyright (c) .NET Foundation. All rights reserved.
  786. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  787. +
  788. +using System.Text.RegularExpressions;
  789. +using Microsoft.AspNetCore.Http;
  790. +using Microsoft.AspNetCore.Testing;
  791. +using Xunit;
  792. +
  793. +namespace Microsoft.AspNetCore.Dispatcher
  794. +{
  795. + public class RegexDispatcherValueConstraintTest
  796. + {
  797. + [Theory]
  798. + [InlineData("abc", "abc", true)] // simple match
  799. + [InlineData("Abc", "abc", true)] // case insensitive match
  800. + [InlineData("Abc ", "abc", true)] // Extra space on input match (because we don't add ^({0})$
  801. + [InlineData("Abcd", "abc", true)] // Extra char
  802. + [InlineData("^Abcd", "abc", true)] // Extra special char
  803. + [InlineData("Abc", " abc", false)] // Missing char
  804. + [InlineData("123-456-2334", @"^\d{3}-\d{3}-\d{4}$", true)] // ssn
  805. + [InlineData(@"12/4/2013", @"^\d{1,2}\/\d{1,2}\/\d{4}$", true)] // date
  806. + [InlineData(@"[email protected]", @"^\w+[\w\.]*\@\w+((-\w+)|(\w*))\.[a-z]{2,3}$", true)] // email
  807. + public void RegexConstraintBuildRegexVerbatimFromInput(
  808. + string routeValue,
  809. + string constraintValue,
  810. + bool shouldMatch)
  811. + {
  812. + // Arrange
  813. + var constraint = new RegexDispatcherValueConstraint(constraintValue);
  814. + var values = new DispatcherValueCollection(new { controller = routeValue });
  815. +
  816. + // Act
  817. + var match = TestConstraint(constraint, values, "controller");
  818. +
  819. + // Assert
  820. + Assert.Equal(shouldMatch, match);
  821. + }
  822. +
  823. + [Fact]
  824. + public void RegexConstraint_TakesRegexAsInput_SimpleMatch()
  825. + {
  826. + // Arrange
  827. + var constraint = new RegexDispatcherValueConstraint(new Regex("^abc$"));
  828. + var values = new DispatcherValueCollection(new { controller = "abc" });
  829. +
  830. + // Act
  831. + var match = TestConstraint(constraint, values, "controller");
  832. +
  833. + // Assert
  834. + Assert.True(match);
  835. + }
  836. +
  837. + [Fact]
  838. + public void RegexConstraintConstructedWithRegex_SimpleFailedMatch()
  839. + {
  840. + // Arrange
  841. + var constraint = new RegexDispatcherValueConstraint(new Regex("^abc$"));
  842. + var values = new DispatcherValueCollection(new { controller = "Abc" });
  843. +
  844. + // Act
  845. + var match = TestConstraint(constraint, values, "controller");
  846. +
  847. + // Assert
  848. + Assert.False(match);
  849. + }
  850. +
  851. + [Fact]
  852. + public void RegexConstraintFailsIfKeyIsNotFoundInRouteValues()
  853. + {
  854. + // Arrange
  855. + var constraint = new RegexDispatcherValueConstraint(new Regex("^abc$"));
  856. + var values = new DispatcherValueCollection(new { action = "abc" });
  857. +
  858. + // Act
  859. + var match = TestConstraint(constraint, values, "controller");
  860. +
  861. + // Assert
  862. + Assert.False(match);
  863. + }
  864. +
  865. + [Theory]
  866. + [InlineData("tr-TR")]
  867. + [InlineData("en-US")]
  868. + public void RegexConstraintIsCultureInsensitiveWhenConstructedWithString(string culture)
  869. + {
  870. + if (TestPlatformHelper.IsMono)
  871. + {
  872. + // The Regex in Mono returns true when matching the Turkish I for the a-z range which causes the test
  873. + // to fail. Tracked via #100.
  874. + return;
  875. + }
  876. +
  877. + // Arrange
  878. + var constraint = new RegexDispatcherValueConstraint("^([a-z]+)$");
  879. + var values = new DispatcherValueCollection(new { controller = "\u0130" }); // Turkish upper-case dotted I
  880. +
  881. + using (new CultureReplacer(culture))
  882. + {
  883. + // Act
  884. + var match = TestConstraint(constraint, values, "controller");
  885. +
  886. + // Assert
  887. + Assert.False(match);
  888. + }
  889. + }
  890. +
  891. + private static bool TestConstraint(IDispatcherValueConstraint constraint, DispatcherValueCollection values, string routeKey)
  892. + {
  893. + var httpContext = new DefaultHttpContext();
  894. + var constraintPurpose = ConstraintPurpose.IncomingRequest;
  895. +
  896. + var dispatcherValueConstraintContext = new DispatcherValueConstraintContext(httpContext, values, constraintPurpose)
  897. + {
  898. + Key = routeKey
  899. + };
  900. +
  901. + return constraint.Match(dispatcherValueConstraintContext);
  902. + }
  903. + }
  904. +}