Routing 724 B

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485
  1. commit fd83b300b8d3a0223762c18e4a3b6a6c32c3bb01
  2. Author: Jass Bagga <[email protected]>
  3. Date: Tue Nov 7 10:51:50 2017 -0800
  4. Port TreeMatcher (#488)
  5. Addresses #472
  6. diff --git a/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs
  7. index a8232e0b026..befa1725ecc 100644
  8. --- a/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs
  9. +++ b/benchmarks/Microsoft.AspNetCore.Dispatcher.Performance/DispatcherBenchmark.cs
  10. @@ -2,15 +2,11 @@
  11. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  12. using System;
  13. -using System.Text.Encodings.Web;
  14. +using System.Collections.Generic;
  15. using System.Threading.Tasks;
  16. using BenchmarkDotNet.Attributes;
  17. using Microsoft.AspNetCore.Http;
  18. using Microsoft.AspNetCore.Routing;
  19. -using Microsoft.AspNetCore.Routing.Tree;
  20. -using Microsoft.Extensions.Logging.Abstractions;
  21. -using Microsoft.Extensions.ObjectPool;
  22. -using Microsoft.Extensions.Options;
  23. namespace Microsoft.AspNetCore.Dispatcher.Performance
  24. {
  25. @@ -19,25 +15,25 @@ namespace Microsoft.AspNetCore.Dispatcher.Performance
  26. private const int NumberOfRequestTypes = 3;
  27. private const int Iterations = 100;
  28. - private readonly IRouter _treeRouter;
  29. + private readonly IMatcher _treeMatcher;
  30. private readonly RequestEntry[] _requests;
  31. public DispatcherBenchmark()
  32. {
  33. - var handler = new RouteHandler((next) => Task.FromResult<object>(null));
  34. -
  35. - var treeBuilder = new TreeRouteBuilder(
  36. - NullLoggerFactory.Instance,
  37. - new RoutePatternBinderFactory(UrlEncoder.Default, new DefaultObjectPoolProvider()),
  38. - new DefaultInlineConstraintResolver(Options.Create(new RouteOptions())));
  39. -
  40. - treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets"), "default", 0);
  41. - treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets/{id}"), "default", 0);
  42. - treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("api/Widgets/search/{term}"), "default", 0);
  43. - treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("admin/users/{id}"), "default", 0);
  44. - treeBuilder.MapInbound(handler, Routing.Template.TemplateParser.Parse("admin/users/{id}/manage"), "default", 0);
  45. + var dataSource = new DefaultDispatcherDataSource()
  46. + {
  47. + Endpoints =
  48. + {
  49. + new RoutePatternEndpoint("api/Widgets", Benchmark_Delegate),
  50. + new RoutePatternEndpoint("api/Widgets/{id}", Benchmark_Delegate),
  51. + new RoutePatternEndpoint("api/Widgets/search/{term}", Benchmark_Delegate),
  52. + new RoutePatternEndpoint("admin/users/{id}", Benchmark_Delegate),
  53. + new RoutePatternEndpoint("admin/users/{id}/manage", Benchmark_Delegate),
  54. + },
  55. + };
  56. - _treeRouter = treeBuilder.Build();
  57. + var factory = new TreeMatcherFactory();
  58. + _treeMatcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  59. _requests = new RequestEntry[NumberOfRequestTypes];
  60. @@ -64,38 +60,38 @@ namespace Microsoft.AspNetCore.Dispatcher.Performance
  61. {
  62. for (var j = 0; j < _requests.Length; j++)
  63. {
  64. - var context = new RouteContext(_requests[j].HttpContext);
  65. + var context = new MatcherContext(_requests[j].HttpContext);
  66. - await _treeRouter.RouteAsync(context);
  67. + await _treeMatcher.MatchAsync(context);
  68. Verify(context, j);
  69. }
  70. }
  71. }
  72. - private void Verify(RouteContext context, int i)
  73. + private void Verify(MatcherContext context, int i)
  74. {
  75. if (_requests[i].IsMatch)
  76. {
  77. - if (context.Handler == null)
  78. + if (context.Endpoint == null)
  79. {
  80. throw new InvalidOperationException($"Failed {i}");
  81. }
  82. var values = _requests[i].Values;
  83. - if (values.Count != context.RouteData.Values.Count)
  84. + if (values.Count != context.Values.Count)
  85. {
  86. throw new InvalidOperationException($"Failed {i}");
  87. }
  88. }
  89. else
  90. {
  91. - if (context.Handler != null)
  92. + if (context.Endpoint != null)
  93. {
  94. throw new InvalidOperationException($"Failed {i}");
  95. }
  96. - if (context.RouteData.Values.Count != 0)
  97. + if (context.Values.Count != 0)
  98. {
  99. throw new InvalidOperationException($"Failed {i}");
  100. }
  101. @@ -108,5 +104,10 @@ namespace Microsoft.AspNetCore.Dispatcher.Performance
  102. public bool IsMatch;
  103. public RouteValueDictionary Values;
  104. }
  105. +
  106. + private static Task Benchmark_Delegate(HttpContext httpContext)
  107. + {
  108. + return Task.CompletedTask;
  109. + }
  110. }
  111. }
  112. diff --git a/samples/DispatcherSample/Startup.cs b/samples/DispatcherSample/Startup.cs
  113. index 7317e8c2575..a9259cc14ef 100644
  114. --- a/samples/DispatcherSample/Startup.cs
  115. +++ b/samples/DispatcherSample/Startup.cs
  116. @@ -1,13 +1,11 @@
  117. // Copyright (c) .NET Foundation. All rights reserved.
  118. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  119. -using System.Linq;
  120. using System.Threading.Tasks;
  121. using Microsoft.AspNetCore.Builder;
  122. using Microsoft.AspNetCore.Dispatcher;
  123. using Microsoft.AspNetCore.Hosting;
  124. using Microsoft.AspNetCore.Http;
  125. -using Microsoft.AspNetCore.Routing.Dispatcher;
  126. using Microsoft.Extensions.DependencyInjection;
  127. using Microsoft.Extensions.Logging;
  128. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/AlphaDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/AlphaDispatcherValueConstraint.cs
  129. new file mode 100644
  130. index 00000000000..8291289ff13
  131. --- /dev/null
  132. +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/AlphaDispatcherValueConstraint.cs
  133. @@ -0,0 +1,18 @@
  134. +// Copyright (c) .NET Foundation. All rights reserved.
  135. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  136. +
  137. +namespace Microsoft.AspNetCore.Dispatcher
  138. +{
  139. + /// <summary>
  140. + /// Constrains a dispatcher value parameter to contain only lowercase or uppercase letters A through Z in the English alphabet.
  141. + /// </summary>
  142. + public class AlphaDispatcherValueConstraint : RegexDispatcherValueConstraint
  143. + {
  144. + /// <summary>
  145. + /// Initializes a new instance of the <see cref="AlphaDispatcherValueConstraint" /> class.
  146. + /// </summary>
  147. + public AlphaDispatcherValueConstraint() : base(@"^[a-z]*$")
  148. + {
  149. + }
  150. + }
  151. +}
  152. \ No newline at end of file
  153. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs
  154. index ed93324b98b..f1363f34e15 100644
  155. --- a/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs
  156. +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/DispatcherValueConstraintBuilder.cs
  157. @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Dispatcher
  158. /// A builder for producing a mapping of keys to <see cref="IDispatcherValueConstraint"/>.
  159. /// </summary>
  160. /// <remarks>
  161. - /// <see cref="DispatcherValueConstraintBuilder"/> allows iterative building a set of route constraints, and will
  162. + /// <see cref="DispatcherValueConstraintBuilder"/> allows iterative building a set of dispatcher value constraints, and will
  163. /// merge multiple entries for the same key.
  164. /// </remarks>
  165. public class DispatcherValueConstraintBuilder
  166. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/IntDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/IntDispatcherValueConstraint.cs
  167. new file mode 100644
  168. index 00000000000..77d9f70f747
  169. --- /dev/null
  170. +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/IntDispatcherValueConstraint.cs
  171. @@ -0,0 +1,36 @@
  172. +// Copyright (c) .NET Foundation. All rights reserved.
  173. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  174. +
  175. +using System;
  176. +using System.Globalization;
  177. +
  178. +namespace Microsoft.AspNetCore.Dispatcher
  179. +{
  180. + /// <summary>
  181. + /// Constrains a dispatcher value parameter to represent only 32-bit integer values.
  182. + /// </summary>
  183. + public class IntDispatcherValueConstraint : IDispatcherValueConstraint
  184. + {
  185. + /// <inheritdoc />
  186. + public bool Match(DispatcherValueConstraintContext constraintContext)
  187. + {
  188. + if (constraintContext == null)
  189. + {
  190. + throw new ArgumentNullException(nameof(constraintContext));
  191. + }
  192. +
  193. + if (constraintContext.Values.TryGetValue(constraintContext.Key, out var value) && value != null)
  194. + {
  195. + if (value is int)
  196. + {
  197. + return true;
  198. + }
  199. +
  200. + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
  201. + return int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result);
  202. + }
  203. +
  204. + return false;
  205. + }
  206. + }
  207. +}
  208. \ No newline at end of file
  209. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexStringDispatcherValueConstraint.cs b/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexStringDispatcherValueConstraint.cs
  210. new file mode 100644
  211. index 00000000000..67bcf263870
  212. --- /dev/null
  213. +++ b/src/Microsoft.AspNetCore.Dispatcher/Constraints/RegexStringDispatcherValueConstraint.cs
  214. @@ -0,0 +1,20 @@
  215. +// Copyright (c) .NET Foundation. All rights reserved.
  216. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  217. +
  218. +namespace Microsoft.AspNetCore.Dispatcher
  219. +{
  220. + /// <summary>
  221. + /// Represents a regex constraint.
  222. + /// </summary>
  223. + public class RegexStringDispatcherValueConstraint : RegexDispatcherValueConstraint
  224. + {
  225. + /// <summary>
  226. + /// Initializes a new instance of the <see cref="RegexStringDispatcherValueConstraint" /> class.
  227. + /// </summary>
  228. + /// <param name="regexPattern">The regular expression pattern to match.</param>
  229. + public RegexStringDispatcherValueConstraint(string regexPattern)
  230. + : base(regexPattern)
  231. + {
  232. + }
  233. + }
  234. +}
  235. diff --git a/src/Microsoft.AspNetCore.Dispatcher/DependencyInjection/DispatcherServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Dispatcher/DependencyInjection/DispatcherServiceCollectionExtensions.cs
  236. index ea36a43083e..26d61305057 100644
  237. --- a/src/Microsoft.AspNetCore.Dispatcher/DependencyInjection/DispatcherServiceCollectionExtensions.cs
  238. +++ b/src/Microsoft.AspNetCore.Dispatcher/DependencyInjection/DispatcherServiceCollectionExtensions.cs
  239. @@ -36,6 +36,7 @@ namespace Microsoft.Extensions.DependencyInjection
  240. // Misc Infrastructure
  241. //
  242. services.TryAddSingleton<RoutePatternBinderFactory>();
  243. + services.TryAddSingleton<IConstraintFactory, DefaultConstraintFactory>();
  244. services.TryAddEnumerable(ServiceDescriptor.Singleton<IHandlerFactory, RoutePatternEndpointHandlerFactory>());
  245. diff --git a/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs b/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs
  246. index a4a26f662f1..6e55164498a 100644
  247. --- a/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs
  248. +++ b/src/Microsoft.AspNetCore.Dispatcher/DispatcherOptions.cs
  249. @@ -10,6 +10,36 @@ namespace Microsoft.AspNetCore.Dispatcher
  250. {
  251. public MatcherCollection Matchers { get; } = new MatcherCollection();
  252. - public IDictionary<string, Type> ConstraintMap = new Dictionary<string, Type>();
  253. + private IDictionary<string, Type> _constraintTypeMap = GetDefaultConstraintMap();
  254. +
  255. + public IDictionary<string, Type> ConstraintMap
  256. + {
  257. + get
  258. + {
  259. + return _constraintTypeMap;
  260. + }
  261. + set
  262. + {
  263. + if (value == null)
  264. + {
  265. + throw new ArgumentNullException(nameof(ConstraintMap));
  266. + }
  267. +
  268. + _constraintTypeMap = value;
  269. + }
  270. + }
  271. +
  272. + private static IDictionary<string, Type> GetDefaultConstraintMap()
  273. + {
  274. + return new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
  275. + {
  276. + // Type-specific constraints
  277. + { "int", typeof(IntDispatcherValueConstraint) },
  278. +
  279. + //// Regex-based constraints
  280. + { "alpha", typeof(AlphaDispatcherValueConstraint) },
  281. + { "regex", typeof(RegexStringDispatcherValueConstraint) },
  282. + };
  283. + }
  284. }
  285. }
  286. diff --git a/src/Microsoft.AspNetCore.Dispatcher/EndpointOrderMetadata.cs b/src/Microsoft.AspNetCore.Dispatcher/EndpointOrderMetadata.cs
  287. new file mode 100644
  288. index 00000000000..eefbf1efa2b
  289. --- /dev/null
  290. +++ b/src/Microsoft.AspNetCore.Dispatcher/EndpointOrderMetadata.cs
  291. @@ -0,0 +1,15 @@
  292. +// Copyright (c) .NET Foundation. All rights reserved.
  293. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  294. +
  295. +namespace Microsoft.AspNetCore.Dispatcher
  296. +{
  297. + public class EndpointOrderMetadata : IEndpointOrderMetadata
  298. + {
  299. + public EndpointOrderMetadata(int order)
  300. + {
  301. + Order = order;
  302. + }
  303. +
  304. + public int Order { get; }
  305. + }
  306. +}
  307. diff --git a/src/Microsoft.AspNetCore.Dispatcher/IEndpointOrderMetadata.cs b/src/Microsoft.AspNetCore.Dispatcher/IEndpointOrderMetadata.cs
  308. new file mode 100644
  309. index 00000000000..31b270ae8f4
  310. --- /dev/null
  311. +++ b/src/Microsoft.AspNetCore.Dispatcher/IEndpointOrderMetadata.cs
  312. @@ -0,0 +1,10 @@
  313. +// Copyright (c) .NET Foundation. All rights reserved.
  314. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  315. +
  316. +namespace Microsoft.AspNetCore.Dispatcher
  317. +{
  318. + public interface IEndpointOrderMetadata
  319. + {
  320. + int Order { get; }
  321. + }
  322. +}
  323. diff --git a/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs b/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs
  324. index 89ff7048b6a..6143a9b3e18 100644
  325. --- a/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs
  326. +++ b/src/Microsoft.AspNetCore.Dispatcher/LoggerExtensions.cs
  327. @@ -83,7 +83,17 @@ namespace Microsoft.AspNetCore.Dispatcher
  328. new EventId(3, "NoEndpointMatchedRequestMethod"),
  329. "No endpoint matched request method '{Method}'.");
  330. - // DispatcherValueConstraintMatcher
  331. + // TreeMatcher
  332. + private static readonly Action<ILogger, string, Exception> _requestShortCircuited = LoggerMessage.Define<string>(
  333. + LogLevel.Information,
  334. + new EventId(3, "RequestShortCircuited"),
  335. + "The current request '{RequestPath}' was short circuited.");
  336. +
  337. + private static readonly Action<ILogger, string, Exception> _matchedRoute = LoggerMessage.Define<string>(
  338. + LogLevel.Debug,
  339. + 1,
  340. + "Request successfully matched the route pattern '{RoutePattern}'.");
  341. +
  342. private static readonly Action<ILogger, object, string, IDispatcherValueConstraint, Exception> _routeValueDoesNotMatchConstraint = LoggerMessage.Define<object, string, IDispatcherValueConstraint>(
  343. LogLevel.Debug,
  344. 1,
  345. @@ -98,6 +108,19 @@ namespace Microsoft.AspNetCore.Dispatcher
  346. _routeValueDoesNotMatchConstraint(logger, routeValue, routeKey, routeConstraint, null);
  347. }
  348. + public static void RequestShortCircuited(this ILogger logger, MatcherContext matcherContext)
  349. + {
  350. + var requestPath = matcherContext.HttpContext.Request.Path;
  351. + _requestShortCircuited(logger, requestPath, null);
  352. + }
  353. +
  354. + public static void MatchedRoute(
  355. + this ILogger logger,
  356. + string routePattern)
  357. + {
  358. + _matchedRoute(logger, routePattern, null);
  359. + }
  360. +
  361. public static void AmbiguousEndpoints(this ILogger logger, string ambiguousEndpoints)
  362. {
  363. _ambiguousEndpoints(logger, ambiguousEndpoints, null);
  364. diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternEndpoint.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternEndpoint.cs
  365. index 6e7a3ca3b07..1047159b6aa 100644
  366. --- a/src/Microsoft.AspNetCore.Dispatcher/RoutePatternEndpoint.cs
  367. +++ b/src/Microsoft.AspNetCore.Dispatcher/RoutePatternEndpoint.cs
  368. @@ -2,8 +2,6 @@
  369. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  370. using System;
  371. -using System.Collections.Generic;
  372. -using System.Linq;
  373. using Microsoft.AspNetCore.Http;
  374. namespace Microsoft.AspNetCore.Dispatcher
  375. diff --git a/src/Microsoft.AspNetCore.Dispatcher/RoutePrecedence.cs b/src/Microsoft.AspNetCore.Dispatcher/RoutePrecedence.cs
  376. new file mode 100644
  377. index 00000000000..231b2f0945e
  378. --- /dev/null
  379. +++ b/src/Microsoft.AspNetCore.Dispatcher/RoutePrecedence.cs
  380. @@ -0,0 +1,77 @@
  381. +// Copyright (c) .NET Foundation. All rights reserved.
  382. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  383. +
  384. +using System;
  385. +using System.Diagnostics;
  386. +using System.Linq;
  387. +using Microsoft.AspNetCore.Dispatcher.Patterns;
  388. +
  389. +namespace Microsoft.AspNetCore.Dispatcher
  390. +{
  391. + /// <summary>
  392. + /// Computes precedence for a route pattern.
  393. + /// </summary>
  394. + public static class RoutePrecedence
  395. + {
  396. + // Compute the precedence for matching a provided url
  397. + // e.g.: /api/template == 1.1
  398. + // /api/template/{id} == 1.13
  399. + // /api/{id:int} == 1.2
  400. + // /api/template/{id:int} == 1.12
  401. + public static decimal ComputeInbound(RoutePattern routePattern)
  402. + {
  403. + // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1,
  404. + // and 4 results in a combined precedence of 2.14 (decimal).
  405. + var precedence = 0m;
  406. +
  407. + for (var i = 0; i < routePattern.PathSegments.Count; i++)
  408. + {
  409. + var segment = routePattern.PathSegments[i];
  410. +
  411. + var digit = ComputeInboundPrecedenceDigit(segment);
  412. + Debug.Assert(digit >= 0 && digit < 10);
  413. +
  414. + precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i));
  415. + }
  416. +
  417. + return precedence;
  418. + }
  419. +
  420. + // Segments have the following order:
  421. + // 1 - Literal segments
  422. + // 2 - Constrained parameter segments / Multi-part segments
  423. + // 3 - Unconstrained parameter segments
  424. + // 4 - Constrained wildcard parameter segments
  425. + // 5 - Unconstrained wildcard parameter segments
  426. + private static int ComputeInboundPrecedenceDigit(RoutePatternPathSegment segment)
  427. + {
  428. + if (segment.Parts.Count > 1)
  429. + {
  430. + // Multi-part segments should appear after literal segments and along with parameter segments
  431. + return 2;
  432. + }
  433. +
  434. + var part = segment.Parts[0];
  435. + // Literal segments always go first
  436. + if (part.IsLiteral)
  437. + {
  438. + return 1;
  439. + }
  440. + else
  441. + {
  442. + Debug.Assert(part.IsParameter);
  443. + var parameter = (RoutePatternParameter)part;
  444. + var digit = parameter.IsCatchAll ? 5 : 3;
  445. +
  446. + // If there is a dispatcher value constraint for the parameter, reduce order by 1
  447. + // Constrained parameters end up with order 2, Constrained catch alls end up with order 4
  448. + if (parameter.Constraints != null && parameter.Constraints.Any())
  449. + {
  450. + digit--;
  451. + }
  452. +
  453. + return digit;
  454. + }
  455. + }
  456. + }
  457. +}
  458. \ No newline at end of file
  459. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Tree/InboundMatch.cs b/src/Microsoft.AspNetCore.Dispatcher/Tree/InboundMatch.cs
  460. new file mode 100644
  461. index 00000000000..018fb02e9e6
  462. --- /dev/null
  463. +++ b/src/Microsoft.AspNetCore.Dispatcher/Tree/InboundMatch.cs
  464. @@ -0,0 +1,29 @@
  465. +// Copyright (c) .NET Foundation. All rights reserved.
  466. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  467. +
  468. +using System.Diagnostics;
  469. +
  470. +namespace Microsoft.AspNetCore.Dispatcher
  471. +{
  472. + /// <summary>
  473. + /// A candidate endpoint to match incoming URLs in a <c>TreeMatcher</c>.
  474. + /// </summary>
  475. + [DebuggerDisplay("{DebuggerToString(),nq}")]
  476. + public class InboundMatch
  477. + {
  478. + /// <summary>
  479. + /// Gets or sets the <see cref="InboundRouteEntry"/>.
  480. + /// </summary>
  481. + public InboundRouteEntry Entry { get; set; }
  482. +
  483. + /// <summary>
  484. + /// Gets or sets the <see cref="RoutePatternMatcher"/>.
  485. + /// </summary>
  486. + public RoutePatternMatcher RoutePatternMatcher { get; set; }
  487. +
  488. + private string DebuggerToString()
  489. + {
  490. + return RoutePatternMatcher?.RoutePattern?.RawText;
  491. + }
  492. + }
  493. +}
  494. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Tree/InboundRouteEntry.cs b/src/Microsoft.AspNetCore.Dispatcher/Tree/InboundRouteEntry.cs
  495. new file mode 100644
  496. index 00000000000..e54d626d800
  497. --- /dev/null
  498. +++ b/src/Microsoft.AspNetCore.Dispatcher/Tree/InboundRouteEntry.cs
  499. @@ -0,0 +1,51 @@
  500. +// Copyright (c) .NET Foundation. All rights reserved.
  501. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  502. +
  503. +using System.Collections.Generic;
  504. +using Microsoft.AspNetCore.Dispatcher.Patterns;
  505. +
  506. +namespace Microsoft.AspNetCore.Dispatcher
  507. +{
  508. + /// <summary>
  509. + /// Used to build a <see cref="TreeMatcher"/>. Represents a route pattern that will be used to match incoming
  510. + /// request URLs.
  511. + /// </summary>
  512. + public class InboundRouteEntry
  513. + {
  514. + /// <summary>
  515. + /// Gets or sets the dispatcher value constraints.
  516. + /// </summary>
  517. + public IDictionary<string, IDispatcherValueConstraint> Constraints { get; set; }
  518. +
  519. + /// <summary>
  520. + /// Gets or sets the dispatcher value defaults.
  521. + /// </summary>
  522. + public DispatcherValueCollection Defaults { get; set; }
  523. +
  524. + /// <summary>
  525. + /// Gets or sets the order of the entry.
  526. + /// </summary>
  527. + /// <remarks>
  528. + /// Entries are ordered first by <see cref="Order"/> (ascending) then by <see cref="Precedence"/> (descending).
  529. + /// </remarks>
  530. + public int Order { get; set; }
  531. +
  532. + /// <summary>
  533. + /// Gets or sets the precedence of the entry.
  534. + /// </summary>
  535. + /// <remarks>
  536. + /// Entries are ordered first by <see cref="Order"/> (ascending) then by <see cref="Precedence"/> (descending).
  537. + /// </remarks>
  538. + public decimal Precedence { get; set; }
  539. +
  540. + /// <summary>
  541. + /// Gets or sets the <see cref="RoutePattern"/>.
  542. + /// </summary>
  543. + public RoutePattern RoutePattern { get; set; }
  544. +
  545. + /// <summary>
  546. + /// Gets or sets an arbitrary value associated with the entry.
  547. + /// </summary>
  548. + public object Tag { get; set; }
  549. + }
  550. +}
  551. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs b/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs
  552. new file mode 100644
  553. index 00000000000..c53c2aa6faa
  554. --- /dev/null
  555. +++ b/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcher.cs
  556. @@ -0,0 +1,543 @@
  557. +// Copyright (c) .NET Foundation. All rights reserved.
  558. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  559. +
  560. +using System;
  561. +using System.Collections;
  562. +using System.Collections.Generic;
  563. +using System.Diagnostics;
  564. +using System.Linq;
  565. +using System.Threading;
  566. +using System.Threading.Tasks;
  567. +using Microsoft.AspNetCore.Dispatcher.Internal;
  568. +using Microsoft.AspNetCore.Dispatcher.Patterns;
  569. +using Microsoft.AspNetCore.Http;
  570. +using Microsoft.Extensions.DependencyInjection;
  571. +using Microsoft.Extensions.Internal;
  572. +
  573. +namespace Microsoft.AspNetCore.Dispatcher
  574. +{
  575. + public class TreeMatcher : MatcherBase
  576. + {
  577. + private bool _dataInitialized;
  578. + private object _lock;
  579. + private Cache _cache;
  580. + private IConstraintFactory _constraintFactory;
  581. +
  582. + private readonly Func<Cache> _initializer;
  583. +
  584. + public TreeMatcher()
  585. + {
  586. + _lock = new object();
  587. + _initializer = CreateCache;
  588. + }
  589. +
  590. + public int Version { get; private set; }
  591. +
  592. + public override async Task MatchAsync(MatcherContext context)
  593. + {
  594. + if (context == null)
  595. + {
  596. + throw new ArgumentNullException(nameof(context));
  597. + }
  598. +
  599. + EnsureServicesInitialized(context);
  600. +
  601. + var cache = LazyInitializer.EnsureInitialized(ref _cache, ref _dataInitialized, ref _lock, _initializer);
  602. +
  603. + var values = new DispatcherValueCollection();
  604. + context.Values = values;
  605. +
  606. + for (var i = 0; i < cache.Trees.Length; i++)
  607. + {
  608. + var tree = cache.Trees[i];
  609. + var tokenizer = new PathTokenizer(context.HttpContext.Request.Path);
  610. +
  611. + var treenumerator = new Treenumerator(tree.Root, tokenizer);
  612. +
  613. + while (treenumerator.MoveNext())
  614. + {
  615. + var node = treenumerator.Current;
  616. + foreach (var item in node.Matches)
  617. + {
  618. + var entry = item.Entry;
  619. + var matcher = item.RoutePatternMatcher;
  620. +
  621. + values.Clear();
  622. + if (!matcher.TryMatch(context.HttpContext.Request.Path, values))
  623. + {
  624. + continue;
  625. + }
  626. +
  627. + Logger.MatchedRoute(entry.RoutePattern.RawText);
  628. +
  629. + if (!MatchConstraints(context.HttpContext, values, entry.Constraints))
  630. + {
  631. + continue;
  632. + }
  633. +
  634. + await SelectEndpointAsync(context, (Endpoint[])entry.Tag);
  635. + if (context.ShortCircuit != null)
  636. + {
  637. + Logger.RequestShortCircuited(context);
  638. + return;
  639. + }
  640. +
  641. + if (context.Endpoint != null)
  642. + {
  643. + if (context.Endpoint is IRoutePatternEndpoint templateEndpoint)
  644. + {
  645. + foreach (var kvp in templateEndpoint.Values)
  646. + {
  647. + if (!context.Values.ContainsKey(kvp.Key))
  648. + {
  649. + context.Values[kvp.Key] = kvp.Value;
  650. + }
  651. + }
  652. + }
  653. +
  654. + return;
  655. + }
  656. + }
  657. + }
  658. + }
  659. + }
  660. +
  661. + private bool MatchConstraints(HttpContext httpContext, DispatcherValueCollection values, IDictionary<string, IDispatcherValueConstraint> constraints)
  662. + {
  663. + if (constraints != null)
  664. + {
  665. + foreach (var kvp in constraints)
  666. + {
  667. + var constraint = kvp.Value;
  668. + var constraintContext = new DispatcherValueConstraintContext(httpContext, values, ConstraintPurpose.IncomingRequest)
  669. + {
  670. + Key = kvp.Key
  671. + };
  672. +
  673. + if (!constraint.Match(constraintContext))
  674. + {
  675. + values.TryGetValue(kvp.Key, out var value);
  676. +
  677. + Logger.RouteValueDoesNotMatchConstraint(value, kvp.Key, kvp.Value);
  678. + return false;
  679. + }
  680. + }
  681. + }
  682. +
  683. + return true;
  684. + }
  685. +
  686. + internal Cache CreateCache()
  687. + {
  688. + var endpoints = GetEndpoints();
  689. +
  690. + var groups = new Dictionary<Key, List<Endpoint>>();
  691. +
  692. + for (var i = 0; i < endpoints.Count; i++)
  693. + {
  694. + var endpoint = endpoints[i];
  695. +
  696. + var templateEndpoint = endpoint as IRoutePatternEndpoint;
  697. + if (templateEndpoint == null)
  698. + {
  699. + continue;
  700. + }
  701. +
  702. + var order = endpoint.Metadata?.GetMetadata<IEndpointOrderMetadata>()?.Order ?? 0;
  703. + if (!groups.TryGetValue(new Key(order, templateEndpoint.Pattern), out var group))
  704. + {
  705. + group = new List<Endpoint>();
  706. + groups.Add(new Key(order, templateEndpoint.Pattern), group);
  707. + }
  708. +
  709. + group.Add(endpoint);
  710. + }
  711. +
  712. + var entries = new List<InboundRouteEntry>();
  713. + foreach (var group in groups)
  714. + {
  715. + var routePattern = RoutePattern.Parse(group.Key.RoutePattern);
  716. + var entryExists = entries.Any(item => item.RoutePattern.RawText == routePattern.RawText);
  717. + if (!entryExists)
  718. + {
  719. + entries.Add(MapInbound(routePattern, group.Value.ToArray(), group.Key.Order));
  720. + }
  721. + }
  722. +
  723. + var trees = new List<UrlMatchingTree>();
  724. + for (var i = 0; i < entries.Count; i++)
  725. + {
  726. + var entry = entries[i];
  727. +
  728. + while (trees.Count <= entry.Order)
  729. + {
  730. + trees.Add(new UrlMatchingTree(entry.Order));
  731. + }
  732. +
  733. + var tree = trees[entry.Order];
  734. +
  735. + AddEntryToTree(tree, entry);
  736. + }
  737. +
  738. + return new Cache(trees.ToArray());
  739. + }
  740. +
  741. + private InboundRouteEntry MapInbound(
  742. + RoutePattern routePattern,
  743. + object tag,
  744. + int order)
  745. + {
  746. + if (routePattern == null)
  747. + {
  748. + throw new ArgumentNullException(nameof(routePattern));
  749. + }
  750. +
  751. + var entry = new InboundRouteEntry()
  752. + {
  753. + Precedence = RoutePrecedence.ComputeInbound(routePattern),
  754. + RoutePattern = routePattern,
  755. + Order = order,
  756. + Tag = tag
  757. + };
  758. +
  759. + var constraintBuilder = new DispatcherValueConstraintBuilder(_constraintFactory, routePattern.RawText);
  760. + foreach (var parameter in routePattern.Parameters)
  761. + {
  762. + if (parameter.Constraints != null)
  763. + {
  764. + if (parameter.IsOptional)
  765. + {
  766. + constraintBuilder.SetOptional(parameter.Name);
  767. + }
  768. +
  769. + foreach (var constraint in parameter.Constraints)
  770. + {
  771. + constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.RawText);
  772. + }
  773. + }
  774. + }
  775. +
  776. + entry.Constraints = constraintBuilder.Build();
  777. +
  778. + entry.Defaults = new DispatcherValueCollection();
  779. + foreach (var parameter in entry.RoutePattern.Parameters)
  780. + {
  781. + if (parameter.DefaultValue != null)
  782. + {
  783. + entry.Defaults.Add(parameter.Name, parameter.DefaultValue);
  784. + }
  785. + }
  786. + return entry;
  787. + }
  788. +
  789. + internal static void AddEntryToTree(UrlMatchingTree tree, InboundRouteEntry entry)
  790. + {
  791. + // The url matching tree represents all the routes asociated with a given
  792. + // order. Each node in the tree represents all the different categories
  793. + // a segment can have for which there is a defined inbound route entry.
  794. + // Each node contains a set of Matches that indicate all the routes for which
  795. + // a URL is a potential match. This list contains the routes with the same
  796. + // number of segments and the routes with the same number of segments plus an
  797. + // additional catch all parameter (as it can be empty).
  798. + // For example, for a set of routes like:
  799. + // 'Customer/Index/{id}'
  800. + // '{Controller}/{Action}/{*parameters}'
  801. + //
  802. + // The route tree will look like:
  803. + // Root ->
  804. + // Literals: Customer ->
  805. + // Literals: Index ->
  806. + // Parameters: {id}
  807. + // Matches: 'Customer/Index/{id}'
  808. + // Parameters: {Controller} ->
  809. + // Parameters: {Action} ->
  810. + // Matches: '{Controller}/{Action}/{*parameters}'
  811. + // CatchAlls: {*parameters}
  812. + // Matches: '{Controller}/{Action}/{*parameters}'
  813. + //
  814. + // When the tree router tries to match a route, it iterates the list of url matching trees
  815. + // in ascending order. For each tree it traverses each node starting from the root in the
  816. + // following order: Literals, constrained parameters, parameters, constrained catch all routes, catch alls.
  817. + // When it gets to a node of the same length as the route its trying to match, it simply looks at the list of
  818. + // candidates (which is in precence order) and tries to match the url against it.
  819. +
  820. + var current = tree.Root;
  821. + var matcher = new RoutePatternMatcher(entry.RoutePattern, entry.Defaults);
  822. +
  823. + for (var i = 0; i < entry.RoutePattern.PathSegments.Count; i++)
  824. + {
  825. + var segment = entry.RoutePattern.PathSegments[i];
  826. + if (!segment.IsSimple)
  827. + {
  828. + // Treat complex segments as a constrained parameter
  829. + if (current.ConstrainedParameters == null)
  830. + {
  831. + current.ConstrainedParameters = new UrlMatchingNode(depth: i + 1);
  832. + }
  833. +
  834. + current = current.ConstrainedParameters;
  835. + continue;
  836. + }
  837. +
  838. + Debug.Assert(segment.Parts.Count == 1);
  839. + var part = segment.Parts[0];
  840. + if (part.IsLiteral)
  841. + {
  842. + var literal = (RoutePatternLiteral)part;
  843. + if (!current.Literals.TryGetValue(literal.Content, out var next))
  844. + {
  845. + next = new UrlMatchingNode(depth: i + 1);
  846. + current.Literals.Add(literal.Content, next);
  847. + }
  848. +
  849. + current = next;
  850. + continue;
  851. + }
  852. +
  853. + // We accept templates that have intermediate optional values, but we ignore
  854. + // those values for route matching. For that reason, we need to add the entry
  855. + // to the list of matches, only if the remaining segments are optional. For example:
  856. + // /{controller}/{action=Index}/{id} will be equivalent to /{controller}/{action}/{id}
  857. + // for the purposes of route matching.
  858. + if (part.IsParameter &&
  859. + RemainingSegmentsAreOptional(entry.RoutePattern.PathSegments, i))
  860. + {
  861. + current.Matches.Add(new InboundMatch() { Entry = entry, RoutePatternMatcher = matcher });
  862. + }
  863. +
  864. + var parameter = part as RoutePatternParameter;
  865. + if (parameter != null && parameter.Constraints.Any() && !parameter.IsCatchAll)
  866. + {
  867. + if (current.ConstrainedParameters == null)
  868. + {
  869. + current.ConstrainedParameters = new UrlMatchingNode(depth: i + 1);
  870. + }
  871. +
  872. + current = current.ConstrainedParameters;
  873. + continue;
  874. + }
  875. +
  876. + if (parameter != null && !parameter.IsCatchAll)
  877. + {
  878. + if (current.Parameters == null)
  879. + {
  880. + current.Parameters = new UrlMatchingNode(depth: i + 1);
  881. + }
  882. +
  883. + current = current.Parameters;
  884. + continue;
  885. + }
  886. +
  887. + if (parameter != null && parameter.Constraints.Any() && parameter.IsCatchAll)
  888. + {
  889. + if (current.ConstrainedCatchAlls == null)
  890. + {
  891. + current.ConstrainedCatchAlls = new UrlMatchingNode(depth: i + 1) { IsCatchAll = true };
  892. + }
  893. +
  894. + current = current.ConstrainedCatchAlls;
  895. + continue;
  896. + }
  897. +
  898. + if (parameter != null && parameter.IsCatchAll)
  899. + {
  900. + if (current.CatchAlls == null)
  901. + {
  902. + current.CatchAlls = new UrlMatchingNode(depth: i + 1) { IsCatchAll = true };
  903. + }
  904. +
  905. + current = current.CatchAlls;
  906. + continue;
  907. + }
  908. +
  909. + Debug.Fail("We shouldn't get here.");
  910. + }
  911. +
  912. + current.Matches.Add(new InboundMatch() { Entry = entry, RoutePatternMatcher = matcher });
  913. + current.Matches.Sort((x, y) =>
  914. + {
  915. + var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence);
  916. + return result == 0 ? x.Entry.RoutePattern.RawText.CompareTo(y.Entry.RoutePattern.RawText) : result;
  917. + });
  918. + }
  919. +
  920. + private static bool RemainingSegmentsAreOptional(IReadOnlyList<RoutePatternPathSegment> segments, int currentParameterIndex)
  921. + {
  922. + for (var i = currentParameterIndex; i < segments.Count; i++)
  923. + {
  924. + if (!segments[i].IsSimple)
  925. + {
  926. + // /{complex}-{segment}
  927. + return false;
  928. + }
  929. +
  930. + var part = segments[i].Parts[0];
  931. + if (!part.IsParameter)
  932. + {
  933. + // /literal
  934. + return false;
  935. + }
  936. +
  937. + var parameter = (RoutePatternParameter)part;
  938. + var isOptionlCatchAllOrHasDefaultValue = parameter.IsOptional ||
  939. + parameter.IsCatchAll ||
  940. + parameter.DefaultValue != null;
  941. +
  942. + if (!isOptionlCatchAllOrHasDefaultValue)
  943. + {
  944. + // /{parameter}
  945. + return false;
  946. + }
  947. + }
  948. +
  949. + return true;
  950. + }
  951. +
  952. + private struct Key : IEquatable<Key>
  953. + {
  954. + public readonly int Order;
  955. + public readonly string RoutePattern;
  956. +
  957. + public Key(int order, string routePattern)
  958. + {
  959. + Order = order;
  960. + RoutePattern = routePattern;
  961. + }
  962. +
  963. + public bool Equals(Key other)
  964. + {
  965. + return Order == other.Order && string.Equals(RoutePattern, other.RoutePattern, StringComparison.OrdinalIgnoreCase);
  966. + }
  967. +
  968. + public override bool Equals(object obj)
  969. + {
  970. + return obj is Key ? Equals((Key)obj) : false;
  971. + }
  972. +
  973. + public override int GetHashCode()
  974. + {
  975. + var hash = new HashCodeCombiner();
  976. + hash.Add(Order);
  977. + hash.Add(RoutePattern, StringComparer.OrdinalIgnoreCase);
  978. + return hash;
  979. + }
  980. + }
  981. +
  982. + internal class Cache
  983. + {
  984. + public readonly UrlMatchingTree[] Trees;
  985. +
  986. + public Cache(UrlMatchingTree[] trees)
  987. + {
  988. + Trees = trees;
  989. + }
  990. + }
  991. +
  992. + private struct Treenumerator : IEnumerator<UrlMatchingNode>
  993. + {
  994. + private readonly Stack<UrlMatchingNode> _stack;
  995. + private readonly PathTokenizer _tokenizer;
  996. +
  997. + public Treenumerator(UrlMatchingNode root, PathTokenizer tokenizer)
  998. + {
  999. + _stack = new Stack<UrlMatchingNode>();
  1000. + _tokenizer = tokenizer;
  1001. + Current = null;
  1002. +
  1003. + _stack.Push(root);
  1004. + }
  1005. +
  1006. + public UrlMatchingNode Current { get; private set; }
  1007. +
  1008. + object IEnumerator.Current => Current;
  1009. +
  1010. + public void Dispose()
  1011. + {
  1012. + }
  1013. +
  1014. + public bool MoveNext()
  1015. + {
  1016. + if (_stack == null)
  1017. + {
  1018. + return false;
  1019. + }
  1020. +
  1021. + while (_stack.Count > 0)
  1022. + {
  1023. + var next = _stack.Pop();
  1024. +
  1025. + // In case of wild card segment, the request path segment length can be greater
  1026. + // Example:
  1027. + // Template: a/{*path}
  1028. + // Request Url: a/b/c/d
  1029. + if (next.IsCatchAll && next.Matches.Count > 0)
  1030. + {
  1031. + Current = next;
  1032. + return true;
  1033. + }
  1034. +
  1035. + // Next template has the same length as the url we are trying to match
  1036. + // The only possible matching segments are either our current matches or
  1037. + // any catch-all segment after this segment in which the catch all is empty.
  1038. + else if (next.Depth >= _tokenizer.Count)
  1039. + {
  1040. + if (next.Matches.Count > 0)
  1041. + {
  1042. + Current = next;
  1043. + return true;
  1044. + }
  1045. + else
  1046. + {
  1047. + // We can stop looking as any other child node from this node will be
  1048. + // either a literal, a constrained parameter or a parameter.
  1049. + // (Catch alls and constrained catch alls will show up as candidate matches).
  1050. + continue;
  1051. + }
  1052. + }
  1053. +
  1054. + if (next.CatchAlls != null)
  1055. + {
  1056. + _stack.Push(next.CatchAlls);
  1057. + }
  1058. +
  1059. + if (next.ConstrainedCatchAlls != null)
  1060. + {
  1061. + _stack.Push(next.ConstrainedCatchAlls);
  1062. + }
  1063. +
  1064. + if (next.Parameters != null)
  1065. + {
  1066. + _stack.Push(next.Parameters);
  1067. + }
  1068. +
  1069. + if (next.ConstrainedParameters != null)
  1070. + {
  1071. + _stack.Push(next.ConstrainedParameters);
  1072. + }
  1073. +
  1074. + if (next.Literals.Count > 0)
  1075. + {
  1076. + Debug.Assert(next.Depth < _tokenizer.Count);
  1077. + if (next.Literals.TryGetValue(_tokenizer[next.Depth].Value, out var node))
  1078. + {
  1079. + _stack.Push(node);
  1080. + }
  1081. + }
  1082. + }
  1083. +
  1084. + return false;
  1085. + }
  1086. +
  1087. + public void Reset()
  1088. + {
  1089. + _stack.Clear();
  1090. + Current = null;
  1091. + }
  1092. + }
  1093. +
  1094. + protected override void InitializeServices(IServiceProvider services)
  1095. + {
  1096. + _constraintFactory = services.GetRequiredService<IConstraintFactory>();
  1097. + }
  1098. + }
  1099. +}
  1100. diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcherFactory.cs b/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcherFactory.cs
  1101. similarity index 90%
  1102. rename from src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcherFactory.cs
  1103. rename to src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcherFactory.cs
  1104. index 694ebe6b67f..badcf48078e 100644
  1105. --- a/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcherFactory.cs
  1106. +++ b/src/Microsoft.AspNetCore.Dispatcher/Tree/TreeMatcherFactory.cs
  1107. @@ -3,9 +3,8 @@
  1108. using System;
  1109. using System.Collections.Generic;
  1110. -using Microsoft.AspNetCore.Dispatcher;
  1111. -namespace Microsoft.AspNetCore.Routing.Dispatcher
  1112. +namespace Microsoft.AspNetCore.Dispatcher
  1113. {
  1114. public class TreeMatcherFactory : IDefaultMatcherFactory
  1115. {
  1116. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Tree/UrlMatchingNode.cs b/src/Microsoft.AspNetCore.Dispatcher/Tree/UrlMatchingNode.cs
  1117. new file mode 100644
  1118. index 00000000000..84150fde776
  1119. --- /dev/null
  1120. +++ b/src/Microsoft.AspNetCore.Dispatcher/Tree/UrlMatchingNode.cs
  1121. @@ -0,0 +1,81 @@
  1122. +// Copyright (c) .NET Foundation. All rights reserved.
  1123. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  1124. +
  1125. +using System;
  1126. +using System.Collections.Generic;
  1127. +using System.Diagnostics;
  1128. +using System.Linq;
  1129. +
  1130. +namespace Microsoft.AspNetCore.Dispatcher
  1131. +{
  1132. + /// <summary>
  1133. + /// A node in a <see cref="UrlMatchingTree"/>.
  1134. + /// </summary>
  1135. + [DebuggerDisplay("{DebuggerToString(),nq}")]
  1136. + public class UrlMatchingNode
  1137. + {
  1138. + /// <summary>
  1139. + /// Initializes a new instance of <see cref="UrlMatchingNode"/>.
  1140. + /// </summary>
  1141. + /// <param name="depth">The length of the path to this node in the <see cref="UrlMatchingTree"/>.</param>
  1142. + public UrlMatchingNode(int depth)
  1143. + {
  1144. + Depth = depth;
  1145. +
  1146. + Matches = new List<InboundMatch>();
  1147. + Literals = new Dictionary<string, UrlMatchingNode>(StringComparer.OrdinalIgnoreCase);
  1148. + }
  1149. +
  1150. + /// <summary>
  1151. + /// Gets the length of the path to this node in the <see cref="UrlMatchingTree"/>.
  1152. + /// </summary>
  1153. + public int Depth { get; }
  1154. +
  1155. + /// <summary>
  1156. + /// Gets or sets a value indicating whether this node represents a catch all segment.
  1157. + /// </summary>
  1158. + public bool IsCatchAll { get; set; }
  1159. +
  1160. + /// <summary>
  1161. + /// Gets the list of matching route entries associated with this node.
  1162. + /// </summary>
  1163. + /// <remarks>
  1164. + /// These entries are sorted by precedence then template.
  1165. + /// </remarks>
  1166. + public List<InboundMatch> Matches { get; }
  1167. +
  1168. + /// <summary>
  1169. + /// Gets the literal segments following this segment.
  1170. + /// </summary>
  1171. + public Dictionary<string, UrlMatchingNode> Literals { get; }
  1172. +
  1173. + /// <summary>
  1174. + /// Gets or sets the <see cref="UrlMatchingNode"/> representing
  1175. + /// parameter segments with constraints following this segment in the <see cref="TreeMatcher"/>.
  1176. + /// </summary>
  1177. + public UrlMatchingNode ConstrainedParameters { get; set; }
  1178. +
  1179. + /// <summary>
  1180. + /// Gets or sets the <see cref="UrlMatchingNode"/> representing
  1181. + /// parameter segments following this segment in the <see cref="TreeMatcher"/>.
  1182. + /// </summary>
  1183. + public UrlMatchingNode Parameters { get; set; }
  1184. +
  1185. + /// <summary>
  1186. + /// Gets or sets the <see cref="UrlMatchingNode"/> representing
  1187. + /// catch all parameter segments with constraints following this segment in the <see cref="TreeMatcher"/>.
  1188. + /// </summary>
  1189. + public UrlMatchingNode ConstrainedCatchAlls { get; set; }
  1190. +
  1191. + /// <summary>
  1192. + /// Gets or sets the <see cref="UrlMatchingNode"/> representing
  1193. + /// catch all parameter segments following this segment in the <see cref="TreeMatcher"/>.
  1194. + /// </summary>
  1195. + public UrlMatchingNode CatchAlls { get; set; }
  1196. +
  1197. + private string DebuggerToString()
  1198. + {
  1199. + return $"Length: {Depth}, Matches: {string.Join(" | ", Matches?.Select(m => $"({m.RoutePatternMatcher.RoutePattern.RawText})"))}";
  1200. + }
  1201. + }
  1202. +}
  1203. diff --git a/src/Microsoft.AspNetCore.Dispatcher/Tree/UrlMatchingTree.cs b/src/Microsoft.AspNetCore.Dispatcher/Tree/UrlMatchingTree.cs
  1204. new file mode 100644
  1205. index 00000000000..5685b2aa719
  1206. --- /dev/null
  1207. +++ b/src/Microsoft.AspNetCore.Dispatcher/Tree/UrlMatchingTree.cs
  1208. @@ -0,0 +1,30 @@
  1209. +// Copyright (c) .NET Foundation. All rights reserved.
  1210. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  1211. +
  1212. +namespace Microsoft.AspNetCore.Dispatcher
  1213. +{
  1214. + /// <summary>
  1215. + /// A tree part of a <see cref="TreeMatcher"/>.
  1216. + /// </summary>
  1217. + public class UrlMatchingTree
  1218. + {
  1219. + /// <summary>
  1220. + /// Initializes a new instance of <see cref="UrlMatchingTree"/>.
  1221. + /// </summary>
  1222. + /// <param name="order">The order associated with endpoints in this <see cref="UrlMatchingTree"/>.</param>
  1223. + public UrlMatchingTree(int order)
  1224. + {
  1225. + Order = order;
  1226. + }
  1227. +
  1228. + /// <summary>
  1229. + /// Gets the order of the endpoints associated with this <see cref="UrlMatchingTree"/>.
  1230. + /// </summary>
  1231. + public int Order { get; }
  1232. +
  1233. + /// <summary>
  1234. + /// Gets the root of the <see cref="UrlMatchingTree"/>.
  1235. + /// </summary>
  1236. + public UrlMatchingNode Root { get; } = new UrlMatchingNode(depth: 0);
  1237. + }
  1238. +}
  1239. diff --git a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs
  1240. index 710ceba83c6..5c757965ad0 100644
  1241. --- a/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs
  1242. +++ b/src/Microsoft.AspNetCore.Routing/DependencyInjection/RoutingServiceCollectionExtensions.cs
  1243. @@ -5,11 +5,9 @@ using System;
  1244. using System.Text.Encodings.Web;
  1245. using Microsoft.AspNetCore.Dispatcher;
  1246. using Microsoft.AspNetCore.Routing;
  1247. -using Microsoft.AspNetCore.Routing.Dispatcher;
  1248. using Microsoft.AspNetCore.Routing.Internal;
  1249. using Microsoft.AspNetCore.Routing.Tree;
  1250. using Microsoft.Extensions.DependencyInjection.Extensions;
  1251. -using Microsoft.Extensions.ObjectPool;
  1252. namespace Microsoft.Extensions.DependencyInjection
  1253. {
  1254. diff --git a/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcher.cs b/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcher.cs
  1255. deleted file mode 100644
  1256. index 443aff97d37..00000000000
  1257. --- a/src/Microsoft.AspNetCore.Routing/Dispatcher/TreeMatcher.cs
  1258. +++ /dev/null
  1259. @@ -1,331 +0,0 @@
  1260. -// Copyright (c) .NET Foundation. All rights reserved.
  1261. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  1262. -
  1263. -using System;
  1264. -using System.Collections;
  1265. -using System.Collections.Generic;
  1266. -using System.Diagnostics;
  1267. -using System.Threading;
  1268. -using System.Threading.Tasks;
  1269. -using Microsoft.AspNetCore.Dispatcher;
  1270. -using Microsoft.AspNetCore.Dispatcher.Internal;
  1271. -using Microsoft.AspNetCore.Http;
  1272. -using Microsoft.AspNetCore.Routing.Logging;
  1273. -using Microsoft.AspNetCore.Routing.Template;
  1274. -using Microsoft.AspNetCore.Routing.Tree;
  1275. -using Microsoft.Extensions.Internal;
  1276. -
  1277. -namespace Microsoft.AspNetCore.Routing.Dispatcher
  1278. -{
  1279. - public class TreeMatcher : MatcherBase
  1280. - {
  1281. - private bool _dataInitialized;
  1282. - private object _lock;
  1283. - private Cache _cache;
  1284. -
  1285. - private readonly Func<Cache> _initializer;
  1286. -
  1287. - public TreeMatcher()
  1288. - {
  1289. - _lock = new object();
  1290. - _initializer = CreateCache;
  1291. - }
  1292. -
  1293. - public override async Task MatchAsync(MatcherContext context)
  1294. - {
  1295. - if (context == null)
  1296. - {
  1297. - throw new ArgumentNullException(nameof(context));
  1298. - }
  1299. -
  1300. - EnsureServicesInitialized(context);
  1301. -
  1302. - var cache = LazyInitializer.EnsureInitialized(ref _cache, ref _dataInitialized, ref _lock, _initializer);
  1303. -
  1304. - var values = new RouteValueDictionary();
  1305. - context.Values = values;
  1306. -
  1307. - for (var i = 0; i < cache.Trees.Length; i++)
  1308. - {
  1309. - var tree = cache.Trees[i];
  1310. - var tokenizer = new PathTokenizer(context.HttpContext.Request.Path);
  1311. -
  1312. - var treenumerator = new Treenumerator(tree.Root, tokenizer);
  1313. -
  1314. - while (treenumerator.MoveNext())
  1315. - {
  1316. - var node = treenumerator.Current;
  1317. - foreach (var item in node.Matches)
  1318. - {
  1319. - var entry = item.Entry;
  1320. - var matcher = item.TemplateMatcher;
  1321. -
  1322. - values.Clear();
  1323. - if (!matcher.TryMatch(context.HttpContext.Request.Path, values))
  1324. - {
  1325. - continue;
  1326. - }
  1327. -
  1328. - Logger.MatchedRoute(entry.RouteName, entry.RouteTemplate.TemplateText);
  1329. -
  1330. - if (!MatchConstraints(context.HttpContext, values, entry.Constraints))
  1331. - {
  1332. - continue;
  1333. - }
  1334. -
  1335. - await SelectEndpointAsync(context, (Endpoint[])entry.Tag);
  1336. - if (context.ShortCircuit != null)
  1337. - {
  1338. - Logger.RequestShortCircuited(context);
  1339. - return;
  1340. - }
  1341. -
  1342. - if (context.Endpoint != null)
  1343. - {
  1344. - if (context.Endpoint is IRoutePatternEndpoint templateEndpoint)
  1345. - {
  1346. - foreach (var kvp in templateEndpoint.Values)
  1347. - {
  1348. - context.Values[kvp.Key] = kvp.Value;
  1349. - }
  1350. - }
  1351. -
  1352. - return;
  1353. - }
  1354. - }
  1355. - }
  1356. - }
  1357. - }
  1358. -
  1359. - private bool MatchConstraints(HttpContext httpContext, RouteValueDictionary values, IDictionary<string, IRouteConstraint> constraints)
  1360. - {
  1361. - if (constraints != null)
  1362. - {
  1363. - foreach (var kvp in constraints)
  1364. - {
  1365. - var constraint = kvp.Value;
  1366. - if (!constraint.Match(httpContext, null, kvp.Key, values, RouteDirection.IncomingRequest))
  1367. - {
  1368. - object value;
  1369. - values.TryGetValue(kvp.Key, out value);
  1370. -
  1371. - Logger.RouteValueDoesNotMatchConstraint(value, kvp.Key, kvp.Value);
  1372. - return false;
  1373. - }
  1374. - }
  1375. - }
  1376. -
  1377. - return true;
  1378. - }
  1379. -
  1380. - private Cache CreateCache()
  1381. - {
  1382. - var endpoints = GetEndpoints();
  1383. -
  1384. - var groups = new Dictionary<Key, List<Endpoint>>();
  1385. -
  1386. - for (var i = 0; i < endpoints.Count; i++)
  1387. - {
  1388. - var endpoint = endpoints[i];
  1389. -
  1390. - var templateEndpoint = endpoint as IRoutePatternEndpoint;
  1391. - if (templateEndpoint == null)
  1392. - {
  1393. - continue;
  1394. - }
  1395. -
  1396. - if (!groups.TryGetValue(new Key(0, templateEndpoint.Pattern), out var group))
  1397. - {
  1398. - group = new List<Endpoint>();
  1399. - groups.Add(new Key(0, templateEndpoint.Pattern), group);
  1400. - }
  1401. -
  1402. - group.Add(endpoint);
  1403. - }
  1404. -
  1405. - var entries = new List<InboundRouteEntry>();
  1406. - foreach (var group in groups)
  1407. - {
  1408. - var template = Template.TemplateParser.Parse(group.Key.RouteTemplate);
  1409. -
  1410. - var defaults = new RouteValueDictionary();
  1411. - for (var i = 0; i < template.Parameters.Count; i++)
  1412. - {
  1413. - var parameter = template.Parameters[i];
  1414. - if (parameter.DefaultValue != null)
  1415. - {
  1416. - defaults.Add(parameter.Name, parameter.DefaultValue);
  1417. - }
  1418. - }
  1419. -
  1420. - entries.Add(new InboundRouteEntry()
  1421. - {
  1422. - Defaults = defaults,
  1423. - Order = group.Key.Order,
  1424. - Precedence = RoutePrecedence.ComputeInbound(template),
  1425. - RouteTemplate = template,
  1426. - Tag = group.Value.ToArray(),
  1427. - });
  1428. - }
  1429. -
  1430. - var trees = new List<UrlMatchingTree>();
  1431. - for (var i = 0; i < entries.Count; i++)
  1432. - {
  1433. - var entry = entries[i];
  1434. -
  1435. - while (trees.Count <= entry.Order)
  1436. - {
  1437. - trees.Add(new UrlMatchingTree(trees.Count));
  1438. - }
  1439. -
  1440. - var tree = trees[entry.Order];
  1441. -
  1442. - TreeRouteBuilder.AddEntryToTree(tree, entry);
  1443. - }
  1444. -
  1445. - return new Cache(trees.ToArray());
  1446. - }
  1447. -
  1448. - private struct Key : IEquatable<Key>
  1449. - {
  1450. - public readonly int Order;
  1451. - public readonly string RouteTemplate;
  1452. -
  1453. - public Key(int order, string routeTemplate)
  1454. - {
  1455. - Order = order;
  1456. - RouteTemplate = routeTemplate;
  1457. - }
  1458. -
  1459. - public bool Equals(Key other)
  1460. - {
  1461. - return Order == other.Order && string.Equals(RouteTemplate, other.RouteTemplate, StringComparison.OrdinalIgnoreCase);
  1462. - }
  1463. -
  1464. - public override bool Equals(object obj)
  1465. - {
  1466. - return obj is Key ? Equals((Key)obj) : false;
  1467. - }
  1468. -
  1469. - public override int GetHashCode()
  1470. - {
  1471. - var hash = new HashCodeCombiner();
  1472. - hash.Add(Order);
  1473. - hash.Add(RouteTemplate, StringComparer.OrdinalIgnoreCase);
  1474. - return hash;
  1475. - }
  1476. - }
  1477. -
  1478. - private class Cache
  1479. - {
  1480. - public readonly UrlMatchingTree[] Trees;
  1481. -
  1482. - public Cache(UrlMatchingTree[] trees)
  1483. - {
  1484. - Trees = trees;
  1485. - }
  1486. - }
  1487. -
  1488. - private struct Treenumerator : IEnumerator<UrlMatchingNode>
  1489. - {
  1490. - private readonly Stack<UrlMatchingNode> _stack;
  1491. - private readonly PathTokenizer _tokenizer;
  1492. -
  1493. - public Treenumerator(UrlMatchingNode root, PathTokenizer tokenizer)
  1494. - {
  1495. - _stack = new Stack<UrlMatchingNode>();
  1496. - _tokenizer = tokenizer;
  1497. - Current = null;
  1498. -
  1499. - _stack.Push(root);
  1500. - }
  1501. -
  1502. - public UrlMatchingNode Current { get; private set; }
  1503. -
  1504. - object IEnumerator.Current => Current;
  1505. -
  1506. - public void Dispose()
  1507. - {
  1508. - }
  1509. -
  1510. - public bool MoveNext()
  1511. - {
  1512. - if (_stack == null)
  1513. - {
  1514. - return false;
  1515. - }
  1516. -
  1517. - while (_stack.Count > 0)
  1518. - {
  1519. - var next = _stack.Pop();
  1520. -
  1521. - // In case of wild card segment, the request path segment length can be greater
  1522. - // Example:
  1523. - // Template: a/{*path}
  1524. - // Request Url: a/b/c/d
  1525. - if (next.IsCatchAll && next.Matches.Count > 0)
  1526. - {
  1527. - Current = next;
  1528. - return true;
  1529. - }
  1530. - // Next template has the same length as the url we are trying to match
  1531. - // The only possible matching segments are either our current matches or
  1532. - // any catch-all segment after this segment in which the catch all is empty.
  1533. - else if (next.Depth == _tokenizer.Count)
  1534. - {
  1535. - if (next.Matches.Count > 0)
  1536. - {
  1537. - Current = next;
  1538. - return true;
  1539. - }
  1540. - else
  1541. - {
  1542. - // We can stop looking as any other child node from this node will be
  1543. - // either a literal, a constrained parameter or a parameter.
  1544. - // (Catch alls and constrained catch alls will show up as candidate matches).
  1545. - continue;
  1546. - }
  1547. - }
  1548. -
  1549. - if (next.CatchAlls != null)
  1550. - {
  1551. - _stack.Push(next.CatchAlls);
  1552. - }
  1553. -
  1554. - if (next.ConstrainedCatchAlls != null)
  1555. - {
  1556. - _stack.Push(next.ConstrainedCatchAlls);
  1557. - }
  1558. -
  1559. - if (next.Parameters != null)
  1560. - {
  1561. - _stack.Push(next.Parameters);
  1562. - }
  1563. -
  1564. - if (next.ConstrainedParameters != null)
  1565. - {
  1566. - _stack.Push(next.ConstrainedParameters);
  1567. - }
  1568. -
  1569. - if (next.Literals.Count > 0)
  1570. - {
  1571. - UrlMatchingNode node;
  1572. - Debug.Assert(next.Depth < _tokenizer.Count);
  1573. - if (next.Literals.TryGetValue(_tokenizer[next.Depth].Value, out node))
  1574. - {
  1575. - _stack.Push(node);
  1576. - }
  1577. - }
  1578. - }
  1579. -
  1580. - return false;
  1581. - }
  1582. -
  1583. - public void Reset()
  1584. - {
  1585. - _stack.Clear();
  1586. - Current = null;
  1587. - }
  1588. - }
  1589. - }
  1590. -}
  1591. diff --git a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs
  1592. index f44aebf160d..9b3c58df6b9 100644
  1593. --- a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs
  1594. +++ b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouteBuilder.cs
  1595. @@ -5,11 +5,9 @@ using System;
  1596. using System.Collections.Generic;
  1597. using System.Diagnostics;
  1598. using System.Linq;
  1599. -using System.Text.Encodings.Web;
  1600. using Microsoft.AspNetCore.Dispatcher;
  1601. using Microsoft.AspNetCore.Routing.Template;
  1602. using Microsoft.Extensions.Logging;
  1603. -using Microsoft.Extensions.ObjectPool;
  1604. namespace Microsoft.AspNetCore.Routing.Tree
  1605. {
  1606. @@ -78,7 +76,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
  1607. {
  1608. Handler = handler,
  1609. Order = order,
  1610. - Precedence = RoutePrecedence.ComputeInbound(routeTemplate),
  1611. + Precedence = Template.RoutePrecedence.ComputeInbound(routeTemplate),
  1612. RouteName = routeName,
  1613. RouteTemplate = routeTemplate,
  1614. };
  1615. @@ -150,7 +148,7 @@ namespace Microsoft.AspNetCore.Routing.Tree
  1616. {
  1617. Handler = handler,
  1618. Order = order,
  1619. - Precedence = RoutePrecedence.ComputeOutbound(routeTemplate),
  1620. + Precedence = Template.RoutePrecedence.ComputeOutbound(routeTemplate),
  1621. RequiredLinkValues = requiredLinkValues,
  1622. RouteName = routeName,
  1623. RouteTemplate = routeTemplate,
  1624. diff --git a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs
  1625. index 9a193cc0154..d5d195c4362 100644
  1626. --- a/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs
  1627. +++ b/test/Microsoft.AspNetCore.Dispatcher.FunctionalTest/ApiAppStartup.cs
  1628. @@ -1,11 +1,9 @@
  1629. // Copyright (c) .NET Foundation. All rights reserved.
  1630. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  1631. -using System.Linq;
  1632. using System.Threading.Tasks;
  1633. using Microsoft.AspNetCore.Builder;
  1634. using Microsoft.AspNetCore.Http;
  1635. -using Microsoft.AspNetCore.Routing.Dispatcher;
  1636. using Microsoft.Extensions.DependencyInjection;
  1637. using Microsoft.Extensions.Logging;
  1638. diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternMatcherTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternMatcherTest.cs
  1639. similarity index 100%
  1640. rename from test/Microsoft.AspNetCore.Dispatcher.Test/RoutePatternMatcherTest.cs
  1641. rename to test/Microsoft.AspNetCore.Dispatcher.Test/Patterns/RoutePatternMatcherTest.cs
  1642. diff --git a/test/Microsoft.AspNetCore.Dispatcher.Test/Tree/TreeMatcherTest.cs b/test/Microsoft.AspNetCore.Dispatcher.Test/Tree/TreeMatcherTest.cs
  1643. new file mode 100644
  1644. index 00000000000..a4d2ad88c57
  1645. --- /dev/null
  1646. +++ b/test/Microsoft.AspNetCore.Dispatcher.Test/Tree/TreeMatcherTest.cs
  1647. @@ -0,0 +1,808 @@
  1648. +// Copyright (c) .NET Foundation. All rights reserved.
  1649. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  1650. +
  1651. +using System.Collections.Generic;
  1652. +using System.Threading.Tasks;
  1653. +using Microsoft.AspNetCore.Http;
  1654. +using Microsoft.Extensions.Logging;
  1655. +using Microsoft.Extensions.Logging.Abstractions;
  1656. +using Microsoft.Extensions.Options;
  1657. +using Moq;
  1658. +using Xunit;
  1659. +
  1660. +namespace Microsoft.AspNetCore.Dispatcher
  1661. +{
  1662. + public class TreeMatcherTest
  1663. + {
  1664. + [Theory]
  1665. + [InlineData("template/5", "template/{parameter:int}")]
  1666. + [InlineData("template/5", "template/{parameter}")]
  1667. + [InlineData("template/5", "template/{*parameter:int}")]
  1668. + [InlineData("template/5", "template/{*parameter}")]
  1669. + [InlineData("template/{parameter}", "template/{parameter:alpha}")] // constraint doesn't match
  1670. + [InlineData("template/{parameter:int}", "template/{parameter}")]
  1671. + [InlineData("template/{parameter:int}", "template/{*parameter:int}")]
  1672. + [InlineData("template/{parameter:int}", "template/{*parameter}")]
  1673. + [InlineData("template/{parameter}", "template/{*parameter:int}")]
  1674. + [InlineData("template/{parameter}", "template/{*parameter}")]
  1675. + [InlineData("template/5", "template/5/{*parameter}")]
  1676. + [InlineData("template/{*parameter:int}", "template/{*parameter}")]
  1677. + public async Task MatchAsync_RespectsPrecedence(
  1678. + string firstTemplate,
  1679. + string secondTemplate)
  1680. + {
  1681. + // Arrange
  1682. + var dataSource = new DefaultDispatcherDataSource()
  1683. + {
  1684. + Endpoints =
  1685. + {
  1686. + new RoutePatternEndpoint(firstTemplate, new { }, Test_Delegate, "Test1"),
  1687. + new RoutePatternEndpoint(secondTemplate, new { }, Test_Delegate, "Test2"),
  1688. + },
  1689. + };
  1690. +
  1691. + var context = CreateMatcherContext("/template/5");
  1692. + var factory = new TreeMatcherFactory();
  1693. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  1694. +
  1695. + // Act
  1696. + await matcher.MatchAsync(context);
  1697. +
  1698. + // Assert
  1699. + Assert.Same(dataSource.Endpoints[0], context.Endpoint);
  1700. + }
  1701. +
  1702. + [Theory]
  1703. + [InlineData("template/5", "template/{parameter:int}")]
  1704. + [InlineData("template/5", "template/{parameter}")]
  1705. + [InlineData("template/5", "template/{*parameter:int}")]
  1706. + [InlineData("template/5", "template/{*parameter}")]
  1707. + [InlineData("template/{parameter:int}", "template/{parameter}")]
  1708. + [InlineData("template/{parameter:int}", "template/{*parameter:int}")]
  1709. + [InlineData("template/{parameter:int}", "template/{*parameter}")]
  1710. + [InlineData("template/{parameter}", "template/{*parameter:int}")]
  1711. + [InlineData("template/{parameter}", "template/{*parameter}")]
  1712. + [InlineData("template/5", "template/5/{*parameter}")]
  1713. + [InlineData("template/{*parameter:int}", "template/{*parameter}")]
  1714. + public async Task MatchAsync_RespectsOrderOverPrecedence(
  1715. + string firstTemplate,
  1716. + string secondTemplate)
  1717. + {
  1718. + // Arrange
  1719. + var dataSource = new DefaultDispatcherDataSource()
  1720. + {
  1721. + Endpoints =
  1722. + {
  1723. + new RoutePatternEndpoint(firstTemplate, new { }, Test_Delegate, "Test1", new EndpointOrderMetadata(1)),
  1724. + new RoutePatternEndpoint(secondTemplate, new { }, Test_Delegate, "Test2", new EndpointOrderMetadata(0)),
  1725. + },
  1726. + };
  1727. +
  1728. + var context = CreateMatcherContext("/template/5");
  1729. + var factory = new TreeMatcherFactory();
  1730. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  1731. +
  1732. + // Act
  1733. + await matcher.MatchAsync(context);
  1734. +
  1735. + // Assert
  1736. + Assert.Same(dataSource.Endpoints[1], context.Endpoint);
  1737. + }
  1738. +
  1739. + [Theory]
  1740. + [InlineData("template/{first:int}", "template/{second:int}")]
  1741. + [InlineData("template/{first}", "template/{second}")]
  1742. + [InlineData("template/{*first:int}", "template/{*second:int}")]
  1743. + [InlineData("template/{*first}", "template/{*second}")]
  1744. + public async Task MatchAsync_EnsuresStableOrdering(string firstTemplate, string secondTemplate)
  1745. + {
  1746. + // Arrange
  1747. + var dataSource = new DefaultDispatcherDataSource()
  1748. + {
  1749. + Endpoints =
  1750. + {
  1751. + new RoutePatternEndpoint(firstTemplate, new { }, Test_Delegate, "Test1"),
  1752. + new RoutePatternEndpoint(secondTemplate, new { }, Test_Delegate, "Test2"),
  1753. + },
  1754. + };
  1755. +
  1756. + var context = CreateMatcherContext("/template/5");
  1757. + var factory = new TreeMatcherFactory();
  1758. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  1759. +
  1760. + // Act
  1761. + await matcher.MatchAsync(context);
  1762. +
  1763. + // Assert
  1764. + Assert.Same(dataSource.Endpoints[0], context.Endpoint);
  1765. + }
  1766. +
  1767. + [Theory]
  1768. + [InlineData("/", 0)]
  1769. + [InlineData("/Literal1", 1)]
  1770. + [InlineData("/Literal1/Literal2", 2)]
  1771. + [InlineData("/Literal1/Literal2/Literal3", 3)]
  1772. + [InlineData("/Literal1/Literal2/Literal3/4", 4)]
  1773. + [InlineData("/Literal1/Literal2/Literal3/Literal4", 5)]
  1774. + [InlineData("/1", 6)]
  1775. + [InlineData("/1/2", 7)]
  1776. + [InlineData("/1/2/3", 8)]
  1777. + [InlineData("/1/2/3/4", 9)]
  1778. + [InlineData("/1/2/3/CatchAll4", 10)]
  1779. + [InlineData("/parameter1", 11)]
  1780. + [InlineData("/parameter1/parameter2", 12)]
  1781. + [InlineData("/parameter1/parameter2/parameter3", 13)]
  1782. + [InlineData("/parameter1/parameter2/parameter3/4", 14)]
  1783. + [InlineData("/parameter1/parameter2/parameter3/CatchAll4", 15)]
  1784. + public async Task MatchAsync_MatchesEndpointWithTheRightLength(string url, int index)
  1785. + {
  1786. + // Arrange
  1787. + var dataSource = new DefaultDispatcherDataSource()
  1788. + {
  1789. + Endpoints =
  1790. + {
  1791. + new RoutePatternEndpoint("", Test_Delegate),
  1792. + new RoutePatternEndpoint("Literal1", Test_Delegate),
  1793. + new RoutePatternEndpoint("Literal1/Literal2", Test_Delegate),
  1794. + new RoutePatternEndpoint("Literal1/Literal2/Literal3", Test_Delegate),
  1795. + new RoutePatternEndpoint("Literal1/Literal2/Literal3/{*constrainedCatchAll:int}", Test_Delegate),
  1796. + new RoutePatternEndpoint("Literal1/Literal2/Literal3/{*catchAll}", Test_Delegate),
  1797. + new RoutePatternEndpoint("{constrained1:int}", Test_Delegate),
  1798. + new RoutePatternEndpoint("{constrained1:int}/{constrained2:int}", Test_Delegate),
  1799. + new RoutePatternEndpoint("{constrained1:int}/{constrained2:int}/{constrained3:int}", Test_Delegate),
  1800. + new RoutePatternEndpoint("{constrained1:int}/{constrained2:int}/{constrained3:int}/{*constrainedCatchAll:int}", Test_Delegate),
  1801. + new RoutePatternEndpoint("{constrained1:int}/{constrained2:int}/{constrained3:int}/{*catchAll}", Test_Delegate),
  1802. + new RoutePatternEndpoint("{parameter1}", Test_Delegate),
  1803. + new RoutePatternEndpoint("{parameter1}/{parameter2}", Test_Delegate),
  1804. + new RoutePatternEndpoint("{parameter1}/{parameter2}/{parameter3}", Test_Delegate),
  1805. + new RoutePatternEndpoint("{parameter1}/{parameter2}/{parameter3}/{*constrainedCatchAll:int}", Test_Delegate),
  1806. + new RoutePatternEndpoint("{parameter1}/{parameter2}/{parameter3}/{*catchAll}", Test_Delegate),
  1807. + },
  1808. + };
  1809. +
  1810. + var context = CreateMatcherContext(url);
  1811. + var factory = new TreeMatcherFactory();
  1812. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  1813. +
  1814. + // Act
  1815. + await matcher.MatchAsync(context);
  1816. +
  1817. + // Assert
  1818. + Assert.Same(dataSource.Endpoints[index], context.Endpoint);
  1819. + }
  1820. +
  1821. + public static TheoryData<string, object[]> MatchesEndpointsWithDefaultsData =>
  1822. + new TheoryData<string, object[]>
  1823. + {
  1824. + { "/", new object[] { "1", "2", "3", "4" } },
  1825. + { "/a", new object[] { "a", "2", "3", "4" } },
  1826. + { "/a/b", new object[] { "a", "b", "3", "4" } },
  1827. + { "/a/b/c", new object[] { "a", "b", "c", "4" } },
  1828. + { "/a/b/c/d", new object[] { "a", "b", "c", "d" } }
  1829. + };
  1830. +
  1831. + [Theory]
  1832. + [MemberData(nameof(MatchesEndpointsWithDefaultsData))]
  1833. + public async Task MatchAsync_MatchesEndpointsWithDefaults(string url, object[] values)
  1834. + {
  1835. + // Arrange
  1836. + var dataSource = new DefaultDispatcherDataSource()
  1837. + {
  1838. + Endpoints =
  1839. + {
  1840. + new RoutePatternEndpoint("{parameter1=1}/{parameter2=2}/{parameter3=3}/{parameter4=4}",
  1841. + new { parameter1 = 1, parameter2 = 2, parameter3 = 3, parameter4 = 4 }, Test_Delegate, "Test"),
  1842. + },
  1843. + };
  1844. +
  1845. + var valueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" };
  1846. + var expectedValues = new DispatcherValueCollection();
  1847. + for (int i = 0; i < valueKeys.Length; i++)
  1848. + {
  1849. + expectedValues.Add(valueKeys[i], values[i]);
  1850. + }
  1851. +
  1852. + var context = CreateMatcherContext(url);
  1853. + var factory = new TreeMatcherFactory();
  1854. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  1855. +
  1856. + // Act
  1857. + await matcher.MatchAsync(context);
  1858. +
  1859. + // Assert
  1860. + foreach (var entry in expectedValues)
  1861. + {
  1862. + var data = Assert.Single(context.Values, v => v.Key == entry.Key);
  1863. + Assert.Equal(entry.Value, data.Value);
  1864. + }
  1865. + }
  1866. +
  1867. + public static TheoryData<string, object[]> MatchesConstrainedEndpointsWithDefaultsData =>
  1868. + new TheoryData<string, object[]>
  1869. + {
  1870. + { "/", new object[] { "1", "2", "3", "4" } },
  1871. + { "/10", new object[] { "10", "2", "3", "4" } },
  1872. + { "/10/11", new object[] { "10", "11", "3", "4" } },
  1873. + { "/10/11/12", new object[] { "10", "11", "12", "4" } },
  1874. + { "/10/11/12/13", new object[] { "10", "11", "12", "13" } }
  1875. + };
  1876. +
  1877. + [Theory]
  1878. + [MemberData(nameof(MatchesConstrainedEndpointsWithDefaultsData))]
  1879. + public async Task MatchAsync_MatchesConstrainedEndpointsWithDefaults(string url, object[] values)
  1880. + {
  1881. + // Arrange
  1882. + var dataSource = new DefaultDispatcherDataSource()
  1883. + {
  1884. + Endpoints =
  1885. + {
  1886. + new RoutePatternEndpoint("{parameter1:int=1}/{parameter2:int=2}/{parameter3:int=3}/{parameter4:int=4}",
  1887. + new { parameter1 = 1, parameter2 = 2, parameter3 = 3, parameter4 = 4 }, Test_Delegate, "Test"),
  1888. + },
  1889. + };
  1890. +
  1891. + var valueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" };
  1892. + var expectedValues = new DispatcherValueCollection();
  1893. + for (int i = 0; i < valueKeys.Length; i++)
  1894. + {
  1895. + expectedValues.Add(valueKeys[i], values[i]);
  1896. + }
  1897. +
  1898. + var context = CreateMatcherContext(url);
  1899. + var factory = new TreeMatcherFactory();
  1900. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  1901. +
  1902. + // Act
  1903. + await matcher.MatchAsync(context);
  1904. +
  1905. + // Assert
  1906. + foreach (var entry in expectedValues)
  1907. + {
  1908. + var data = Assert.Single(context.Values, v => v.Key == entry.Key);
  1909. + Assert.Equal(entry.Value, data.Value);
  1910. + }
  1911. + }
  1912. +
  1913. + [Fact]
  1914. + public async Task MatchAsync_MatchesCatchAllEndpointsWithDefaults()
  1915. + {
  1916. + // Arrange
  1917. + var dataSource = new DefaultDispatcherDataSource()
  1918. + {
  1919. + Endpoints =
  1920. + {
  1921. + new RoutePatternEndpoint("{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}",
  1922. + new { parameter1 = 1, parameter2 = 2, parameter3 = 3, parameter4 = 4 }, Test_Delegate, "Test"),
  1923. + },
  1924. + };
  1925. +
  1926. + var url = "/a/b/c";
  1927. + var values = new[] { "a", "b", "c", "4" };
  1928. +
  1929. + var valueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" };
  1930. + var expectedValues = new DispatcherValueCollection();
  1931. + for (int i = 0; i < valueKeys.Length; i++)
  1932. + {
  1933. + expectedValues.Add(valueKeys[i], values[i]);
  1934. + }
  1935. +
  1936. + var context = CreateMatcherContext(url);
  1937. + var factory = new TreeMatcherFactory();
  1938. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  1939. +
  1940. + // Act
  1941. + await matcher.MatchAsync(context);
  1942. +
  1943. + // Assert
  1944. + foreach (var entry in expectedValues)
  1945. + {
  1946. + var data = Assert.Single(context.Values, v => v.Key == entry.Key);
  1947. + Assert.Equal(entry.Value, data.Value);
  1948. + }
  1949. + }
  1950. +
  1951. + [Fact]
  1952. + public async Task MatchAsync_DoesNotMatchEndpointsWithIntermediateDefaultValues()
  1953. + {
  1954. + // Arrange
  1955. + var url = "/a/b";
  1956. + var dataSource = new DefaultDispatcherDataSource()
  1957. + {
  1958. + Endpoints =
  1959. + {
  1960. + new RoutePatternEndpoint("a/b/{parameter3=3}/d",
  1961. + new { parameter3 = 3}, Test_Delegate, "Test"),
  1962. + },
  1963. + };
  1964. +
  1965. + var context = CreateMatcherContext(url);
  1966. + var factory = new TreeMatcherFactory();
  1967. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  1968. +
  1969. + // Act
  1970. + await matcher.MatchAsync(context);
  1971. +
  1972. + // Assert
  1973. + Assert.Null(context.Endpoint);
  1974. + }
  1975. +
  1976. + [Theory]
  1977. + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a")]
  1978. + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b")]
  1979. + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c")]
  1980. + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d")]
  1981. + public async Task MatchAsync_DoesNotMatchEndpointsWithMultipleIntermediateDefaultOrOptionalValues(string template, string url)
  1982. + {
  1983. + // Arrange
  1984. + var dataSource = new DefaultDispatcherDataSource()
  1985. + {
  1986. + Endpoints =
  1987. + {
  1988. + new RoutePatternEndpoint(template,
  1989. + new { b = 3}, Test_Delegate, "Test"),
  1990. + },
  1991. + };
  1992. +
  1993. + var context = CreateMatcherContext(url);
  1994. + var factory = new TreeMatcherFactory();
  1995. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  1996. +
  1997. + // Act
  1998. + await matcher.MatchAsync(context);
  1999. +
  2000. + // Assert
  2001. + Assert.Null(context.Endpoint);
  2002. + }
  2003. +
  2004. + [Theory]
  2005. + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e")]
  2006. + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e/f")]
  2007. + public async Task MatchAsync_MatchRoutesWithMultipleIntermediateDefaultOrOptionalValues_WhenAllIntermediateValuesAreProvided(string template, string url)
  2008. + {
  2009. + // Arrange
  2010. + var dataSource = new DefaultDispatcherDataSource()
  2011. + {
  2012. + Endpoints =
  2013. + {
  2014. + new RoutePatternEndpoint(template,
  2015. + new { b = 3}, Test_Delegate, "Test"),
  2016. + },
  2017. + };
  2018. +
  2019. + var context = CreateMatcherContext(url);
  2020. + var factory = new TreeMatcherFactory();
  2021. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  2022. +
  2023. + // Act
  2024. + await matcher.MatchAsync(context);
  2025. +
  2026. + // Assert
  2027. + Assert.NotNull(context.Endpoint);
  2028. + }
  2029. +
  2030. + [Fact]
  2031. + public void MatchAsync_DoesNotMatchShorterUrl()
  2032. + {
  2033. + // Arrange
  2034. + var dataSource = new DefaultDispatcherDataSource()
  2035. + {
  2036. + Endpoints =
  2037. + {
  2038. + new RoutePatternEndpoint("Literal1/Literal2/Literal3",
  2039. + new object(), Test_Delegate, "Test"),
  2040. + },
  2041. + };
  2042. +
  2043. + var routes = new[] {
  2044. + "Literal1/Literal2/Literal3",
  2045. + };
  2046. +
  2047. + var context = CreateMatcherContext("/Literal1");
  2048. + var factory = new TreeMatcherFactory();
  2049. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  2050. +
  2051. + // Assert
  2052. + Assert.Null(context.Endpoint);
  2053. + }
  2054. +
  2055. + [Theory]
  2056. + [InlineData("///")]
  2057. + [InlineData("/a//")]
  2058. + [InlineData("/a/b//")]
  2059. + [InlineData("//b//")]
  2060. + [InlineData("///c")]
  2061. + [InlineData("///c/")]
  2062. + public async Task MatchAsync_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url)
  2063. + {
  2064. + // Arrange
  2065. + var dataSource = new DefaultDispatcherDataSource()
  2066. + {
  2067. + Endpoints =
  2068. + {
  2069. + new RoutePatternEndpoint("{controller?}/{action?}/{id?}",
  2070. + new object(), Test_Delegate, "Test"),
  2071. + },
  2072. + };
  2073. + var context = CreateMatcherContext(url);
  2074. + var factory = new TreeMatcherFactory();
  2075. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  2076. +
  2077. + // Act
  2078. + await matcher.MatchAsync(context);
  2079. +
  2080. + // Assert
  2081. + Assert.Null(context.Endpoint);
  2082. + }
  2083. +
  2084. + [Theory]
  2085. + [InlineData("")]
  2086. + [InlineData("/")]
  2087. + [InlineData("/a")]
  2088. + [InlineData("/a/")]
  2089. + [InlineData("/a/b")]
  2090. + [InlineData("/a/b/")]
  2091. + [InlineData("/a/b/c")]
  2092. + [InlineData("/a/b/c/")]
  2093. + public async Task MatchAsync_MultipleOptionalParameters_WithIncrementalOptionalValues(string url)
  2094. + {
  2095. + // Arrange
  2096. + var dataSource = new DefaultDispatcherDataSource()
  2097. + {
  2098. + Endpoints =
  2099. + {
  2100. + new RoutePatternEndpoint("{controller?}/{action?}/{id?}", new {}, Test_Delegate, "Test"),
  2101. + },
  2102. + };
  2103. +
  2104. + var context = CreateMatcherContext(url);
  2105. + var factory = new TreeMatcherFactory();
  2106. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  2107. +
  2108. + // Act
  2109. + await matcher.MatchAsync(context);
  2110. +
  2111. + // Assert
  2112. + Assert.NotNull(context.Endpoint);
  2113. + }
  2114. +
  2115. + [Theory]
  2116. + [InlineData("///")]
  2117. + [InlineData("////")]
  2118. + [InlineData("/a//")]
  2119. + [InlineData("/a///")]
  2120. + [InlineData("//b/")]
  2121. + [InlineData("//b//")]
  2122. + [InlineData("///c")]
  2123. + [InlineData("///c/")]
  2124. + public async Task MatchAsync_MultipleParameters_WithEmptyValuesDoesNotMatch(string url)
  2125. + {
  2126. + // Arrange
  2127. + var dataSource = new DefaultDispatcherDataSource()
  2128. + {
  2129. + Endpoints =
  2130. + {
  2131. + new RoutePatternEndpoint("{controller?}/{action?}/{id?}",
  2132. + new object(), Test_Delegate, "Test"),
  2133. + },
  2134. + };
  2135. +
  2136. + var context = CreateMatcherContext(url);
  2137. + var factory = new TreeMatcherFactory();
  2138. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  2139. +
  2140. + // Act
  2141. + await matcher.MatchAsync(context);
  2142. +
  2143. + // Assert
  2144. + Assert.Null(context.Endpoint);
  2145. + }
  2146. +
  2147. + [Theory]
  2148. + [InlineData("/a/b/c//")]
  2149. + [InlineData("/a/b/c/////")]
  2150. + public async Task MatchAsync_CatchAllParameters_WithEmptyValuesAtTheEnd(string url)
  2151. + {
  2152. + // Arrange
  2153. + var dataSource = new DefaultDispatcherDataSource()
  2154. + {
  2155. + Endpoints =
  2156. + {
  2157. + new RoutePatternEndpoint("{controller}/{action}/{*id}",
  2158. + new object(), Test_Delegate, "Test"),
  2159. + },
  2160. + };
  2161. +
  2162. + var context = CreateMatcherContext(url);
  2163. + var factory = new TreeMatcherFactory();
  2164. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  2165. +
  2166. + // Act
  2167. + await matcher.MatchAsync(context);
  2168. +
  2169. + // Assert
  2170. + Assert.Same(dataSource.Endpoints[0], context.Endpoint);
  2171. + }
  2172. +
  2173. + [Theory]
  2174. + [InlineData("/a/b//")]
  2175. + [InlineData("/a/b///c")]
  2176. + public async Task MatchAsync_CatchAllParameters_WithEmptyValues(string url)
  2177. + {
  2178. + // Arrange
  2179. + var dataSource = new DefaultDispatcherDataSource()
  2180. + {
  2181. + Endpoints =
  2182. + {
  2183. + new RoutePatternEndpoint("{controller}/{action}/{*id}",
  2184. + new object(), Test_Delegate, "Test"),
  2185. + },
  2186. + };
  2187. +
  2188. + var context = CreateMatcherContext(url);
  2189. + var factory = new TreeMatcherFactory();
  2190. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  2191. +
  2192. + // Act
  2193. + await matcher.MatchAsync(context);
  2194. +
  2195. + // Assert
  2196. + Assert.Null(context.Endpoint);
  2197. + }
  2198. +
  2199. + [Theory]
  2200. + [InlineData("{*path}", "/a", "a")]
  2201. + [InlineData("{*path}", "/a/b/c", "a/b/c")]
  2202. + [InlineData("a/{*path}", "/a/b", "b")]
  2203. + [InlineData("a/{*path}", "/a/b/c/d", "b/c/d")]
  2204. + [InlineData("a/{*path:regex(10/20/30)}", "/a/10/20/30", "10/20/30")]
  2205. + public async Task MatchAsync_MatchesWildCard_ForLargerPathSegments(
  2206. + string template,
  2207. + string requestPath,
  2208. + string expectedResult)
  2209. + {
  2210. + // Arrange
  2211. + var dataSource = new DefaultDispatcherDataSource()
  2212. + {
  2213. + Endpoints =
  2214. + {
  2215. + new RoutePatternEndpoint(template,
  2216. + new object(), Test_Delegate, "Test"),
  2217. + },
  2218. + };
  2219. +
  2220. + var context = CreateMatcherContext(requestPath);
  2221. + var factory = new TreeMatcherFactory();
  2222. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  2223. +
  2224. + // Act
  2225. + await matcher.MatchAsync(context);
  2226. +
  2227. + // Assert
  2228. + Assert.Same(dataSource.Endpoints[0], context.Endpoint);
  2229. + Assert.Equal(expectedResult, context.Values["path"]);
  2230. + }
  2231. +
  2232. + [Theory]
  2233. + [InlineData("a/{*path}", "/a")]
  2234. + [InlineData("a/{*path}", "/a/")]
  2235. + public async Task MatchAsync_MatchesCatchAll_NullValue(
  2236. + string template,
  2237. + string requestPath)
  2238. + {
  2239. + // Arrange
  2240. + var dataSource = new DefaultDispatcherDataSource()
  2241. + {
  2242. + Endpoints =
  2243. + {
  2244. + new RoutePatternEndpoint(template,
  2245. + new object(), Test_Delegate, "Test"),
  2246. + },
  2247. + };
  2248. +
  2249. + var context = CreateMatcherContext(requestPath);
  2250. + var factory = new TreeMatcherFactory();
  2251. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  2252. +
  2253. + // Act
  2254. + await matcher.MatchAsync(context);
  2255. +
  2256. + // Assert
  2257. + Assert.Same(dataSource.Endpoints[0], context.Endpoint);
  2258. + Assert.Null(context.Values["path"]);
  2259. + }
  2260. +
  2261. + [Theory]
  2262. + [InlineData("a/{*path=default}", "/a")]
  2263. + [InlineData("a/{*path=default}", "/a/")]
  2264. + public async Task MatchAsync_MatchesCatchAll_UsesDefaultValue(
  2265. + string template,
  2266. + string requestPath)
  2267. + {
  2268. + // Arrange
  2269. + var dataSource = new DefaultDispatcherDataSource()
  2270. + {
  2271. + Endpoints =
  2272. + {
  2273. + new RoutePatternEndpoint(template,
  2274. + new object(), Test_Delegate, "Test"),
  2275. + },
  2276. + };
  2277. +
  2278. + var factory = new TreeMatcherFactory();
  2279. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  2280. +
  2281. + var context = CreateMatcherContext(requestPath);
  2282. +
  2283. + // Act
  2284. + await matcher.MatchAsync(context);
  2285. +
  2286. + // Assert
  2287. + Assert.Same(dataSource.Endpoints[0], context.Endpoint);
  2288. + Assert.Equal("default", context.Values["path"]);
  2289. + }
  2290. +
  2291. + [Theory]
  2292. + [InlineData("template/{parameter:int}", "/template/5", true)]
  2293. + [InlineData("template/{parameter:int?}", "/template/5", true)]
  2294. + [InlineData("template/{parameter:int?}", "/template", true)]
  2295. + [InlineData("template/{parameter:int?}", "/template/qwer", false)]
  2296. + public async Task MatchAsync_WithOptionalConstraint(
  2297. + string template,
  2298. + string request,
  2299. + bool expectedResult)
  2300. + {
  2301. + // Arrange
  2302. + var dataSource = new DefaultDispatcherDataSource()
  2303. + {
  2304. + Endpoints =
  2305. + {
  2306. + new RoutePatternEndpoint(template,
  2307. + new object(), Test_Delegate, "Test"),
  2308. + },
  2309. + };
  2310. +
  2311. + var factory = new TreeMatcherFactory();
  2312. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  2313. + var context = CreateMatcherContext(request);
  2314. +
  2315. + // Act
  2316. + await matcher.MatchAsync(context);
  2317. +
  2318. + // Assert
  2319. + if (expectedResult)
  2320. + {
  2321. + Assert.NotNull(context.Endpoint);
  2322. + }
  2323. + else
  2324. + {
  2325. + Assert.Null(context.Endpoint);
  2326. + }
  2327. + }
  2328. +
  2329. + [Theory]
  2330. + [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", "foo", "bar", null)]
  2331. + [InlineData("moo/{p1?}", "/moo/foo", "foo", null, null)]
  2332. + [InlineData("moo/{p1?}", "/moo", null, null, null)]
  2333. + [InlineData("moo/{p1}.{p2?}", "/moo/foo", "foo", null, null)]
  2334. + [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", "foo.", "bar", null)]
  2335. + [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", "foo.moo", "bar", null)]
  2336. + [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", "foo", "bar", null)]
  2337. + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", "moo", "bar", null)]
  2338. + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", "moo", null, null)]
  2339. + [InlineData("moo/.{p2?}", "/moo/.foo", null, "foo", null)]
  2340. + [InlineData("moo/{p1}.{p2?}", "/moo/....", "..", ".", null)]
  2341. + [InlineData("moo/{p1}.{p2?}", "/moo/.bar", ".bar", null, null)]
  2342. + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")]
  2343. + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)]
  2344. + [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")]
  2345. + [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")]
  2346. + [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")]
  2347. + [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")]
  2348. + public async Task MatchAsync_WithOptionalCompositeParameter_Valid(
  2349. + string template,
  2350. + string request,
  2351. + string p1,
  2352. + string p2,
  2353. + string p3)
  2354. + {
  2355. + // Arrange
  2356. + var dataSource = new DefaultDispatcherDataSource()
  2357. + {
  2358. + Endpoints =
  2359. + {
  2360. + new RoutePatternEndpoint(template,
  2361. + new object(), Test_Delegate, "Test"),
  2362. + },
  2363. + };
  2364. +
  2365. + var factory = new TreeMatcherFactory();
  2366. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  2367. + var context = CreateMatcherContext(request);
  2368. +
  2369. + // Act
  2370. + await matcher.MatchAsync(context);
  2371. +
  2372. + // Assert
  2373. + Assert.NotNull(context.Endpoint);
  2374. + if (p1 != null)
  2375. + {
  2376. + Assert.Equal(p1, context.Values["p1"]);
  2377. + }
  2378. + if (p2 != null)
  2379. + {
  2380. + Assert.Equal(p2, context.Values["p2"]);
  2381. + }
  2382. + if (p3 != null)
  2383. + {
  2384. + Assert.Equal(p3, context.Values["p3"]);
  2385. + }
  2386. + }
  2387. +
  2388. + [Theory]
  2389. + [InlineData("moo/{p1}.{p2?}", "/moo/foo.")]
  2390. + [InlineData("moo/{p1}.{p2?}", "/moo/.")]
  2391. + [InlineData("moo/{p1}.{p2}", "/foo.")]
  2392. + [InlineData("moo/{p1}.{p2}", "/foo")]
  2393. + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")]
  2394. + [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")]
  2395. + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")]
  2396. + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")]
  2397. + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")]
  2398. + [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")]
  2399. + [InlineData("moo/.{p2?}", "/moo/.")]
  2400. + [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")]
  2401. + public async Task MatchAsync_WithOptionalCompositeParameter_Invalid(
  2402. + string template,
  2403. + string request)
  2404. + {
  2405. + // Arrange
  2406. + var dataSource = new DefaultDispatcherDataSource()
  2407. + {
  2408. + Endpoints =
  2409. + {
  2410. + new RoutePatternEndpoint(template,
  2411. + new object(), Test_Delegate, "Test"),
  2412. + },
  2413. + };
  2414. +
  2415. + var factory = new TreeMatcherFactory();
  2416. + var matcher = factory.CreateMatcher(dataSource, new List<EndpointSelector>());
  2417. + var context = CreateMatcherContext(request);
  2418. +
  2419. + // Act
  2420. + await matcher.MatchAsync(context);
  2421. +
  2422. + // Assert
  2423. + Assert.Null(context.Endpoint);
  2424. + }
  2425. +
  2426. + private static MatcherContext CreateMatcherContext(string requestPath)
  2427. + {
  2428. + var request = new Mock<HttpRequest>(MockBehavior.Strict);
  2429. + request.SetupGet(r => r.Path).Returns(new PathString(requestPath));
  2430. +
  2431. + var context = new Mock<HttpContext>(MockBehavior.Strict);
  2432. + context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory)))
  2433. + .Returns(NullLoggerFactory.Instance);
  2434. + context.Setup(m => m.RequestServices.GetService(typeof(IConstraintFactory)))
  2435. + .Returns(CreateConstraintFactory);
  2436. + context.SetupGet(c => c.Request).Returns(request.Object);
  2437. +
  2438. + return new MatcherContext(context.Object);
  2439. + }
  2440. +
  2441. + private static DefaultConstraintFactory CreateConstraintFactory()
  2442. + {
  2443. + var options = new DispatcherOptions();
  2444. + var optionsMock = new Mock<IOptions<DispatcherOptions>>();
  2445. + optionsMock.SetupGet(o => o.Value).Returns(options);
  2446. +
  2447. + return new DefaultConstraintFactory(optionsMock.Object);
  2448. + }
  2449. +
  2450. + private static Task Test_Delegate(HttpContext httpContext)
  2451. + {
  2452. + return Task.CompletedTask;
  2453. + }
  2454. + }
  2455. +}