From a99fefd28d131411ea29cd804261e4502ed1a0f1 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 28 Jan 2014 09:01:44 -0800 Subject: [PATCH 001/616] initial checkin (readme) --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000000..b238fb750f --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Routing for Project K \ No newline at end of file From 879fa2c4e5bc7aeac6f259a390cc87397e6a6ec5 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 28 Jan 2014 09:09:15 -0800 Subject: [PATCH 002/616] Adding build system files --- .gitignore | 31 +++++++++++++++++++++++++++++++ NuGet.Config | 13 +++++++++++++ build.cmd | 16 ++++++++++++++++ makefile.shade | 7 +++++++ 4 files changed, 67 insertions(+) create mode 100644 .gitignore create mode 100644 NuGet.Config create mode 100644 build.cmd create mode 100644 makefile.shade diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..5cf7c13c7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +[Oo]bj/ +[Bb]in/ +*.xap +*.user +/TestResults +*.vspscc +*.vssscc +*.suo +*.cache +*.docstates +_ReSharper.* +*.csproj.user +*[Rr]e[Ss]harper.user +_ReSharper.*/ +packages/* +artifacts/* +msbuild.log +PublishProfiles/ +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution +*.log +*.vspx +/.symbols +nuget.exe +build/ +*net45.csproj +*k10.csproj \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000000..ab583b0ff7 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000000..c3b2462019 --- /dev/null +++ b/build.cmd @@ -0,0 +1,16 @@ +@echo off +cd %~dp0 + +IF EXIST .nuget\NuGet.exe goto restore +echo Downloading latest version of NuGet.exe... +md .nuget +@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://www.nuget.org/nuget.exe' -OutFile '.nuget\NuGet.exe'" + +:restore +IF EXIST build goto run +.nuget\NuGet.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre +xcopy packages\KoreBuild\build build\ /Y +.nuget\NuGet.exe install Sake -version 0.2 -o packages + +:run +packages\Sake.0.2\tools\Sake.exe -I build -f makefile.shade %* diff --git a/makefile.shade b/makefile.shade new file mode 100644 index 0000000000..6357ea2841 --- /dev/null +++ b/makefile.shade @@ -0,0 +1,7 @@ + +var VERSION='0.1' +var FULL_VERSION='0.1' +var AUTHORS='Microsoft' + +use-standard-lifecycle +k-standard-goals From 876cfaeb5778f908ea06a61fa5168f7e4a416369 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 28 Jan 2014 09:13:56 -0800 Subject: [PATCH 003/616] checkin of project structure --- src/Microsoft.AspNet.Routing/project.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/Microsoft.AspNet.Routing/project.json diff --git a/src/Microsoft.AspNet.Routing/project.json b/src/Microsoft.AspNet.Routing/project.json new file mode 100644 index 0000000000..5f23498788 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/project.json @@ -0,0 +1,8 @@ +{ + "version": "0.1-alpha-*", + "dependencies": {}, + "configurations": { + "net45": { }, + "k10" : { } + } +} \ No newline at end of file From 4183bc98be89371c65f5a708b1f0ec351ccd6ffd Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 28 Jan 2014 14:25:49 -0800 Subject: [PATCH 004/616] Getting routing prototype into history --- .gitattributes | 63 ++ Routing.sln | 43 + samples/RoutingSample/Program.cs | 19 + samples/RoutingSample/Startup.cs | 151 ++++ samples/RoutingSample/project.json | 18 + src/Microsoft.AspNet.Routing/BoundRoute.cs | 26 + .../ContextExtensions.cs | 43 + .../HttpMethod/HttpMethodRoute.cs | 29 + .../HttpMethod/HttpMethodRouteExtensions.cs | 62 ++ src/Microsoft.AspNet.Routing/IConstraint.cs | 15 + src/Microsoft.AspNet.Routing/IRoute.cs | 12 + src/Microsoft.AspNet.Routing/IRouteBuilder.cs | 27 + .../IRouteEndpoint.cs | 16 + src/Microsoft.AspNet.Routing/IRouteEngine.cs | 12 + .../Lambda/LambdaRoute.cs | 44 + .../Lambda/LambdaRouteExtensions.cs | 25 + .../Legacy/BoundRouteTemplate.cs | 16 + .../Legacy/HttpParsedRoute.cs | 821 ++++++++++++++++++ .../Legacy/HttpRouteValueDictionary.cs | 54 ++ .../Legacy/PathSegment.cs | 169 ++++ .../Legacy/PropertyHelper.cs | 189 ++++ .../Legacy/RouteParser.cs | 377 ++++++++ .../Owin/AppBuilderExtensions.cs | 22 + .../Owin/AppFuncMiddleware.cs | 34 + .../Owin/RouteBuilder.cs | 53 ++ .../Owin/RouteEndpoint.cs | 34 + .../Owin/RouterMiddleware.cs | 48 + src/Microsoft.AspNet.Routing/PrefixRoute.cs | 67 ++ .../Properties/AssemblyInfo.cs | 36 + .../RouteBindingContext.cs | 36 + .../RouteBuilderExtensions.cs | 46 + src/Microsoft.AspNet.Routing/RouteEngine.cs | 72 ++ .../RouteEngineExtensions.cs | 18 + src/Microsoft.AspNet.Routing/RouteMatch.cs | 34 + src/Microsoft.AspNet.Routing/RouteTable.cs | 44 + .../RoutingContext.cs | 34 + .../Template/TemplateRoute.cs | 173 ++++ .../Template/TemplateRouteExtensions.cs | 48 + .../Tree/ITreeRouteBuilder.cs | 17 + .../Tree/ITreeSegment.cs | 12 + .../Tree/ParameterSegment.cs | 17 + .../Tree/PathSegment.cs | 21 + .../Tree/TreeRoute.cs | 21 + .../Tree/TreeRouteBuilder.cs | 65 ++ .../Tree/TreeRouteBuilderExtensions.cs | 24 + .../Tree/TreeRouteExtensions.cs | 16 + src/Microsoft.AspNet.Routing/project.json | 7 +- 47 files changed, 3229 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 Routing.sln create mode 100644 samples/RoutingSample/Program.cs create mode 100644 samples/RoutingSample/Startup.cs create mode 100644 samples/RoutingSample/project.json create mode 100644 src/Microsoft.AspNet.Routing/BoundRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/ContextExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRouteExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/IConstraint.cs create mode 100644 src/Microsoft.AspNet.Routing/IRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/IRouteBuilder.cs create mode 100644 src/Microsoft.AspNet.Routing/IRouteEndpoint.cs create mode 100644 src/Microsoft.AspNet.Routing/IRouteEngine.cs create mode 100644 src/Microsoft.AspNet.Routing/Lambda/LambdaRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/Lambda/LambdaRouteExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/Legacy/BoundRouteTemplate.cs create mode 100644 src/Microsoft.AspNet.Routing/Legacy/HttpParsedRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/Legacy/HttpRouteValueDictionary.cs create mode 100644 src/Microsoft.AspNet.Routing/Legacy/PathSegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Legacy/PropertyHelper.cs create mode 100644 src/Microsoft.AspNet.Routing/Legacy/RouteParser.cs create mode 100644 src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/Owin/AppFuncMiddleware.cs create mode 100644 src/Microsoft.AspNet.Routing/Owin/RouteBuilder.cs create mode 100644 src/Microsoft.AspNet.Routing/Owin/RouteEndpoint.cs create mode 100644 src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs create mode 100644 src/Microsoft.AspNet.Routing/PrefixRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.Routing/RouteBindingContext.cs create mode 100644 src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/RouteEngine.cs create mode 100644 src/Microsoft.AspNet.Routing/RouteEngineExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/RouteMatch.cs create mode 100644 src/Microsoft.AspNet.Routing/RouteTable.cs create mode 100644 src/Microsoft.AspNet.Routing/RoutingContext.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateRouteExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/Tree/ITreeRouteBuilder.cs create mode 100644 src/Microsoft.AspNet.Routing/Tree/ITreeSegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Tree/ParameterSegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Tree/PathSegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Tree/TreeRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilder.cs create mode 100644 src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilderExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/Tree/TreeRouteExtensions.cs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..1ff0c42304 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/Routing.sln b/Routing.sln new file mode 100644 index 0000000000..f7f38f58df --- /dev/null +++ b/Routing.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.21005.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Routing.net45", "src\Microsoft.AspNet.Routing\Microsoft.AspNet.Routing.net45.csproj", "{BD60E294-D97A-46CE-A6F0-2A293989B56B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{949A5859-ACBA-4687-B393-644BCF32C1D8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Routing.k10", "src\Microsoft.AspNet.Routing\Microsoft.AspNet.Routing.k10.csproj", "{4463D373-7BAA-42DC-803E-C4B2B8D3A2F4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoutingSample.net45", "samples\RoutingSample\RoutingSample.net45.csproj", "{6552385C-3FB0-40A0-A29B-2B5A5C411621}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{2B8B4E94-8C98-4959-9116-4BAD8FF69468}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BD60E294-D97A-46CE-A6F0-2A293989B56B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD60E294-D97A-46CE-A6F0-2A293989B56B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD60E294-D97A-46CE-A6F0-2A293989B56B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD60E294-D97A-46CE-A6F0-2A293989B56B}.Release|Any CPU.Build.0 = Release|Any CPU + {4463D373-7BAA-42DC-803E-C4B2B8D3A2F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4463D373-7BAA-42DC-803E-C4B2B8D3A2F4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4463D373-7BAA-42DC-803E-C4B2B8D3A2F4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4463D373-7BAA-42DC-803E-C4B2B8D3A2F4}.Release|Any CPU.Build.0 = Release|Any CPU + {6552385C-3FB0-40A0-A29B-2B5A5C411621}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6552385C-3FB0-40A0-A29B-2B5A5C411621}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6552385C-3FB0-40A0-A29B-2B5A5C411621}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6552385C-3FB0-40A0-A29B-2B5A5C411621}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {BD60E294-D97A-46CE-A6F0-2A293989B56B} = {949A5859-ACBA-4687-B393-644BCF32C1D8} + {4463D373-7BAA-42DC-803E-C4B2B8D3A2F4} = {949A5859-ACBA-4687-B393-644BCF32C1D8} + {6552385C-3FB0-40A0-A29B-2B5A5C411621} = {2B8B4E94-8C98-4959-9116-4BAD8FF69468} + EndGlobalSection +EndGlobal diff --git a/samples/RoutingSample/Program.cs b/samples/RoutingSample/Program.cs new file mode 100644 index 0000000000..6db14b0a1e --- /dev/null +++ b/samples/RoutingSample/Program.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.Owin.Hosting; + +namespace RoutingSample +{ + class Program + { + static void Main(string[] args) + { + string url = "http://localhost:30000"; + using (WebApp.Start(url)) + { + Console.WriteLine("Listening on: {0}", url); + Console.WriteLine("Press ENTER to exit."); + Console.ReadLine(); + } + } + } +} diff --git a/samples/RoutingSample/Startup.cs b/samples/RoutingSample/Startup.cs new file mode 100644 index 0000000000..0a040f7b78 --- /dev/null +++ b/samples/RoutingSample/Startup.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Routing.HttpMethod; +using Microsoft.AspNet.Routing.Lambda; +using Microsoft.AspNet.Routing.Legacy; +using Microsoft.AspNet.Routing.Owin; +using Microsoft.AspNet.Routing.Template; +using Microsoft.AspNet.Routing.Tree; +using Owin; + +namespace RoutingSample +{ + internal class Startup + { + public void Configuration(IAppBuilder app) + { + // Configuring the router places it in the OWIN pipeline and retuns an IRouteBuilder which is used + // To create routes - all routes are directed to nested pipelines. + var router = app.UseRouter(); + + + // At the simplest level - we can support microframework style routing. + // + // This route matches a prefix of the request path, and a specific HTTP method. + router.Get("1/echo", async (context) => + { + string url = (string)context["owin.RequestPath"]; + await WriteAsync(context, url); + }); + + + // This route takes a lambda, and can apply arbitrary criteria for routing without needing to couple + // to an object model. + router.On(context => ((string)context["owin.RequestPath"]).StartsWith("2"), async (context) => + { + string url = (string)context["owin.RequestPath"]; + await WriteAsync(context, url); + }); + + + // The return value is an IRouteEndpoint - extension method friendly for adding more routes and different + // route types. + // + // All of these routes go to the same delegate. + router.Get("3/Store", async (context) => + { + string method = (string)context["owin.RequestMethod"]; + await WriteAsync(context, method); + }) + .Post("3/Store/Checkout") + .On(context => ((string)context["owin.RequestPath"]).StartsWith("3/api")); + + + // Routing to a middleware using IAppBuilder -- allowing routing to a more complex pipeline. + router.ForApp((builder) => builder.Use(async (context, next) => + { + await context.Response.WriteAsync("Hello, World!"); + })) + .Get("4/Hello") + .Post("4/Hello/World"); + + + // Nested Router + router.ForApp((builder) => + { + var nested = builder.UseRouter(); + + nested.Get(async (context) => await WriteAsync(context, "Get")); + nested.Post(async (context) => await WriteAsync(context, "Post")); + }) + .Get("5/Store"); + + + + // MVC/WebAPI Stuff below - using 'tradition' template routes. + + + // Routing with parameter capturing - the route data is stored in the owin context + router.ForApp((builder) => builder.Use(async (context, next) => + { + string controller = (string)context.Environment.GetRouteMatchValues()["controller"]; + await context.Response.WriteAsync(controller); + + })) + .AddTemplateRoute("6/api/{controller}", new HttpRouteValueDictionary(new { controller = "Home" })); + + + // Routing with data tokens - these are added to the context when a route matches + // This technique can be used for MVC/Web API to perform action selection as part of routing + router.ForApp((builder) => builder.Use(async (context, next) => + { + string stringValue = (string)context.Environment["myapp_StringValue"]; + await context.Response.WriteAsync(stringValue); + + })) + .AddTemplateRoute("7", null, null, data: new HttpRouteValueDictionary(new { myapp_StringValue = "cool" })); + + + // The route engine can be provided as a parameter to the app builder function so that it can be + // captured and used inside of the application. + // + // It's also provided as part of the owin context on a routed request for apps that would prefer + // a stateless style. + router.ForApp((builder, engine) => builder.Use(async (context, next) => + { + if (Object.Equals(engine, context.Environment.GetRouteEngine())) + { + await context.Response.WriteAsync(engine.GetType().ToString()); + } + else + { + await next(); + } + })) + .AddTemplateRoute("8"); + + + // Generating a link by name + router.ForApp((builder, engine) => builder.Use(async (context, next) => + { + string url = engine.GetUrl("ByName", context.Environment, new HttpRouteValueDictionary(new { })).Url; + await context.Response.WriteAsync(url); + })) + .AddTemplateRoute("ByName", "9/{value1}", new HttpRouteValueDictionary(new { value1 = "Cool" })); + + + // Tree Routing + var tree = router.AddTreeRoute(); + tree.Path("10/api").Parameter("controller").Endpoint(router.ForApp((builder) => builder.Use(async (context, next) => + { + string url = context.Request.Uri.PathAndQuery; + await context.Response.WriteAsync(url); + }))); + + + tree.Build(); + } + + private static Task WriteAsync(IDictionary context, string value) + { + var response = (Stream)context["owin.ResponseBody"]; + + var bytes = Encoding.UTF8.GetBytes(value); + return response.WriteAsync(bytes, 0, bytes.Length); + } + } +} diff --git a/samples/RoutingSample/project.json b/samples/RoutingSample/project.json new file mode 100644 index 0000000000..8bc6ed75af --- /dev/null +++ b/samples/RoutingSample/project.json @@ -0,0 +1,18 @@ +{ + "version": "0.1-alpha-*", + "dependencies": { + "Microsoft.AspNet.Routing" : "" + }, + "configurations": { + "net45": { + "dependencies": { + "Owin": "1.0", + "Microsoft.Owin.Diagnostics": "2.1.0", + "Microsoft.Owin.Host.HttpListener": "2.1.0", + "Microsoft.Owin.Hosting": "2.1.0", + "Microsoft.Owin.SelfHost": "2.1.0", + "Microsoft.Owin": "2.1.0" + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/BoundRoute.cs b/src/Microsoft.AspNet.Routing/BoundRoute.cs new file mode 100644 index 0000000000..e51c5fb6d7 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/BoundRoute.cs @@ -0,0 +1,26 @@ + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public class BoundRoute + { + public BoundRoute(string url, IDictionary values) + { + this.Url = url; + this.Values = values; + } + + public string Url + { + get; + private set; + } + + public IDictionary Values + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/ContextExtensions.cs b/src/Microsoft.AspNet.Routing/ContextExtensions.cs new file mode 100644 index 0000000000..4b2b65f40c --- /dev/null +++ b/src/Microsoft.AspNet.Routing/ContextExtensions.cs @@ -0,0 +1,43 @@ + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public static class OwinExtensions + { + public static string EngineKey = "routing.Engine"; + public static string MatchValuesKey = "routing.Values"; + + public static IRouteEngine GetRouteEngine(this IDictionary context) + { + object obj; + if (context.TryGetValue(EngineKey, out obj)) + { + return obj as IRouteEngine; + } + + return null; + } + + public static void SetRouteEngine(this IDictionary context, IRouteEngine value) + { + context[EngineKey] = value; + } + + public static IDictionary GetRouteMatchValues(this IDictionary context) + { + object obj; + if (context.TryGetValue(MatchValuesKey, out obj)) + { + return obj as IDictionary; + } + + return null; + } + + public static void SetRouteMatchValues(this IDictionary context, IDictionary values) + { + context[MatchValuesKey] = values; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRoute.cs b/src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRoute.cs new file mode 100644 index 0000000000..8f17124b9f --- /dev/null +++ b/src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRoute.cs @@ -0,0 +1,29 @@ +using System; + +namespace Microsoft.AspNet.Routing.HttpMethod +{ + public class HttpMethodRoute : PrefixRoute + { + public HttpMethodRoute(IRouteEndpoint endpoint, string prefix, string method) + : base(endpoint, prefix) + { + this.Method = method; + } + + private string Method + { + get; + set; + } + + public override RouteMatch GetMatch(RoutingContext context) + { + if (String.Equals(context.RequestMethod, this.Method, StringComparison.OrdinalIgnoreCase)) + { + return base.GetMatch(context); + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRouteExtensions.cs b/src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRouteExtensions.cs new file mode 100644 index 0000000000..86c64510ca --- /dev/null +++ b/src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRouteExtensions.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Routing.Template; + +using AppFunc = System.Func, System.Threading.Tasks.Task>; + +namespace Microsoft.AspNet.Routing.HttpMethod +{ + public static class HttpMethodRouteExtensions + { + public static IRouteEndpoint Get(this IRouteEndpoint endpoint) + { + endpoint.AddRoute(null, new HttpMethodRoute(endpoint, null, "GET")); + return endpoint; + } + + public static IRouteEndpoint Get(this IRouteBuilder routeBuilder, AppFunc handler) + { + var endpoint = routeBuilder.ForApp((next) => handler); + return endpoint.Get(); + } + + public static IRouteEndpoint Get(this IRouteEndpoint endpoint, string prefix) + { + endpoint.AddRoute(null, new HttpMethodRoute(endpoint, prefix, "GET")); + return endpoint; + } + + public static IRouteEndpoint Get(this IRouteBuilder routeBuilder, string prefix, AppFunc handler) + { + var endpoint = routeBuilder.ForApp((next) => handler); + return endpoint.Get(prefix); + } + + public static IRouteEndpoint Post(this IRouteEndpoint endpoint) + { + endpoint.AddRoute(null, new HttpMethodRoute(endpoint, null, "POST")); + return endpoint; + } + + public static IRouteEndpoint Post(this IRouteBuilder routeBuilder, AppFunc handler) + { + var endpoint = routeBuilder.ForApp((next) => handler); + return endpoint.Post(); + } + + public static IRouteEndpoint Post(this IRouteEndpoint endpoint, string prefix) + { + endpoint.AddRoute(null, new HttpMethodRoute(endpoint, prefix, "POST")); + return endpoint; + } + + public static IRouteEndpoint Post(this IRouteBuilder routeBuilder, string prefix, AppFunc handler) + { + var endpoint = routeBuilder.ForApp((next) => handler); + return endpoint.Post(prefix); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/IConstraint.cs b/src/Microsoft.AspNet.Routing/IConstraint.cs new file mode 100644 index 0000000000..3190c4517b --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IConstraint.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing +{ + public interface IConstraint + { + bool MatchInbound(RoutingContext context, IDictionary values, object value); + + bool MatchOutbound(RouteBindingContext context, IDictionary values, object value); + } +} diff --git a/src/Microsoft.AspNet.Routing/IRoute.cs b/src/Microsoft.AspNet.Routing/IRoute.cs new file mode 100644 index 0000000000..6ba80191a7 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRoute.cs @@ -0,0 +1,12 @@ + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public interface IRoute + { + BoundRoute Bind(RouteBindingContext context); + + RouteMatch GetMatch(RoutingContext context); + } +} diff --git a/src/Microsoft.AspNet.Routing/IRouteBuilder.cs b/src/Microsoft.AspNet.Routing/IRouteBuilder.cs new file mode 100644 index 0000000000..ba5730d818 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRouteBuilder.cs @@ -0,0 +1,27 @@ + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +#if NET45 +using Owin; +#endif + +namespace Microsoft.AspNet.Routing +{ + public interface IRouteBuilder + { +#if NET45 + IAppBuilder AppBuilder + { + get; + } +#endif + IRouteEngine Engine + { + get; + } + + IRouteEndpoint ForApp(Func, Task>> handlerFactory); + } +} + diff --git a/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs b/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs new file mode 100644 index 0000000000..5ad2bb8753 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing +{ + public interface IRouteEndpoint + { + Func, Task> AppFunc + { + get; + } + + IRouteEndpoint AddRoute(string name, IRoute route); + } +} diff --git a/src/Microsoft.AspNet.Routing/IRouteEngine.cs b/src/Microsoft.AspNet.Routing/IRouteEngine.cs new file mode 100644 index 0000000000..8d7694722e --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRouteEngine.cs @@ -0,0 +1,12 @@ + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public interface IRouteEngine + { + RouteMatch GetMatch(IDictionary context); + + BoundRoute GetUrl(string routeName, IDictionary context, IDictionary values); + } +} diff --git a/src/Microsoft.AspNet.Routing/Lambda/LambdaRoute.cs b/src/Microsoft.AspNet.Routing/Lambda/LambdaRoute.cs new file mode 100644 index 0000000000..c3954970cd --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Lambda/LambdaRoute.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing.Lambda +{ + public class LambdaRoute : IRoute + { + public LambdaRoute(IRouteEndpoint endpoint, Func, bool> condition) + { + this.Endpoint = endpoint; + this.Condition = condition; + } + + private Func, bool> Condition + { + get; + set; + } + + private IRouteEndpoint Endpoint + { + get; + set; + } + + public BoundRoute Bind(RouteBindingContext context) + { + return null; + } + + public RouteMatch GetMatch(RoutingContext context) + { + if (Condition(context.Context)) + { + return new RouteMatch(this.Endpoint.AppFunc); + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Lambda/LambdaRouteExtensions.cs b/src/Microsoft.AspNet.Routing/Lambda/LambdaRouteExtensions.cs new file mode 100644 index 0000000000..cf0cb37ede --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Lambda/LambdaRouteExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using AppFunc = System.Func, System.Threading.Tasks.Task>; + +namespace Microsoft.AspNet.Routing.Lambda +{ + public static class LambdaRouteExtensions + { + public static IRouteEndpoint On(this IRouteEndpoint endpoint, Func, bool> condition) + { + endpoint.AddRoute(null, new LambdaRoute(endpoint, condition)); + return endpoint; + } + + public static IRouteEndpoint On(this IRouteBuilder routeBuilder, Func, bool> condition, AppFunc handler) + { + var endpoint = routeBuilder.ForApp(handler); + return endpoint.On(condition); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Legacy/BoundRouteTemplate.cs b/src/Microsoft.AspNet.Routing/Legacy/BoundRouteTemplate.cs new file mode 100644 index 0000000000..34003330b3 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Legacy/BoundRouteTemplate.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing.Legacy +{ + /// + /// Represents a URI generated from a . + /// + internal class BoundRouteTemplate + { + public string BoundTemplate { get; set; } + + public IDictionary Values { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Legacy/HttpParsedRoute.cs b/src/Microsoft.AspNet.Routing/Legacy/HttpParsedRoute.cs new file mode 100644 index 0000000000..52f114dd03 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Legacy/HttpParsedRoute.cs @@ -0,0 +1,821 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.AspNet.Routing.Legacy +{ + internal sealed class HttpParsedRoute + { + public HttpParsedRoute(List pathSegments) + { + Contract.Assert(pathSegments != null); + PathSegments = pathSegments; + + this.ParameterSegments = new List(); + + for (int i = 0; i < this.PathSegments.Count; i++) + { + var segment = this.PathSegments[i] as PathContentSegment; + if (segment != null) + { + for (int j = 0; j < segment.Subsegments.Count; j++) + { + var subsegment = segment.Subsegments[j] as PathParameterSubsegment; + if (subsegment != null) + { + this.ParameterSegments.Add(subsegment); + } + } + } + } + } + + public List PathSegments { get; private set; } + + public List ParameterSegments { get; private set; } + + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Not changing original algorithm")] + [SuppressMessage("Microsoft.Maintainability", "CA1505:AvoidUnmaintainableCode", Justification = "Not changing original algorithm")] + public BoundRouteTemplate Bind(RouteBindingContext context, IDictionary defaults) + { + IDictionary currentValues = context.AmbientValues; + IDictionary values = context.Values; + + IDictionary constraints = null; + + if (currentValues == null) + { + currentValues = new Dictionary(); + } + + if (values == null) + { + values = new Dictionary(); + } + + if (defaults == null) + { + defaults = new Dictionary(); + } + + // The set of values we should be using when generating the URI in this route + Dictionary acceptedValues = new Dictionary(); + + // Keep track of which new values have been used + HashSet unusedNewValues = new HashSet(values.Keys, StringComparer.OrdinalIgnoreCase); + + // Step 1: Get the list of values we're going to try to use to match and generate this URI + + // Find out which entries in the URI are valid for the URI we want to generate. + // If the URI had ordered parameters a="1", b="2", c="3" and the new values + // specified that b="9", then we need to invalidate everything after it. The new + // values should then be a="1", b="9", c=. + for (int i = 0; i < this.ParameterSegments.Count; i++) + { + PathParameterSubsegment parameter = this.ParameterSegments[i]; + + // If it's a parameter subsegment, examine the current value to see if it matches the new value + string parameterName = parameter.ParameterName; + + object newParameterValue; + bool hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue); + if (hasNewParameterValue) + { + unusedNewValues.Remove(parameterName); + } + + object currentParameterValue; + bool hasCurrentParameterValue = currentValues.TryGetValue(parameterName, out currentParameterValue); + + if (hasNewParameterValue && hasCurrentParameterValue) + { + if (!RoutePartsEqual(currentParameterValue, newParameterValue)) + { + // Stop copying current values when we find one that doesn't match + break; + } + } + + // If the parameter is a match, add it to the list of values we will use for URI generation + if (hasNewParameterValue) + { + if (IsRoutePartNonEmpty(newParameterValue)) + { + acceptedValues.Add(parameterName, newParameterValue); + } + } + else + { + if (hasCurrentParameterValue) + { + acceptedValues.Add(parameterName, currentParameterValue); + } + } + } + + + // Add all remaining new values to the list of values we will use for URI generation + foreach (var newValue in values) + { + if (IsRoutePartNonEmpty(newValue.Value)) + { + if (!acceptedValues.ContainsKey(newValue.Key)) + { + acceptedValues.Add(newValue.Key, newValue.Value); + } + } + } + + // Add all current values that aren't in the URI at all + foreach (var currentValue in currentValues) + { + string parameterName = currentValue.Key; + if (!acceptedValues.ContainsKey(parameterName)) + { + PathParameterSubsegment parameterSubsegment = GetParameterSubsegment(PathSegments, parameterName); + if (parameterSubsegment == null) + { + acceptedValues.Add(parameterName, currentValue.Value); + } + } + } + + // Add all remaining default values from the route to the list of values we will use for URI generation + for (int i = 0; i < this.ParameterSegments.Count; i++) + { + PathParameterSubsegment parameter = this.ParameterSegments[i]; + if (!acceptedValues.ContainsKey(parameter.ParameterName)) + { + object defaultValue; + if (!IsParameterRequired(parameter, defaults, out defaultValue)) + { + // Add the default value only if there isn't already a new value for it and + // only if it actually has a default value, which we determine based on whether + // the parameter value is required. + acceptedValues.Add(parameter.ParameterName, defaultValue); + } + } + } + + // All required parameters in this URI must have values from somewhere (i.e. the accepted values) + bool hasAllRequiredValues = true; + for (int i = 0; i < this.ParameterSegments.Count; i++) + { + PathParameterSubsegment parameter = this.ParameterSegments[i]; + + object defaultValue; + if (IsParameterRequired(parameter, defaults, out defaultValue)) + { + if (!acceptedValues.ContainsKey(parameter.ParameterName)) + { + // If the route parameter value is required that means there's + // no default value, so if there wasn't a new value for it + // either, this route won't match. + hasAllRequiredValues = false; + break; + } + } + } + + if (!hasAllRequiredValues) + { + return null; + } + + // All other default values must match if they are explicitly defined in the new values + Dictionary otherDefaultValues = new Dictionary(defaults, StringComparer.OrdinalIgnoreCase); + + for (int i = 0; i < this.ParameterSegments.Count; i++) + { + PathParameterSubsegment parameter = this.ParameterSegments[i]; + otherDefaultValues.Remove(parameter.ParameterName); + } + + foreach (var defaultValue in otherDefaultValues) + { + object value; + if (values.TryGetValue(defaultValue.Key, out value)) + { + unusedNewValues.Remove(defaultValue.Key); + if (!RoutePartsEqual(value, defaultValue.Value)) + { + // If there is a non-parameterized value in the route and there is a + // new value for it and it doesn't match, this route won't match. + return null; + } + } + } + + // Step 2: If the route is a match generate the appropriate URI + + StringBuilder uri = new StringBuilder(); + StringBuilder pendingParts = new StringBuilder(); + + bool pendingPartsAreAllSafe = false; + bool blockAllUriAppends = false; + + for (int i = 0; i < PathSegments.Count; i++) + { + PathSegment pathSegment = PathSegments[i]; // parsedRouteUriPart + + if (pathSegment is PathSeparatorSegment) + { + if (pendingPartsAreAllSafe) + { + // Accept + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + pendingParts.Length = 0; + } + } + pendingPartsAreAllSafe = false; + + // Guard against appending multiple separators for empty segments + if (pendingParts.Length > 0 && pendingParts[pendingParts.Length - 1] == '/') + { + // Dev10 676725: Route should not be matched if that causes mismatched tokens + // Dev11 86819: We will allow empty matches if all subsequent segments are null + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI (without the trailing slash) and prevent any future appends + uri.Append(pendingParts.ToString(0, pendingParts.Length - 1)); + pendingParts.Length = 0; + blockAllUriAppends = true; + } + else + { + pendingParts.Append("/"); + } + } + else + { + PathContentSegment contentPathSegment = pathSegment as PathContentSegment; + if (contentPathSegment != null) + { + // Segments are treated as all-or-none. We should never output a partial segment. + // If we add any subsegment of this segment to the generated URI, we have to add + // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we + // used a value for {p1}, we have to output the entire segment up to the next "/". + // Otherwise we could end up with the partial segment "v1" instead of the entire + // segment "v1-v2.xml". + bool addedAnySubsegments = false; + + foreach (PathSubsegment subsegment in contentPathSegment.Subsegments) + { + PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment; + if (literalSubsegment != null) + { + // If it's a literal we hold on to it until we are sure we need to add it + pendingPartsAreAllSafe = true; + pendingParts.Append(literalSubsegment.Literal); + } + else + { + PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment; + if (parameterSubsegment != null) + { + if (pendingPartsAreAllSafe) + { + // Accept + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + pendingParts.Length = 0; + + addedAnySubsegments = true; + } + } + pendingPartsAreAllSafe = false; + + // If it's a parameter, get its value + object acceptedParameterValue; + bool hasAcceptedParameterValue = acceptedValues.TryGetValue(parameterSubsegment.ParameterName, out acceptedParameterValue); + if (hasAcceptedParameterValue) + { + unusedNewValues.Remove(parameterSubsegment.ParameterName); + } + + object defaultParameterValue; + defaults.TryGetValue(parameterSubsegment.ParameterName, out defaultParameterValue); + + if (RoutePartsEqual(acceptedParameterValue, defaultParameterValue)) + { + // If the accepted value is the same as the default value, mark it as pending since + // we won't necessarily add it to the URI we generate. + pendingParts.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture)); + } + else + { + if (blockAllUriAppends) + { + return null; + } + + // Add the new part to the URI as well as any pending parts + if (pendingParts.Length > 0) + { + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + pendingParts.Length = 0; + } + uri.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture)); + + addedAnySubsegments = true; + } + } + else + { + Contract.Assert(false, "Invalid path subsegment type"); + } + } + } + + if (addedAnySubsegments) + { + // See comment above about why we add the pending parts + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + pendingParts.Length = 0; + } + } + } + else + { + Contract.Assert(false, "Invalid path segment type"); + } + } + } + + if (pendingPartsAreAllSafe) + { + // Accept + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + } + } + + // Process constraints keys + if (constraints != null) + { + // If there are any constraints, mark all the keys as being used so that we don't + // generate query string items for custom constraints that don't appear as parameters + // in the URI format. + foreach (var constraintsItem in constraints) + { + unusedNewValues.Remove(constraintsItem.Key); + } + } + + // Encode the URI before we append the query string, otherwise we would double encode the query string + StringBuilder encodedUri = new StringBuilder(); + encodedUri.Append(UriEncode(uri.ToString())); + uri = encodedUri; + + // Add remaining new values as query string parameters to the URI + if (unusedNewValues.Count > 0) + { + // Generate the query string + bool firstParam = true; + foreach (string unusedNewValue in unusedNewValues) + { + object value; + if (acceptedValues.TryGetValue(unusedNewValue, out value)) + { + uri.Append(firstParam ? '?' : '&'); + firstParam = false; + uri.Append(Uri.EscapeDataString(unusedNewValue)); + uri.Append('='); + uri.Append(Uri.EscapeDataString(Convert.ToString(value, CultureInfo.InvariantCulture))); + } + } + } + + return new BoundRouteTemplate() + { + BoundTemplate = uri.ToString(), + Values = acceptedValues + }; + } + + private static string EscapeReservedCharacters(Match m) + { +#if NET45 + return Uri.HexEscape(m.Value[0]); +#else + return m.Value; +#endif + } + + private PathParameterSubsegment GetParameterSubsegment(IList pathSegments, string parameterName) + { + for (int i = 0; i < this.ParameterSegments.Count; i++) + { + PathParameterSubsegment parameter = this.ParameterSegments[i]; + if (String.Equals(parameterName, parameter.ParameterName, StringComparison.OrdinalIgnoreCase)) + { + return parameter; + } + } + + return null; + } + + private static bool IsParameterRequired(PathParameterSubsegment parameterSubsegment, IDictionary defaultValues, out object defaultValue) + { + if (parameterSubsegment.IsCatchAll) + { + defaultValue = null; + return false; + } + + return !defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultValue); + } + + private static bool IsRoutePartNonEmpty(object routePart) + { + string routePartString = routePart as string; + if (routePartString != null) + { + return routePartString.Length > 0; + } + return routePart != null; + } + + public IDictionary Match(RoutingContext context, IDictionary defaultValues) + { + List requestPathSegments = RouteParser.SplitUriToPathSegmentStrings(context.RequestPath.Substring(1)); + + if (defaultValues == null) + { + defaultValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + Dictionary matchedValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // This flag gets set once all the data in the URI has been parsed through, but + // the route we're trying to match against still has more parts. At this point + // we'll only continue matching separator characters and parameters that have + // default values. + bool ranOutOfStuffToParse = false; + + // This value gets set once we start processing a catchall parameter (if there is one + // at all). Once we set this value we consume all remaining parts of the URI into its + // parameter value. + bool usedCatchAllParameter = false; + + for (int i = 0; i < PathSegments.Count; i++) + { + PathSegment pathSegment = PathSegments[i]; + + if (requestPathSegments.Count <= i) + { + ranOutOfStuffToParse = true; + } + + string requestPathSegment = ranOutOfStuffToParse ? null : requestPathSegments[i]; + + if (pathSegment is PathSeparatorSegment) + { + if (ranOutOfStuffToParse) + { + // If we're trying to match a separator in the route but there's no more content, that's OK + } + else + { + if (!String.Equals(requestPathSegment, "/", StringComparison.Ordinal)) + { + return null; + } + } + } + else + { + PathContentSegment contentPathSegment = pathSegment as PathContentSegment; + if (contentPathSegment != null) + { + if (contentPathSegment.IsCatchAll) + { + Contract.Assert(i == (PathSegments.Count - 1), "If we're processing a catch-all, we should be on the last route segment."); + MatchCatchAll(contentPathSegment, requestPathSegments.Skip(i), defaultValues, matchedValues); + usedCatchAllParameter = true; + } + else + { + if (!MatchContentPathSegment(contentPathSegment, requestPathSegment, defaultValues, matchedValues)) + { + return null; + } + } + } + else + { + Contract.Assert(false, "Invalid path segment type"); + } + } + } + + if (!usedCatchAllParameter) + { + if (PathSegments.Count < requestPathSegments.Count) + { + // If we've already gone through all the parts defined in the route but the URI + // still contains more content, check that the remaining content is all separators. + for (int i = PathSegments.Count; i < requestPathSegments.Count; i++) + { + if (!RouteParser.IsSeparator(requestPathSegments[i])) + { + return null; + } + } + } + } + + // Copy all remaining default values to the route data + if (defaultValues != null) + { + foreach (var defaultValue in defaultValues) + { + if (!matchedValues.ContainsKey(defaultValue.Key)) + { + matchedValues.Add(defaultValue.Key, defaultValue.Value); + } + } + } + + return matchedValues; + } + + private static void MatchCatchAll(PathContentSegment contentPathSegment, IEnumerable remainingRequestSegments, IDictionary defaultValues, IDictionary matchedValues) + { + string remainingRequest = String.Join(String.Empty, remainingRequestSegments.ToArray()); + + PathParameterSubsegment catchAllSegment = contentPathSegment.Subsegments[0] as PathParameterSubsegment; + + object catchAllValue; + + if (remainingRequest.Length > 0) + { + catchAllValue = remainingRequest; + } + else + { + defaultValues.TryGetValue(catchAllSegment.ParameterName, out catchAllValue); + } + + matchedValues.Add(catchAllSegment.ParameterName, catchAllValue); + } + + private static bool MatchContentPathSegment(PathContentSegment routeSegment, string requestPathSegment, IDictionary defaultValues, IDictionary matchedValues) + { + if (String.IsNullOrEmpty(requestPathSegment)) + { + // If there's no data to parse, we must have exactly one parameter segment and no other segments - otherwise no match + + if (routeSegment.Subsegments.Count > 1) + { + return false; + } + + PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[0] as PathParameterSubsegment; + if (parameterSubsegment == null) + { + return false; + } + + // We must have a default value since there's no value in the request URI + object parameterValue; + if (defaultValues.TryGetValue(parameterSubsegment.ParameterName, out parameterValue)) + { + // If there's a default value for this parameter, use that default value + matchedValues.Add(parameterSubsegment.ParameterName, parameterValue); + return true; + } + else + { + // If there's no default value, this segment doesn't match + return false; + } + } + + // Optimize for the common case where there is only one subsegment in the segment - either a parameter or a literal + if (routeSegment.Subsegments.Count == 1) + { + return MatchSingleContentPathSegment(routeSegment.Subsegments[0], requestPathSegment, matchedValues); + } + + // Find last literal segment and get its last index in the string + + int lastIndex = requestPathSegment.Length; + int indexOfLastSegmentUsed = routeSegment.Subsegments.Count - 1; + + PathParameterSubsegment parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value + PathLiteralSubsegment lastLiteral = null; // Keeps track of the left-most literal we've encountered + + while (indexOfLastSegmentUsed >= 0) + { + int newLastIndex = lastIndex; + + PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathParameterSubsegment; + if (parameterSubsegment != null) + { + // Hold on to the parameter so that we can fill it in when we locate the next literal + parameterNeedsValue = parameterSubsegment; + } + else + { + PathLiteralSubsegment literalSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathLiteralSubsegment; + if (literalSubsegment != null) + { + lastLiteral = literalSubsegment; + + int startIndex = lastIndex - 1; + // If we have a pending parameter subsegment, we must leave at least one character for that + if (parameterNeedsValue != null) + { + startIndex--; + } + + if (startIndex < 0) + { + return false; + } + + int indexOfLiteral = requestPathSegment.LastIndexOf(literalSubsegment.Literal, startIndex, StringComparison.OrdinalIgnoreCase); + if (indexOfLiteral == -1) + { + // If we couldn't find this literal index, this segment cannot match + return false; + } + + // If the first subsegment is a literal, it must match at the right-most extent of the request URI. + // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/". + // This check is related to the check we do at the very end of this function. + if (indexOfLastSegmentUsed == (routeSegment.Subsegments.Count - 1)) + { + if ((indexOfLiteral + literalSubsegment.Literal.Length) != requestPathSegment.Length) + { + return false; + } + } + + newLastIndex = indexOfLiteral; + } + else + { + Contract.Assert(false, "Invalid path segment type"); + } + } + + if ((parameterNeedsValue != null) && (((lastLiteral != null) && (parameterSubsegment == null)) || (indexOfLastSegmentUsed == 0))) + { + // If we have a pending parameter that needs a value, grab that value + + int parameterStartIndex; + int parameterTextLength; + + if (lastLiteral == null) + { + if (indexOfLastSegmentUsed == 0) + { + parameterStartIndex = 0; + } + else + { + parameterStartIndex = newLastIndex; + Contract.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above"); + } + parameterTextLength = lastIndex; + } + else + { + // If we're getting a value for a parameter that is somewhere in the middle of the segment + if ((indexOfLastSegmentUsed == 0) && (parameterSubsegment != null)) + { + parameterStartIndex = 0; + parameterTextLength = lastIndex; + } + else + { + parameterStartIndex = newLastIndex + lastLiteral.Literal.Length; + parameterTextLength = lastIndex - parameterStartIndex; + } + } + + string parameterValueString = requestPathSegment.Substring(parameterStartIndex, parameterTextLength); + + if (String.IsNullOrEmpty(parameterValueString)) + { + // If we're here that means we have a segment that contains multiple sub-segments. + // For these segments all parameters must have non-empty values. If the parameter + // has an empty value it's not a match. + return false; + } + else + { + // If there's a value in the segment for this parameter, use the subsegment value + matchedValues.Add(parameterNeedsValue.ParameterName, parameterValueString); + } + + parameterNeedsValue = null; + lastLiteral = null; + } + + lastIndex = newLastIndex; + indexOfLastSegmentUsed--; + } + + // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of + // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment + // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching + // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire* + // request URI in order for it to be a match. + // This check is related to the check we do earlier in this function for LiteralSubsegments. + return (lastIndex == 0) || (routeSegment.Subsegments[0] is PathParameterSubsegment); + } + + private static bool MatchSingleContentPathSegment(PathSubsegment pathSubsegment, string requestPathSegment, IDictionary matchedValues) + { + PathParameterSubsegment parameterSubsegment = pathSubsegment as PathParameterSubsegment; + if (parameterSubsegment == null) + { + // Handle a single literal segment + PathLiteralSubsegment literalSubsegment = pathSubsegment as PathLiteralSubsegment; + Contract.Assert(literalSubsegment != null, "Invalid path segment type"); + return literalSubsegment.Literal.Equals(requestPathSegment, StringComparison.OrdinalIgnoreCase); + } + else + { + // Handle a single parameter segment + matchedValues.Add(parameterSubsegment.ParameterName, requestPathSegment); + return true; + } + } + + private static bool RoutePartsEqual(object a, object b) + { + string sa = a as string; + string sb = b as string; + if (sa != null && sb != null) + { + // For strings do a case-insensitive comparison + return String.Equals(sa, sb, StringComparison.OrdinalIgnoreCase); + } + else + { + if (a != null && b != null) + { + // Explicitly call .Equals() in case it is overridden in the type + return a.Equals(b); + } + else + { + // At least one of them is null. Return true if they both are + return a == b; + } + } + } + + private static string UriEncode(string str) + { + string escape = Uri.EscapeUriString(str); + return Regex.Replace(escape, "([#?])", new MatchEvaluator(EscapeReservedCharacters)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Legacy/HttpRouteValueDictionary.cs b/src/Microsoft.AspNet.Routing/Legacy/HttpRouteValueDictionary.cs new file mode 100644 index 0000000000..f6109cbe33 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Legacy/HttpRouteValueDictionary.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +#if NET45 +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.Routing.Legacy +{ + [SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable", Justification = "This class will never be serialized.")] + public class HttpRouteValueDictionary : Dictionary + { + public HttpRouteValueDictionary() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + public HttpRouteValueDictionary(IDictionary dictionary) + : base(StringComparer.OrdinalIgnoreCase) + { + if (dictionary != null) + { + foreach (KeyValuePair current in dictionary) + { + Add(current.Key, current.Value); + } + } + } + + public HttpRouteValueDictionary(object values) + : base(StringComparer.OrdinalIgnoreCase) + { + IDictionary valuesAsDictionary = values as IDictionary; + if (valuesAsDictionary != null) + { + foreach (KeyValuePair current in valuesAsDictionary) + { + Add(current.Key, current.Value); + } + } + else if (values != null) + { + foreach (PropertyHelper property in PropertyHelper.GetProperties(values)) + { + // Extract the property values from the property helper + // The advantage here is that the property helper caches fast accessors. + Add(property.Name, property.GetValue(values)); + } + } + } + } +} +#endif diff --git a/src/Microsoft.AspNet.Routing/Legacy/PathSegment.cs b/src/Microsoft.AspNet.Routing/Legacy/PathSegment.cs new file mode 100644 index 0000000000..db9605b06e --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Legacy/PathSegment.cs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.Routing.Legacy +{ + // Represents a segment of a URI such as a separator or content + internal abstract class PathSegment + { +#if ROUTE_DEBUGGING + public abstract string LiteralText + { + get; + } +#endif + } + + // Represents a segment of a URI that is not a separator. It contains subsegments such as literals and parameters. + internal sealed class PathContentSegment : PathSegment + { + public PathContentSegment(List subsegments) + { + Subsegments = subsegments; + } + + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm.")] + public bool IsCatchAll + { + get + { + // TODO: Verify this is correct. Maybe add an assert. + // Performance sensitive + // Caching count is faster for IList + int subsegmentCount = Subsegments.Count; + for (int i = 0; i < subsegmentCount; i++) + { + PathSubsegment seg = Subsegments[i]; + PathParameterSubsegment paramterSubSegment = seg as PathParameterSubsegment; + if (paramterSubSegment != null && paramterSubSegment.IsCatchAll) + { + return true; + } + } + return false; + } + } + + public List Subsegments { get; private set; } + +#if ROUTE_DEBUGGING + public override string LiteralText + { + get + { + List s = new List(); + foreach (PathSubsegment subsegment in Subsegments) + { + s.Add(subsegment.LiteralText); + } + return String.Join(String.Empty, s.ToArray()); + } + } + + public override string ToString() + { + List s = new List(); + foreach (PathSubsegment subsegment in Subsegments) + { + s.Add(subsegment.ToString()); + } + return "[ " + String.Join(", ", s.ToArray()) + " ]"; + } +#endif + } + + // Represents a literal subsegment of a ContentPathSegment + internal sealed class PathLiteralSubsegment : PathSubsegment + { + public PathLiteralSubsegment(string literal) + { + Literal = literal; + } + + public string Literal { get; private set; } + +#if ROUTE_DEBUGGING + public override string LiteralText + { + get + { + return Literal; + } + } + + public override string ToString() + { + return "\"" + Literal + "\""; + } +#endif + } + + // Represents a parameter subsegment of a ContentPathSegment + internal sealed class PathParameterSubsegment : PathSubsegment + { + public PathParameterSubsegment(string parameterName) + { + if (parameterName.StartsWith("*", StringComparison.Ordinal)) + { + ParameterName = parameterName.Substring(1); + IsCatchAll = true; + } + else + { + ParameterName = parameterName; + } + } + + public bool IsCatchAll { get; private set; } + + public string ParameterName { get; private set; } + +#if ROUTE_DEBUGGING + public override string LiteralText + { + get + { + return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}"; + } + } + + public override string ToString() + { + return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}"; + } +#endif + } + + // Represents a "/" separator in a URI + internal sealed class PathSeparatorSegment : PathSegment + { +#if ROUTE_DEBUGGING + public override string LiteralText + { + get + { + return "/"; + } + } + + public override string ToString() + { + return "\"/\""; + } +#endif + } + + // Represents a subsegment of a ContentPathSegment such as a parameter or a literal. + internal abstract class PathSubsegment + { +#if ROUTE_DEBUGGING + public abstract string LiteralText + { + get; + } +#endif + } +} diff --git a/src/Microsoft.AspNet.Routing/Legacy/PropertyHelper.cs b/src/Microsoft.AspNet.Routing/Legacy/PropertyHelper.cs new file mode 100644 index 0000000000..ba18b342d6 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Legacy/PropertyHelper.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +#if NET45 +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Reflection; + +namespace Microsoft.AspNet.Routing.Legacy +{ + internal class PropertyHelper + { + private static ConcurrentDictionary _reflectionCache = new ConcurrentDictionary(); + + private Func _valueGetter; + + /// + /// Initializes a fast property helper. This constructor does not cache the helper. + /// + [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "This is intended the Name is auto set differently per type and the type is internal")] + public PropertyHelper(PropertyInfo property) + { + Contract.Assert(property != null); + + Name = property.Name; + _valueGetter = MakeFastPropertyGetter(property); + } + + /// + /// Creates a single fast property setter. The result is not cached. + /// + /// propertyInfo to extract the getter for. + /// a fast setter. + /// This method is more memory efficient than a dynamically compiled lambda, and about the same speed. + public static Action MakeFastPropertySetter(PropertyInfo propertyInfo) + where TDeclaringType : class + { + Contract.Assert(propertyInfo != null); + + MethodInfo setMethod = propertyInfo.GetSetMethod(); + + Contract.Assert(setMethod != null); + Contract.Assert(!setMethod.IsStatic); + Contract.Assert(setMethod.GetParameters().Length == 1); + Contract.Assert(!propertyInfo.ReflectedType.IsValueType); + + // Instance methods in the CLR can be turned into static methods where the first parameter + // is open over "this". This parameter is always passed by reference, so we have a code + // path for value types and a code path for reference types. + Type typeInput = propertyInfo.ReflectedType; + Type typeValue = setMethod.GetParameters()[0].ParameterType; + + Delegate callPropertySetterDelegate; + + // Create a delegate TValue -> "TDeclaringType.Property" + var propertySetterAsAction = setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(typeInput, typeValue)); + var callPropertySetterClosedGenericMethod = _callPropertySetterOpenGenericMethod.MakeGenericMethod(typeInput, typeValue); + callPropertySetterDelegate = Delegate.CreateDelegate(typeof(Action), propertySetterAsAction, callPropertySetterClosedGenericMethod); + + return (Action)callPropertySetterDelegate; + } + + public virtual string Name { get; protected set; } + + public object GetValue(object instance) + { + Contract.Assert(_valueGetter != null, "Must call Initialize before using this object"); + + return _valueGetter(instance); + } + + /// + /// Creates and caches fast property helpers that expose getters for every public get property on the underlying type. + /// + /// the instance to extract property accessors for. + /// a cached array of all public property getters from the underlying type of this instance. + public static PropertyHelper[] GetProperties(object instance) + { + return GetProperties(instance, CreateInstance, _reflectionCache); + } + + /// + /// Creates a single fast property getter. The result is not cached. + /// + /// propertyInfo to extract the getter for. + /// a fast getter. + /// This method is more memory efficient than a dynamically compiled lambda, and about the same speed. + public static Func MakeFastPropertyGetter(PropertyInfo propertyInfo) + { + Contract.Assert(propertyInfo != null); + + MethodInfo getMethod = propertyInfo.GetGetMethod(); + Contract.Assert(getMethod != null); + Contract.Assert(!getMethod.IsStatic); + Contract.Assert(getMethod.GetParameters().Length == 0); + + // Instance methods in the CLR can be turned into static methods where the first parameter + // is open over "this". This parameter is always passed by reference, so we have a code + // path for value types and a code path for reference types. + Type typeInput = getMethod.ReflectedType; + Type typeOutput = getMethod.ReturnType; + + Delegate callPropertyGetterDelegate; + if (typeInput.IsValueType) + { + // Create a delegate (ref TDeclaringType) -> TValue + Delegate propertyGetterAsFunc = getMethod.CreateDelegate(typeof(ByRefFunc<,>).MakeGenericType(typeInput, typeOutput)); + MethodInfo callPropertyGetterClosedGenericMethod = _callPropertyGetterByReferenceOpenGenericMethod.MakeGenericMethod(typeInput, typeOutput); + callPropertyGetterDelegate = Delegate.CreateDelegate(typeof(Func), propertyGetterAsFunc, callPropertyGetterClosedGenericMethod); + } + else + { + // Create a delegate TDeclaringType -> TValue + Delegate propertyGetterAsFunc = getMethod.CreateDelegate(typeof(Func<,>).MakeGenericType(typeInput, typeOutput)); + MethodInfo callPropertyGetterClosedGenericMethod = _callPropertyGetterOpenGenericMethod.MakeGenericMethod(typeInput, typeOutput); + callPropertyGetterDelegate = Delegate.CreateDelegate(typeof(Func), propertyGetterAsFunc, callPropertyGetterClosedGenericMethod); + } + + return (Func)callPropertyGetterDelegate; + } + + private static PropertyHelper CreateInstance(PropertyInfo property) + { + return new PropertyHelper(property); + } + + // Implementation of the fast getter. + private delegate TValue ByRefFunc(ref TDeclaringType arg); + + private static readonly MethodInfo _callPropertyGetterOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertyGetter", BindingFlags.NonPublic | BindingFlags.Static); + private static readonly MethodInfo _callPropertyGetterByReferenceOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertyGetterByReference", BindingFlags.NonPublic | BindingFlags.Static); + + private static object CallPropertyGetter(Func getter, object @this) + { + return getter((TDeclaringType)@this); + } + + private static object CallPropertyGetterByReference(ByRefFunc getter, object @this) + { + TDeclaringType unboxed = (TDeclaringType)@this; + return getter(ref unboxed); + } + + // Implementation of the fast setter. + private static readonly MethodInfo _callPropertySetterOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertySetter", BindingFlags.NonPublic | BindingFlags.Static); + + private static void CallPropertySetter(Action setter, object @this, object value) + { + setter((TDeclaringType)@this, (TValue)value); + } + + protected static PropertyHelper[] GetProperties(object instance, + Func createPropertyHelper, + ConcurrentDictionary cache) + { + // Using an array rather than IEnumerable, as this will be called on the hot path numerous times. + PropertyHelper[] helpers; + + Type type = instance.GetType(); + + if (!cache.TryGetValue(type, out helpers)) + { + // We avoid loading indexed properties using the where statement. + // Indexed properties are not useful (or valid) for grabbing properties off an anonymous object. + IEnumerable properties = type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance) + .Where(prop => prop.GetIndexParameters().Length == 0 && + prop.GetMethod != null); + + var newHelpers = new List(); + + foreach (PropertyInfo property in properties) + { + PropertyHelper propertyHelper = createPropertyHelper(property); + + newHelpers.Add(propertyHelper); + } + + helpers = newHelpers.ToArray(); + cache.TryAdd(type, helpers); + } + + return helpers; + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Legacy/RouteParser.cs b/src/Microsoft.AspNet.Routing/Legacy/RouteParser.cs new file mode 100644 index 0000000000..bfdce5aa83 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Legacy/RouteParser.cs @@ -0,0 +1,377 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +#define ASPNETWEBAPI + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Linq; + +#if ASPNETWEBAPI +using TParsedRoute = Microsoft.AspNet.Routing.Legacy.HttpParsedRoute; +#else +using ErrorResources = System.Web.Mvc.Properties.MvcResources; +using TParsedRoute = System.Web.Mvc.Routing.ParsedRoute; +#endif + +#if ASPNETWEBAPI +namespace Microsoft.AspNet.Routing.Legacy +#else +namespace System.Web.Mvc.Routing +#endif +{ + // in the MVC case, route parsing is done for AttributeRouting's sake, so that + // it could order the discovered routes before pushing them into the routeCollection, + // where, unfortunately, they would be parsed again. + internal static class RouteParser + { + private static string GetLiteral(string segmentLiteral) + { + // Scan for errant single { and } and convert double {{ to { and double }} to } + + // First we eliminate all escaped braces and then check if any other braces are remaining + string newLiteral = segmentLiteral.Replace("{{", String.Empty).Replace("}}", String.Empty); + if (newLiteral.Contains("{") || newLiteral.Contains("}")) + { + return null; + } + + // If it's a valid format, we unescape the braces + return segmentLiteral.Replace("{{", "{").Replace("}}", "}"); + } + + private static int IndexOfFirstOpenParameter(string segment, int startIndex) + { + // Find the first unescaped open brace + while (true) + { + startIndex = segment.IndexOf('{', startIndex); + if (startIndex == -1) + { + // If there are no more open braces, stop + return -1; + } + if ((startIndex + 1 == segment.Length) || + ((startIndex + 1 < segment.Length) && (segment[startIndex + 1] != '{'))) + { + // If we found an open brace that is followed by a non-open brace, it's + // a parameter delimiter. + // It's also a delimiter if the open brace is the last character - though + // it ends up being being called out as invalid later on. + return startIndex; + } + // Increment by two since we want to skip both the open brace that + // we're on as well as the subsequent character since we know for + // sure that it is part of an escape sequence. + startIndex += 2; + } + } + + internal static bool IsSeparator(string s) + { + return String.Equals(s, "/", StringComparison.Ordinal); + } + + private static bool IsValidParameterName(string parameterName) + { + if (parameterName.Length == 0) + { + return false; + } + + for (int i = 0; i < parameterName.Length; i++) + { + char c = parameterName[i]; + if (c == '/' || c == '{' || c == '}') + { + return false; + } + } + + return true; + } + + internal static bool IsInvalidRouteTemplate(string routeTemplate) + { + return routeTemplate.StartsWith("~", StringComparison.Ordinal) || + routeTemplate.StartsWith("/", StringComparison.Ordinal) || + (routeTemplate.IndexOf('?') != -1); + } + + public static TParsedRoute Parse(string routeTemplate) + { + if (routeTemplate == null) + { + routeTemplate = String.Empty; + } + + if (IsInvalidRouteTemplate(routeTemplate)) + { + throw new ArgumentException(); + } + + IList uriParts = SplitUriToPathSegmentStrings(routeTemplate); + Exception ex = ValidateUriParts(uriParts); + if (ex != null) + { + throw ex; + } + + List pathSegments = SplitUriToPathSegments(uriParts); + + Contract.Assert(uriParts.Count == pathSegments.Count, "The number of string segments should be the same as the number of path segments"); + + return new TParsedRoute(pathSegments); + } + + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", + Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] + private static List ParseUriSegment(string segment, out Exception exception) + { + int startIndex = 0; + + List pathSubsegments = new List(); + + while (startIndex < segment.Length) + { + int nextParameterStart = IndexOfFirstOpenParameter(segment, startIndex); + if (nextParameterStart == -1) + { + // If there are no more parameters in the segment, capture the remainder as a literal and stop + string lastLiteralPart = GetLiteral(segment.Substring(startIndex)); + if (lastLiteralPart == null) + { + exception = new ArgumentException(); + return null; + } + + if (lastLiteralPart.Length > 0) + { + pathSubsegments.Add(new PathLiteralSubsegment(lastLiteralPart)); + } + break; + } + + int nextParameterEnd = segment.IndexOf('}', nextParameterStart + 1); + if (nextParameterEnd == -1) + { + exception = new ArgumentException(); + return null; + } + + string literalPart = GetLiteral(segment.Substring(startIndex, nextParameterStart - startIndex)); + if (literalPart == null) + { + exception = new ArgumentException(); + return null; + } + + if (literalPart.Length > 0) + { + pathSubsegments.Add(new PathLiteralSubsegment(literalPart)); + } + + string parameterName = segment.Substring(nextParameterStart + 1, nextParameterEnd - nextParameterStart - 1); + pathSubsegments.Add(new PathParameterSubsegment(parameterName)); + + startIndex = nextParameterEnd + 1; + } + + exception = null; + return pathSubsegments; + } + + private static List SplitUriToPathSegments(IList uriParts) + { + List pathSegments = new List(); + + foreach (string pathSegment in uriParts) + { + bool isCurrentPartSeparator = IsSeparator(pathSegment); + if (isCurrentPartSeparator) + { + pathSegments.Add(new PathSeparatorSegment()); + } + else + { + Exception exception; + List subsegments = ParseUriSegment(pathSegment, out exception); + Contract.Assert(exception == null, "This only gets called after the path has been validated, so there should never be an exception here"); + pathSegments.Add(new PathContentSegment(subsegments)); + } + } + return pathSegments; + } + + internal static List SplitUriToPathSegmentStrings(string uri) + { + List parts = new List(); + + if (String.IsNullOrEmpty(uri)) + { + return parts; + } + + int currentIndex = 0; + + // Split the incoming URI into individual parts + while (currentIndex < uri.Length) + { + int indexOfNextSeparator = uri.IndexOf('/', currentIndex); + if (indexOfNextSeparator == -1) + { + // If there are no more separators, the rest of the string is the last part + string finalPart = uri.Substring(currentIndex); + if (finalPart.Length > 0) + { + parts.Add(finalPart); + } + break; + } + + string nextPart = uri.Substring(currentIndex, indexOfNextSeparator - currentIndex); + if (nextPart.Length > 0) + { + parts.Add(nextPart); + } + + Contract.Assert(uri[indexOfNextSeparator] == '/', "The separator char itself should always be a '/'."); + parts.Add("/"); + currentIndex = indexOfNextSeparator + 1; + } + + return parts; + } + + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm")] + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", + Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] + private static Exception ValidateUriParts(IList pathSegments) + { + Contract.Assert(pathSegments != null, "The value should always come from SplitUri(), and that function should never return null."); + + HashSet usedParameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); + bool? isPreviousPartSeparator = null; + + bool foundCatchAllParameter = false; + + foreach (string pathSegment in pathSegments) + { + if (foundCatchAllParameter) + { + // If we ever start an iteration of the loop and we've already found a + // catchall parameter then we have an invalid URI format. + return new ArgumentException(); + } + + bool isCurrentPartSeparator; + if (isPreviousPartSeparator == null) + { + // Prime the loop with the first value + isPreviousPartSeparator = IsSeparator(pathSegment); + isCurrentPartSeparator = isPreviousPartSeparator.Value; + } + else + { + isCurrentPartSeparator = IsSeparator(pathSegment); + + // If both the previous part and the current part are separators, it's invalid + if (isCurrentPartSeparator && isPreviousPartSeparator.Value) + { + return new ArgumentException(); + } + + Contract.Assert(isCurrentPartSeparator != isPreviousPartSeparator.Value, "This assert should only happen if both the current and previous parts are non-separators. This should never happen because consecutive non-separators are always parsed as a single part."); + isPreviousPartSeparator = isCurrentPartSeparator; + } + + // If it's not a separator, parse the segment for parameters and validate it + if (!isCurrentPartSeparator) + { + Exception exception; + IList subsegments = ParseUriSegment(pathSegment, out exception); + if (exception != null) + { + return exception; + } + + exception = ValidateUriSegment(subsegments, usedParameterNames); + if (exception != null) + { + return exception; + } + + foundCatchAllParameter = subsegments.Any(seg => (seg is PathParameterSubsegment) && ((PathParameterSubsegment)seg).IsCatchAll); + } + } + return null; + } + + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", + Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] + private static Exception ValidateUriSegment(IList pathSubsegments, HashSet usedParameterNames) + { + bool segmentContainsCatchAll = false; + + Type previousSegmentType = null; + + foreach (PathSubsegment subsegment in pathSubsegments) + { + if (previousSegmentType != null) + { + if (previousSegmentType == subsegment.GetType()) + { + return new ArgumentException(); + } + } + previousSegmentType = subsegment.GetType(); + + PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment; + if (literalSubsegment != null) + { + // Nothing to validate for literals - everything is valid + } + else + { + PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment; + if (parameterSubsegment != null) + { + string parameterName = parameterSubsegment.ParameterName; + + if (parameterSubsegment.IsCatchAll) + { + segmentContainsCatchAll = true; + } + + // Check for valid characters in the parameter name + if (!IsValidParameterName(parameterName)) + { + return new ArgumentException(); + } + + if (usedParameterNames.Contains(parameterName)) + { + return new ArgumentException(); + } + else + { + usedParameterNames.Add(parameterName); + } + } + else + { + Contract.Assert(false, "Invalid path subsegment type"); + } + } + } + + if (segmentContainsCatchAll && (pathSubsegments.Count != 1)) + { + return new ArgumentException(); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs b/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs new file mode 100644 index 0000000000..03e32fcd9e --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs @@ -0,0 +1,22 @@ + +#if NET45 + +using Owin; + +namespace Microsoft.AspNet.Routing.Owin +{ + public static class AppBuilderExtensions + { + public static IRouteBuilder UseRouter(this IAppBuilder builder) + { + var routes = new RouteTable(); + var engine = new RouteEngine(routes); + + var next = builder.Use(typeof(RouterMiddleware), engine, routes); + + return new RouteBuilder(next, engine, routes); + } + } +} + +#endif diff --git a/src/Microsoft.AspNet.Routing/Owin/AppFuncMiddleware.cs b/src/Microsoft.AspNet.Routing/Owin/AppFuncMiddleware.cs new file mode 100644 index 0000000000..fd555067bf --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Owin/AppFuncMiddleware.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing.Owin +{ + internal class AppFuncMiddleware + { + public AppFuncMiddleware(Func, Task> next, Func, Task> appFunc) + { + this.Next = next; + this.AppFunc = appFunc; + } + + private Func, Task> AppFunc + { + get; + set; + } + + private Func, Task> Next + { + get; + set; + } + + public Task Invoke(IDictionary context) + { + return this.AppFunc(context); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Owin/RouteBuilder.cs b/src/Microsoft.AspNet.Routing/Owin/RouteBuilder.cs new file mode 100644 index 0000000000..b6c62d6303 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Owin/RouteBuilder.cs @@ -0,0 +1,53 @@ + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +#if NET45 +using Owin; +#endif + +namespace Microsoft.AspNet.Routing.Owin +{ + internal class RouteBuilder : IRouteBuilder + { +#if NET45 + public RouteBuilder(IAppBuilder builder, IRouteEngine engine, RouteTable routes) + { + this.AppBuilder = builder; + this.Engine = engine; + this.Routes = routes; + } +#else + public RouteBuilder(IRouteEngine engine, RouteTable routes) + { + this.Engine = engine; + this.Routes = routes; + } +#endif + +#if NET45 + public IAppBuilder AppBuilder + { + get; + private set; + } +#endif + + public IRouteEngine Engine + { + get; + private set; + } + + private RouteTable Routes + { + get; + set; + } + + public IRouteEndpoint ForApp(Func, Task>> handlerFactory) + { + return new RouteEndpoint(handlerFactory(), this.Routes); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Owin/RouteEndpoint.cs b/src/Microsoft.AspNet.Routing/Owin/RouteEndpoint.cs new file mode 100644 index 0000000000..36c645c70f --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Owin/RouteEndpoint.cs @@ -0,0 +1,34 @@ + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing.Owin +{ + internal class RouteEndpoint : IRouteEndpoint + { + public RouteEndpoint(Func, Task> appFunc, RouteTable routes) + { + this.AppFunc = appFunc; + this.Routes = routes; + } + + public Func, Task> AppFunc + { + get; + private set; + } + + private RouteTable Routes + { + get; + set; + } + + public IRouteEndpoint AddRoute(string name, IRoute route) + { + this.Routes.Add(name, route); + return this; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs b/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs new file mode 100644 index 0000000000..389e0f146a --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs @@ -0,0 +1,48 @@ + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing.Owin +{ + public class RouterMiddleware + { + public RouterMiddleware(Func, Task> next, IRouteEngine engine, RouteTable routes) + { + this.Next = next; + this.Engine = engine; + this.Routes = routes; + } + + private IRouteEngine Engine + { + get; + set; + } + + private Func, Task> Next + { + get; + set; + } + + private RouteTable Routes + { + get; + set; + } + + public Task Invoke(IDictionary context) + { + var match = this.Engine.GetMatch(context); + if (match == null) + { + return Next.Invoke(context); + } + else + { + return match.Destination.Invoke(context); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/PrefixRoute.cs b/src/Microsoft.AspNet.Routing/PrefixRoute.cs new file mode 100644 index 0000000000..9154990ddf --- /dev/null +++ b/src/Microsoft.AspNet.Routing/PrefixRoute.cs @@ -0,0 +1,67 @@ +using System; + +namespace Microsoft.AspNet.Routing +{ + public class PrefixRoute : IRoute + { + public PrefixRoute(IRouteEndpoint endpoint, string prefix) + { + this.Endpoint = endpoint; + + if (prefix != null) + { + if (prefix.Length == 0 || prefix[0] != '/') + { + prefix = '/' + prefix; + } + + if (prefix.Length > 1 && prefix[prefix.Length - 1] == '/') + { + prefix = prefix.Substring(0, prefix.Length - 1); + } + + this.Prefix = prefix; + } + } + + private IRouteEndpoint Endpoint + { + get; + set; + } + + private string Prefix + { + get; + set; + } + + public virtual BoundRoute Bind(RouteBindingContext context) + { + return null; + } + + public virtual RouteMatch GetMatch(RoutingContext context) + { + if (this.Prefix == null) + { + return new RouteMatch(this.Endpoint.AppFunc); + } + else if (context.RequestPath.StartsWith(this.Prefix, StringComparison.OrdinalIgnoreCase)) + { + if (context.RequestPath.Length > this.Prefix.Length) + { + char next = context.RequestPath[this.Prefix.Length]; + if (next != '/' && next != '#' && next != '?') + { + return null; + } + } + + return new RouteMatch(this.Endpoint.AppFunc); + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Routing/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..78c63bee70 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Microsoft.AspNet.Routing")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Microsoft.AspNet.Routing")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("625c3577-6c55-4ed0-80cc-3b3481955bd0")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Microsoft.AspNet.Routing/RouteBindingContext.cs b/src/Microsoft.AspNet.Routing/RouteBindingContext.cs new file mode 100644 index 0000000000..b250b3773c --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteBindingContext.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.AspNet.Routing +{ + public class RouteBindingContext + { + public RouteBindingContext(IDictionary context, IDictionary values) + { + this.Context = context; + this.Values = values; + + this.AmbientValues = context.GetRouteMatchValues(); + } + + public IDictionary AmbientValues + { + get; + private set; + } + + public IDictionary Context + { + get; + private set; + } + + public IDictionary Values + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs b/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs new file mode 100644 index 0000000000..68efb000fd --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs @@ -0,0 +1,46 @@ + +using System; +#if NET45 +using Owin; +#endif + +using AppFunc = System.Func, System.Threading.Tasks.Task>; + +namespace Microsoft.AspNet.Routing +{ + public static class RouteBuilderExtensions + { +#if NET45 + public static IRouteEndpoint ForApp(this IRouteBuilder routeBuilder, Func handlerBuilder) + { + var builder = handlerBuilder.Invoke(routeBuilder.AppBuilder.New(), routeBuilder.Engine); + var appFunc = (AppFunc)builder.Build(typeof(AppFunc)); + return routeBuilder.ForApp(() => appFunc); + } + + public static IRouteEndpoint ForApp(this IRouteBuilder routeBuilder, Action handlerBuilder) + { + return routeBuilder.ForApp((builder, engine) => { handlerBuilder(builder, engine); return builder; }); + } + + public static IRouteEndpoint ForApp(this IRouteBuilder routeBuilder, Action handlerBuilder) + { + return routeBuilder.ForApp((builder, engine) => { handlerBuilder(builder); return builder; }); + } + + public static IRouteEndpoint ForApp(this IRouteBuilder routeBuilder, Func handlerBuilder) + { + return routeBuilder.ForApp((builder, engine) => handlerBuilder(builder)); + } +#endif + public static IRouteEndpoint ForApp(this IRouteBuilder routeBuilder, Func handlerBuilder) + { + return routeBuilder.ForApp(() => handlerBuilder(null)); + } + + public static IRouteEndpoint ForApp(this IRouteBuilder routeBuilder, AppFunc handler) + { + return routeBuilder.ForApp((Func)((next) => handler)); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RouteEngine.cs b/src/Microsoft.AspNet.Routing/RouteEngine.cs new file mode 100644 index 0000000000..cda55a4067 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteEngine.cs @@ -0,0 +1,72 @@ + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public class RouteEngine : IRouteEngine + { + public RouteEngine(RouteTable routes) + { + this.Routes = routes; + } + + public RouteTable Routes + { + get; + private set; + } + + public RouteMatch GetMatch(IDictionary context) + { + var routingContext = new RoutingContext(context); + + for (int i = 0; i < this.Routes.Routes.Count; i++) + { + var route = this.Routes.Routes[i]; + + var match = route.GetMatch(routingContext); + if (match == null) + { + continue; + } + + context.SetRouteMatchValues(match.Values); + context.SetRouteEngine(this); + + return match; + } + + return null; + } + + public BoundRoute GetUrl(string name, IDictionary context, IDictionary values) + { + var bindingContext = new RouteBindingContext(context, values); + + IRoute route; + + if (!String.IsNullOrWhiteSpace(name)) + { + if (Routes.NamedRoutes.TryGetValue(name, out route)) + { + return route.Bind(bindingContext); + } + } + + for (int j = 0; j < this.Routes.Routes.Count; j++) + { + route = this.Routes.Routes[j]; + + var result = route.Bind(bindingContext); + if (result != null) + { + return result; + } + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RouteEngineExtensions.cs b/src/Microsoft.AspNet.Routing/RouteEngineExtensions.cs new file mode 100644 index 0000000000..cf76ec68e5 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteEngineExtensions.cs @@ -0,0 +1,18 @@ + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public static class RouteEngineExtensions + { + public static BoundRoute GetUrl(this IRouteEngine engine, IDictionary context, IDictionary values) + { + return engine.GetUrl(null, context, values); + } + + public static BoundRoute GetUrl(this IRouteEngine engine, string name, IDictionary context, IDictionary values) + { + return engine.GetUrl(name, context, values); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RouteMatch.cs b/src/Microsoft.AspNet.Routing/RouteMatch.cs new file mode 100644 index 0000000000..271bbe9d1d --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteMatch.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing +{ + public class RouteMatch + { + public RouteMatch(Func, Task> destination) + : this(destination, null) + { + } + + public RouteMatch(Func, Task> destination, IDictionary values) + { + this.Destination = destination; + this.Values = values; + } + + public Func, Task> Destination + { + get; + private set; + } + + public IDictionary Values + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RouteTable.cs b/src/Microsoft.AspNet.Routing/RouteTable.cs new file mode 100644 index 0000000000..01a3a85560 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteTable.cs @@ -0,0 +1,44 @@ + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing +{ + public class RouteTable + { + public RouteTable() + { + this.Routes = new List(); + + this.NamedRoutes = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + public List Routes + { + get; + private set; + } + + public IDictionary NamedRoutes + { + get; + private set; + } + + public void Add(IRoute route) + { + this.Add(null, route); + } + + public void Add(string name, IRoute route) + { + this.Routes.Add(route); + + if (!String.IsNullOrEmpty(name)) + { + this.NamedRoutes.Add(name, route); + } + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RoutingContext.cs b/src/Microsoft.AspNet.Routing/RoutingContext.cs new file mode 100644 index 0000000000..029f86e8d0 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RoutingContext.cs @@ -0,0 +1,34 @@ + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public class RoutingContext + { + public RoutingContext(IDictionary context) + { + this.Context = context; + + this.RequestMethod = (string)context["owin.RequestMethod"]; + this.RequestPath = (string)context["owin.RequestPath"]; + } + + public IDictionary Context + { + get; + private set; + } + + public string RequestMethod + { + get; + private set; + } + + public string RequestPath + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs new file mode 100644 index 0000000000..7b34b994a4 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Routing.Legacy; + +namespace Microsoft.AspNet.Routing.Template +{ + public class TemplateRoute : IRoute + { + public TemplateRoute(IRouteEndpoint destination, string template) + : this(destination, template, null, null, null) + { + } + + public TemplateRoute(IRouteEndpoint destination, string template, IDictionary defaults, IDictionary constraints, IDictionary data) + { + this.Destination = destination; + this.Template = template; + this.Defaults = defaults; + this.Constraints = constraints; + this.Data = data; + + this.ParsedRoute = RouteParser.Parse(template); + + this.Initialize(); + } + + private IDictionary Constraints + { + get; + set; + } + + private IDictionary Data + { + get; + set; + } + + private IRouteEndpoint Destination + { + get; + set; + } + + public IDictionary FilterValues + { + get + { + return this.Defaults; + } + } + + private IDictionary Defaults + { + get; + set; + } + + private List> ConstraintsInternal + { + get; + set; + } + + private HttpParsedRoute ParsedRoute + { + get; + set; + } + + private string Template + { + get; + set; + } + + public RouteMatch GetMatch(RoutingContext context) + { + var match = this.ParsedRoute.Match(context, this.Defaults); + if (match == null) + { + return null; + } + + for (int i = 0; i < this.ConstraintsInternal.Count; i++) + { + var kvp = this.ConstraintsInternal[i]; + + object value = null; + if (!String.IsNullOrEmpty(kvp.Key)) + { + match.TryGetValue(kvp.Key, out value); + } + + if (!kvp.Value.MatchInbound(context, match, value)) + { + return null; + } + } + + return new RouteMatch(this.Destination.AppFunc, match); + } + + public bool OnSelected(IDictionary context, RouteMatch match) + { + if (this.Data != null) + { + foreach (var kvp in this.Data) + { + context[kvp.Key] = kvp.Value; + } + } + + return true; + } + + public BoundRoute Bind(RouteBindingContext context) + { + var template = this.ParsedRoute.Bind(context, this.Defaults); + if (template == null) + { + return null; + } + + for (int i = 0; i < this.ConstraintsInternal.Count; i++) + { + var kvp = this.ConstraintsInternal[i]; + + object value = null; + if (!String.IsNullOrEmpty(kvp.Key)) + { + template.Values.TryGetValue(kvp.Key, out value); + } + + if (!kvp.Value.MatchOutbound(context, template.Values, value)) + { + return null; + } + } + + return new BoundRoute(template.BoundTemplate, template.Values); + } + + private void Initialize() + { + this.ConstraintsInternal = new List>(); + + if (this.Constraints == null) + { + return; + } + + foreach (var kvp in this.Constraints) + { + string constraintString; + IConstraint constraint; + + if ((constraintString = kvp.Value as string) != null) + { + // TODO regex constraints + } + else if ((constraint = kvp.Value as IConstraint) != null) + { + this.ConstraintsInternal.Add(new KeyValuePair(kvp.Key, constraint)); + } + else + { + throw new ArgumentException(); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRouteExtensions.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRouteExtensions.cs new file mode 100644 index 0000000000..dd8968542a --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRouteExtensions.cs @@ -0,0 +1,48 @@ + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing.Template +{ + public static class HttpMethodRouteExtensions + { + public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string template) + { + return endpoint.AddTemplateRoute(null, template, null, null, null); + } + + public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string template, IDictionary defaults) + { + return endpoint.AddTemplateRoute(null, template, defaults, null, null); + } + + public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string template, IDictionary defaults, IDictionary constraints) + { + return endpoint.AddTemplateRoute(null, template, defaults, constraints, null); + } + + public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string template, IDictionary defaults, IDictionary constraints, IDictionary data) + { + return endpoint.AddTemplateRoute(null, template, defaults, constraints, data); + } + + public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string name, string template) + { + return endpoint.AddTemplateRoute(name, template, null, null, null); + } + + public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string name, string template, IDictionary defaults) + { + return endpoint.AddTemplateRoute(name, template, defaults, null, null); + } + + public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string name, string template, IDictionary defaults, IDictionary constraints) + { + return endpoint.AddTemplateRoute(name, template, defaults, constraints, null); + } + + public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string name, string template, IDictionary defaults, IDictionary constraints, IDictionary data) + { + return endpoint.AddRoute(name, new TemplateRoute(endpoint, template, defaults, constraints, data)); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Tree/ITreeRouteBuilder.cs b/src/Microsoft.AspNet.Routing/Tree/ITreeRouteBuilder.cs new file mode 100644 index 0000000000..8dac83ac0a --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Tree/ITreeRouteBuilder.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing.Tree +{ + public interface ITreeRouteBuilder + { + void Build(); + + ITreeRouteBuilder Endpoint(IRouteEndpoint endpoint); + + ITreeRouteBuilder Segment(Func segmentBuilder); + } +} diff --git a/src/Microsoft.AspNet.Routing/Tree/ITreeSegment.cs b/src/Microsoft.AspNet.Routing/Tree/ITreeSegment.cs new file mode 100644 index 0000000000..18ef83203c --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Tree/ITreeSegment.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing.Tree +{ + public interface ITreeSegment + { + } +} diff --git a/src/Microsoft.AspNet.Routing/Tree/ParameterSegment.cs b/src/Microsoft.AspNet.Routing/Tree/ParameterSegment.cs new file mode 100644 index 0000000000..60be40c4d5 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Tree/ParameterSegment.cs @@ -0,0 +1,17 @@ + +namespace Microsoft.AspNet.Routing.Tree +{ + internal class ParameterSegment : ITreeSegment + { + public ParameterSegment(string name) + { + this.Name = name; + } + + public string Name + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Tree/PathSegment.cs b/src/Microsoft.AspNet.Routing/Tree/PathSegment.cs new file mode 100644 index 0000000000..11c53b3acb --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Tree/PathSegment.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.AspNet.Routing.Tree +{ + internal class PathSegment : ITreeSegment + { + public PathSegment(string path) + { + this.Path = path; + } + + private string Path + { + get; + set; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Tree/TreeRoute.cs b/src/Microsoft.AspNet.Routing/Tree/TreeRoute.cs new file mode 100644 index 0000000000..4d9374e79a --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Tree/TreeRoute.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing.Tree +{ + public class TreeRoute : IRoute + { + public BoundRoute Bind(RouteBindingContext context) + { + throw new NotImplementedException(); + } + + public RouteMatch GetMatch(RoutingContext context) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilder.cs b/src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilder.cs new file mode 100644 index 0000000000..01da5be476 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilder.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing.Tree +{ + internal class TreeRouteBuilder : ITreeRouteBuilder + { + public TreeRouteBuilder(IRouteBuilder routeBuilder) + { + this.RouteBuilder = routeBuilder; + } + + public TreeRouteBuilder(Func current, TreeRouteBuilder parent) + { + this.Current = current; + this.Parent = parent; + + this.RouteBuilder = parent.RouteBuilder; + this.RouteEndpoint = parent.RouteEndpoint; + } + + private Func Current + { + get; + set; + } + + private TreeRouteBuilder Parent + { + get; + set; + } + + private IRouteBuilder RouteBuilder + { + get; + set; + } + + private IRouteEndpoint RouteEndpoint + { + get; + set; + } + + public void Build() + { + throw new NotImplementedException(); + } + + public ITreeRouteBuilder Endpoint(IRouteEndpoint endpoint) + { + this.RouteEndpoint = endpoint; + return this; + } + + public ITreeRouteBuilder Segment(Func segmentBuilder) + { + return new TreeRouteBuilder(segmentBuilder, this); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilderExtensions.cs b/src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilderExtensions.cs new file mode 100644 index 0000000000..da457ab33f --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilderExtensions.cs @@ -0,0 +1,24 @@ + +using System; + +namespace Microsoft.AspNet.Routing.Tree +{ + public static class TreeRouteBuilderExtensions + { + public static ITreeRouteBuilder Path(this ITreeRouteBuilder routeBuilder, string path) + { + ITreeRouteBuilder current = routeBuilder; + foreach (var segment in path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries)) + { + current = current.Segment(() => new PathSegment(segment)); + } + + return current; + } + + public static ITreeRouteBuilder Parameter(this ITreeRouteBuilder routeBuilder, string name) + { + return routeBuilder.Segment(() => new ParameterSegment(name)); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Tree/TreeRouteExtensions.cs b/src/Microsoft.AspNet.Routing/Tree/TreeRouteExtensions.cs new file mode 100644 index 0000000000..8540c7006f --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Tree/TreeRouteExtensions.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing.Tree +{ + public static class TreeRouteExtensions + { + public static ITreeRouteBuilder AddTreeRoute(this IRouteBuilder routeBuilder) + { + return new TreeRouteBuilder(routeBuilder); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/project.json b/src/Microsoft.AspNet.Routing/project.json index 5f23498788..f0cefe6d12 100644 --- a/src/Microsoft.AspNet.Routing/project.json +++ b/src/Microsoft.AspNet.Routing/project.json @@ -2,7 +2,12 @@ "version": "0.1-alpha-*", "dependencies": {}, "configurations": { - "net45": { }, + "net45": { + "dependencies": { + "Owin": "1.0", + "Microsoft.Owin": "2.1.0" + } + }, "k10" : { } } } \ No newline at end of file From c0660f347a45a37f9aa1f560f9fd656a369dcd56 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 28 Jan 2014 14:26:58 -0800 Subject: [PATCH 005/616] Revert "Getting routing prototype into history" We're not using the prototype as a base, just want to to be in the history so we can easily refer to it. This reverts commit 4183bc98be89371c65f5a708b1f0ec351ccd6ffd. --- .gitattributes | 63 -- Routing.sln | 43 - samples/RoutingSample/Program.cs | 19 - samples/RoutingSample/Startup.cs | 151 ---- samples/RoutingSample/project.json | 18 - src/Microsoft.AspNet.Routing/BoundRoute.cs | 26 - .../ContextExtensions.cs | 43 - .../HttpMethod/HttpMethodRoute.cs | 29 - .../HttpMethod/HttpMethodRouteExtensions.cs | 62 -- src/Microsoft.AspNet.Routing/IConstraint.cs | 15 - src/Microsoft.AspNet.Routing/IRoute.cs | 12 - src/Microsoft.AspNet.Routing/IRouteBuilder.cs | 27 - .../IRouteEndpoint.cs | 16 - src/Microsoft.AspNet.Routing/IRouteEngine.cs | 12 - .../Lambda/LambdaRoute.cs | 44 - .../Lambda/LambdaRouteExtensions.cs | 25 - .../Legacy/BoundRouteTemplate.cs | 16 - .../Legacy/HttpParsedRoute.cs | 821 ------------------ .../Legacy/HttpRouteValueDictionary.cs | 54 -- .../Legacy/PathSegment.cs | 169 ---- .../Legacy/PropertyHelper.cs | 189 ---- .../Legacy/RouteParser.cs | 377 -------- .../Owin/AppBuilderExtensions.cs | 22 - .../Owin/AppFuncMiddleware.cs | 34 - .../Owin/RouteBuilder.cs | 53 -- .../Owin/RouteEndpoint.cs | 34 - .../Owin/RouterMiddleware.cs | 48 - src/Microsoft.AspNet.Routing/PrefixRoute.cs | 67 -- .../Properties/AssemblyInfo.cs | 36 - .../RouteBindingContext.cs | 36 - .../RouteBuilderExtensions.cs | 46 - src/Microsoft.AspNet.Routing/RouteEngine.cs | 72 -- .../RouteEngineExtensions.cs | 18 - src/Microsoft.AspNet.Routing/RouteMatch.cs | 34 - src/Microsoft.AspNet.Routing/RouteTable.cs | 44 - .../RoutingContext.cs | 34 - .../Template/TemplateRoute.cs | 173 ---- .../Template/TemplateRouteExtensions.cs | 48 - .../Tree/ITreeRouteBuilder.cs | 17 - .../Tree/ITreeSegment.cs | 12 - .../Tree/ParameterSegment.cs | 17 - .../Tree/PathSegment.cs | 21 - .../Tree/TreeRoute.cs | 21 - .../Tree/TreeRouteBuilder.cs | 65 -- .../Tree/TreeRouteBuilderExtensions.cs | 24 - .../Tree/TreeRouteExtensions.cs | 16 - src/Microsoft.AspNet.Routing/project.json | 7 +- 47 files changed, 1 insertion(+), 3229 deletions(-) delete mode 100644 .gitattributes delete mode 100644 Routing.sln delete mode 100644 samples/RoutingSample/Program.cs delete mode 100644 samples/RoutingSample/Startup.cs delete mode 100644 samples/RoutingSample/project.json delete mode 100644 src/Microsoft.AspNet.Routing/BoundRoute.cs delete mode 100644 src/Microsoft.AspNet.Routing/ContextExtensions.cs delete mode 100644 src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRoute.cs delete mode 100644 src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRouteExtensions.cs delete mode 100644 src/Microsoft.AspNet.Routing/IConstraint.cs delete mode 100644 src/Microsoft.AspNet.Routing/IRoute.cs delete mode 100644 src/Microsoft.AspNet.Routing/IRouteBuilder.cs delete mode 100644 src/Microsoft.AspNet.Routing/IRouteEndpoint.cs delete mode 100644 src/Microsoft.AspNet.Routing/IRouteEngine.cs delete mode 100644 src/Microsoft.AspNet.Routing/Lambda/LambdaRoute.cs delete mode 100644 src/Microsoft.AspNet.Routing/Lambda/LambdaRouteExtensions.cs delete mode 100644 src/Microsoft.AspNet.Routing/Legacy/BoundRouteTemplate.cs delete mode 100644 src/Microsoft.AspNet.Routing/Legacy/HttpParsedRoute.cs delete mode 100644 src/Microsoft.AspNet.Routing/Legacy/HttpRouteValueDictionary.cs delete mode 100644 src/Microsoft.AspNet.Routing/Legacy/PathSegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Legacy/PropertyHelper.cs delete mode 100644 src/Microsoft.AspNet.Routing/Legacy/RouteParser.cs delete mode 100644 src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs delete mode 100644 src/Microsoft.AspNet.Routing/Owin/AppFuncMiddleware.cs delete mode 100644 src/Microsoft.AspNet.Routing/Owin/RouteBuilder.cs delete mode 100644 src/Microsoft.AspNet.Routing/Owin/RouteEndpoint.cs delete mode 100644 src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs delete mode 100644 src/Microsoft.AspNet.Routing/PrefixRoute.cs delete mode 100644 src/Microsoft.AspNet.Routing/Properties/AssemblyInfo.cs delete mode 100644 src/Microsoft.AspNet.Routing/RouteBindingContext.cs delete mode 100644 src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs delete mode 100644 src/Microsoft.AspNet.Routing/RouteEngine.cs delete mode 100644 src/Microsoft.AspNet.Routing/RouteEngineExtensions.cs delete mode 100644 src/Microsoft.AspNet.Routing/RouteMatch.cs delete mode 100644 src/Microsoft.AspNet.Routing/RouteTable.cs delete mode 100644 src/Microsoft.AspNet.Routing/RoutingContext.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateRouteExtensions.cs delete mode 100644 src/Microsoft.AspNet.Routing/Tree/ITreeRouteBuilder.cs delete mode 100644 src/Microsoft.AspNet.Routing/Tree/ITreeSegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Tree/ParameterSegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Tree/PathSegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Tree/TreeRoute.cs delete mode 100644 src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilder.cs delete mode 100644 src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilderExtensions.cs delete mode 100644 src/Microsoft.AspNet.Routing/Tree/TreeRouteExtensions.cs diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 1ff0c42304..0000000000 --- a/.gitattributes +++ /dev/null @@ -1,63 +0,0 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### -* text=auto - -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain diff --git a/Routing.sln b/Routing.sln deleted file mode 100644 index f7f38f58df..0000000000 --- a/Routing.sln +++ /dev/null @@ -1,43 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.21005.1 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Routing.net45", "src\Microsoft.AspNet.Routing\Microsoft.AspNet.Routing.net45.csproj", "{BD60E294-D97A-46CE-A6F0-2A293989B56B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{949A5859-ACBA-4687-B393-644BCF32C1D8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Routing.k10", "src\Microsoft.AspNet.Routing\Microsoft.AspNet.Routing.k10.csproj", "{4463D373-7BAA-42DC-803E-C4B2B8D3A2F4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoutingSample.net45", "samples\RoutingSample\RoutingSample.net45.csproj", "{6552385C-3FB0-40A0-A29B-2B5A5C411621}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{2B8B4E94-8C98-4959-9116-4BAD8FF69468}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {BD60E294-D97A-46CE-A6F0-2A293989B56B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BD60E294-D97A-46CE-A6F0-2A293989B56B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BD60E294-D97A-46CE-A6F0-2A293989B56B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BD60E294-D97A-46CE-A6F0-2A293989B56B}.Release|Any CPU.Build.0 = Release|Any CPU - {4463D373-7BAA-42DC-803E-C4B2B8D3A2F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4463D373-7BAA-42DC-803E-C4B2B8D3A2F4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4463D373-7BAA-42DC-803E-C4B2B8D3A2F4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4463D373-7BAA-42DC-803E-C4B2B8D3A2F4}.Release|Any CPU.Build.0 = Release|Any CPU - {6552385C-3FB0-40A0-A29B-2B5A5C411621}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6552385C-3FB0-40A0-A29B-2B5A5C411621}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6552385C-3FB0-40A0-A29B-2B5A5C411621}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6552385C-3FB0-40A0-A29B-2B5A5C411621}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {BD60E294-D97A-46CE-A6F0-2A293989B56B} = {949A5859-ACBA-4687-B393-644BCF32C1D8} - {4463D373-7BAA-42DC-803E-C4B2B8D3A2F4} = {949A5859-ACBA-4687-B393-644BCF32C1D8} - {6552385C-3FB0-40A0-A29B-2B5A5C411621} = {2B8B4E94-8C98-4959-9116-4BAD8FF69468} - EndGlobalSection -EndGlobal diff --git a/samples/RoutingSample/Program.cs b/samples/RoutingSample/Program.cs deleted file mode 100644 index 6db14b0a1e..0000000000 --- a/samples/RoutingSample/Program.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using Microsoft.Owin.Hosting; - -namespace RoutingSample -{ - class Program - { - static void Main(string[] args) - { - string url = "http://localhost:30000"; - using (WebApp.Start(url)) - { - Console.WriteLine("Listening on: {0}", url); - Console.WriteLine("Press ENTER to exit."); - Console.ReadLine(); - } - } - } -} diff --git a/samples/RoutingSample/Startup.cs b/samples/RoutingSample/Startup.cs deleted file mode 100644 index 0a040f7b78..0000000000 --- a/samples/RoutingSample/Startup.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNet.Routing; -using Microsoft.AspNet.Routing.HttpMethod; -using Microsoft.AspNet.Routing.Lambda; -using Microsoft.AspNet.Routing.Legacy; -using Microsoft.AspNet.Routing.Owin; -using Microsoft.AspNet.Routing.Template; -using Microsoft.AspNet.Routing.Tree; -using Owin; - -namespace RoutingSample -{ - internal class Startup - { - public void Configuration(IAppBuilder app) - { - // Configuring the router places it in the OWIN pipeline and retuns an IRouteBuilder which is used - // To create routes - all routes are directed to nested pipelines. - var router = app.UseRouter(); - - - // At the simplest level - we can support microframework style routing. - // - // This route matches a prefix of the request path, and a specific HTTP method. - router.Get("1/echo", async (context) => - { - string url = (string)context["owin.RequestPath"]; - await WriteAsync(context, url); - }); - - - // This route takes a lambda, and can apply arbitrary criteria for routing without needing to couple - // to an object model. - router.On(context => ((string)context["owin.RequestPath"]).StartsWith("2"), async (context) => - { - string url = (string)context["owin.RequestPath"]; - await WriteAsync(context, url); - }); - - - // The return value is an IRouteEndpoint - extension method friendly for adding more routes and different - // route types. - // - // All of these routes go to the same delegate. - router.Get("3/Store", async (context) => - { - string method = (string)context["owin.RequestMethod"]; - await WriteAsync(context, method); - }) - .Post("3/Store/Checkout") - .On(context => ((string)context["owin.RequestPath"]).StartsWith("3/api")); - - - // Routing to a middleware using IAppBuilder -- allowing routing to a more complex pipeline. - router.ForApp((builder) => builder.Use(async (context, next) => - { - await context.Response.WriteAsync("Hello, World!"); - })) - .Get("4/Hello") - .Post("4/Hello/World"); - - - // Nested Router - router.ForApp((builder) => - { - var nested = builder.UseRouter(); - - nested.Get(async (context) => await WriteAsync(context, "Get")); - nested.Post(async (context) => await WriteAsync(context, "Post")); - }) - .Get("5/Store"); - - - - // MVC/WebAPI Stuff below - using 'tradition' template routes. - - - // Routing with parameter capturing - the route data is stored in the owin context - router.ForApp((builder) => builder.Use(async (context, next) => - { - string controller = (string)context.Environment.GetRouteMatchValues()["controller"]; - await context.Response.WriteAsync(controller); - - })) - .AddTemplateRoute("6/api/{controller}", new HttpRouteValueDictionary(new { controller = "Home" })); - - - // Routing with data tokens - these are added to the context when a route matches - // This technique can be used for MVC/Web API to perform action selection as part of routing - router.ForApp((builder) => builder.Use(async (context, next) => - { - string stringValue = (string)context.Environment["myapp_StringValue"]; - await context.Response.WriteAsync(stringValue); - - })) - .AddTemplateRoute("7", null, null, data: new HttpRouteValueDictionary(new { myapp_StringValue = "cool" })); - - - // The route engine can be provided as a parameter to the app builder function so that it can be - // captured and used inside of the application. - // - // It's also provided as part of the owin context on a routed request for apps that would prefer - // a stateless style. - router.ForApp((builder, engine) => builder.Use(async (context, next) => - { - if (Object.Equals(engine, context.Environment.GetRouteEngine())) - { - await context.Response.WriteAsync(engine.GetType().ToString()); - } - else - { - await next(); - } - })) - .AddTemplateRoute("8"); - - - // Generating a link by name - router.ForApp((builder, engine) => builder.Use(async (context, next) => - { - string url = engine.GetUrl("ByName", context.Environment, new HttpRouteValueDictionary(new { })).Url; - await context.Response.WriteAsync(url); - })) - .AddTemplateRoute("ByName", "9/{value1}", new HttpRouteValueDictionary(new { value1 = "Cool" })); - - - // Tree Routing - var tree = router.AddTreeRoute(); - tree.Path("10/api").Parameter("controller").Endpoint(router.ForApp((builder) => builder.Use(async (context, next) => - { - string url = context.Request.Uri.PathAndQuery; - await context.Response.WriteAsync(url); - }))); - - - tree.Build(); - } - - private static Task WriteAsync(IDictionary context, string value) - { - var response = (Stream)context["owin.ResponseBody"]; - - var bytes = Encoding.UTF8.GetBytes(value); - return response.WriteAsync(bytes, 0, bytes.Length); - } - } -} diff --git a/samples/RoutingSample/project.json b/samples/RoutingSample/project.json deleted file mode 100644 index 8bc6ed75af..0000000000 --- a/samples/RoutingSample/project.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "version": "0.1-alpha-*", - "dependencies": { - "Microsoft.AspNet.Routing" : "" - }, - "configurations": { - "net45": { - "dependencies": { - "Owin": "1.0", - "Microsoft.Owin.Diagnostics": "2.1.0", - "Microsoft.Owin.Host.HttpListener": "2.1.0", - "Microsoft.Owin.Hosting": "2.1.0", - "Microsoft.Owin.SelfHost": "2.1.0", - "Microsoft.Owin": "2.1.0" - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/BoundRoute.cs b/src/Microsoft.AspNet.Routing/BoundRoute.cs deleted file mode 100644 index e51c5fb6d7..0000000000 --- a/src/Microsoft.AspNet.Routing/BoundRoute.cs +++ /dev/null @@ -1,26 +0,0 @@ - -using System.Collections.Generic; - -namespace Microsoft.AspNet.Routing -{ - public class BoundRoute - { - public BoundRoute(string url, IDictionary values) - { - this.Url = url; - this.Values = values; - } - - public string Url - { - get; - private set; - } - - public IDictionary Values - { - get; - private set; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/ContextExtensions.cs b/src/Microsoft.AspNet.Routing/ContextExtensions.cs deleted file mode 100644 index 4b2b65f40c..0000000000 --- a/src/Microsoft.AspNet.Routing/ContextExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ - -using System.Collections.Generic; - -namespace Microsoft.AspNet.Routing -{ - public static class OwinExtensions - { - public static string EngineKey = "routing.Engine"; - public static string MatchValuesKey = "routing.Values"; - - public static IRouteEngine GetRouteEngine(this IDictionary context) - { - object obj; - if (context.TryGetValue(EngineKey, out obj)) - { - return obj as IRouteEngine; - } - - return null; - } - - public static void SetRouteEngine(this IDictionary context, IRouteEngine value) - { - context[EngineKey] = value; - } - - public static IDictionary GetRouteMatchValues(this IDictionary context) - { - object obj; - if (context.TryGetValue(MatchValuesKey, out obj)) - { - return obj as IDictionary; - } - - return null; - } - - public static void SetRouteMatchValues(this IDictionary context, IDictionary values) - { - context[MatchValuesKey] = values; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRoute.cs b/src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRoute.cs deleted file mode 100644 index 8f17124b9f..0000000000 --- a/src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRoute.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; - -namespace Microsoft.AspNet.Routing.HttpMethod -{ - public class HttpMethodRoute : PrefixRoute - { - public HttpMethodRoute(IRouteEndpoint endpoint, string prefix, string method) - : base(endpoint, prefix) - { - this.Method = method; - } - - private string Method - { - get; - set; - } - - public override RouteMatch GetMatch(RoutingContext context) - { - if (String.Equals(context.RequestMethod, this.Method, StringComparison.OrdinalIgnoreCase)) - { - return base.GetMatch(context); - } - - return null; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRouteExtensions.cs b/src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRouteExtensions.cs deleted file mode 100644 index 86c64510ca..0000000000 --- a/src/Microsoft.AspNet.Routing/HttpMethod/HttpMethodRouteExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNet.Routing.Template; - -using AppFunc = System.Func, System.Threading.Tasks.Task>; - -namespace Microsoft.AspNet.Routing.HttpMethod -{ - public static class HttpMethodRouteExtensions - { - public static IRouteEndpoint Get(this IRouteEndpoint endpoint) - { - endpoint.AddRoute(null, new HttpMethodRoute(endpoint, null, "GET")); - return endpoint; - } - - public static IRouteEndpoint Get(this IRouteBuilder routeBuilder, AppFunc handler) - { - var endpoint = routeBuilder.ForApp((next) => handler); - return endpoint.Get(); - } - - public static IRouteEndpoint Get(this IRouteEndpoint endpoint, string prefix) - { - endpoint.AddRoute(null, new HttpMethodRoute(endpoint, prefix, "GET")); - return endpoint; - } - - public static IRouteEndpoint Get(this IRouteBuilder routeBuilder, string prefix, AppFunc handler) - { - var endpoint = routeBuilder.ForApp((next) => handler); - return endpoint.Get(prefix); - } - - public static IRouteEndpoint Post(this IRouteEndpoint endpoint) - { - endpoint.AddRoute(null, new HttpMethodRoute(endpoint, null, "POST")); - return endpoint; - } - - public static IRouteEndpoint Post(this IRouteBuilder routeBuilder, AppFunc handler) - { - var endpoint = routeBuilder.ForApp((next) => handler); - return endpoint.Post(); - } - - public static IRouteEndpoint Post(this IRouteEndpoint endpoint, string prefix) - { - endpoint.AddRoute(null, new HttpMethodRoute(endpoint, prefix, "POST")); - return endpoint; - } - - public static IRouteEndpoint Post(this IRouteBuilder routeBuilder, string prefix, AppFunc handler) - { - var endpoint = routeBuilder.ForApp((next) => handler); - return endpoint.Post(prefix); - } - } -} diff --git a/src/Microsoft.AspNet.Routing/IConstraint.cs b/src/Microsoft.AspNet.Routing/IConstraint.cs deleted file mode 100644 index 3190c4517b..0000000000 --- a/src/Microsoft.AspNet.Routing/IConstraint.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Routing -{ - public interface IConstraint - { - bool MatchInbound(RoutingContext context, IDictionary values, object value); - - bool MatchOutbound(RouteBindingContext context, IDictionary values, object value); - } -} diff --git a/src/Microsoft.AspNet.Routing/IRoute.cs b/src/Microsoft.AspNet.Routing/IRoute.cs deleted file mode 100644 index 6ba80191a7..0000000000 --- a/src/Microsoft.AspNet.Routing/IRoute.cs +++ /dev/null @@ -1,12 +0,0 @@ - -using System.Collections.Generic; - -namespace Microsoft.AspNet.Routing -{ - public interface IRoute - { - BoundRoute Bind(RouteBindingContext context); - - RouteMatch GetMatch(RoutingContext context); - } -} diff --git a/src/Microsoft.AspNet.Routing/IRouteBuilder.cs b/src/Microsoft.AspNet.Routing/IRouteBuilder.cs deleted file mode 100644 index ba5730d818..0000000000 --- a/src/Microsoft.AspNet.Routing/IRouteBuilder.cs +++ /dev/null @@ -1,27 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -#if NET45 -using Owin; -#endif - -namespace Microsoft.AspNet.Routing -{ - public interface IRouteBuilder - { -#if NET45 - IAppBuilder AppBuilder - { - get; - } -#endif - IRouteEngine Engine - { - get; - } - - IRouteEndpoint ForApp(Func, Task>> handlerFactory); - } -} - diff --git a/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs b/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs deleted file mode 100644 index 5ad2bb8753..0000000000 --- a/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Routing -{ - public interface IRouteEndpoint - { - Func, Task> AppFunc - { - get; - } - - IRouteEndpoint AddRoute(string name, IRoute route); - } -} diff --git a/src/Microsoft.AspNet.Routing/IRouteEngine.cs b/src/Microsoft.AspNet.Routing/IRouteEngine.cs deleted file mode 100644 index 8d7694722e..0000000000 --- a/src/Microsoft.AspNet.Routing/IRouteEngine.cs +++ /dev/null @@ -1,12 +0,0 @@ - -using System.Collections.Generic; - -namespace Microsoft.AspNet.Routing -{ - public interface IRouteEngine - { - RouteMatch GetMatch(IDictionary context); - - BoundRoute GetUrl(string routeName, IDictionary context, IDictionary values); - } -} diff --git a/src/Microsoft.AspNet.Routing/Lambda/LambdaRoute.cs b/src/Microsoft.AspNet.Routing/Lambda/LambdaRoute.cs deleted file mode 100644 index c3954970cd..0000000000 --- a/src/Microsoft.AspNet.Routing/Lambda/LambdaRoute.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Routing.Lambda -{ - public class LambdaRoute : IRoute - { - public LambdaRoute(IRouteEndpoint endpoint, Func, bool> condition) - { - this.Endpoint = endpoint; - this.Condition = condition; - } - - private Func, bool> Condition - { - get; - set; - } - - private IRouteEndpoint Endpoint - { - get; - set; - } - - public BoundRoute Bind(RouteBindingContext context) - { - return null; - } - - public RouteMatch GetMatch(RoutingContext context) - { - if (Condition(context.Context)) - { - return new RouteMatch(this.Endpoint.AppFunc); - } - - return null; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Lambda/LambdaRouteExtensions.cs b/src/Microsoft.AspNet.Routing/Lambda/LambdaRouteExtensions.cs deleted file mode 100644 index cf0cb37ede..0000000000 --- a/src/Microsoft.AspNet.Routing/Lambda/LambdaRouteExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -using AppFunc = System.Func, System.Threading.Tasks.Task>; - -namespace Microsoft.AspNet.Routing.Lambda -{ - public static class LambdaRouteExtensions - { - public static IRouteEndpoint On(this IRouteEndpoint endpoint, Func, bool> condition) - { - endpoint.AddRoute(null, new LambdaRoute(endpoint, condition)); - return endpoint; - } - - public static IRouteEndpoint On(this IRouteBuilder routeBuilder, Func, bool> condition, AppFunc handler) - { - var endpoint = routeBuilder.ForApp(handler); - return endpoint.On(condition); - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Legacy/BoundRouteTemplate.cs b/src/Microsoft.AspNet.Routing/Legacy/BoundRouteTemplate.cs deleted file mode 100644 index 34003330b3..0000000000 --- a/src/Microsoft.AspNet.Routing/Legacy/BoundRouteTemplate.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System.Collections.Generic; - -namespace Microsoft.AspNet.Routing.Legacy -{ - /// - /// Represents a URI generated from a . - /// - internal class BoundRouteTemplate - { - public string BoundTemplate { get; set; } - - public IDictionary Values { get; set; } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Legacy/HttpParsedRoute.cs b/src/Microsoft.AspNet.Routing/Legacy/HttpParsedRoute.cs deleted file mode 100644 index 52f114dd03..0000000000 --- a/src/Microsoft.AspNet.Routing/Legacy/HttpParsedRoute.cs +++ /dev/null @@ -1,821 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace Microsoft.AspNet.Routing.Legacy -{ - internal sealed class HttpParsedRoute - { - public HttpParsedRoute(List pathSegments) - { - Contract.Assert(pathSegments != null); - PathSegments = pathSegments; - - this.ParameterSegments = new List(); - - for (int i = 0; i < this.PathSegments.Count; i++) - { - var segment = this.PathSegments[i] as PathContentSegment; - if (segment != null) - { - for (int j = 0; j < segment.Subsegments.Count; j++) - { - var subsegment = segment.Subsegments[j] as PathParameterSubsegment; - if (subsegment != null) - { - this.ParameterSegments.Add(subsegment); - } - } - } - } - } - - public List PathSegments { get; private set; } - - public List ParameterSegments { get; private set; } - - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Not changing original algorithm")] - [SuppressMessage("Microsoft.Maintainability", "CA1505:AvoidUnmaintainableCode", Justification = "Not changing original algorithm")] - public BoundRouteTemplate Bind(RouteBindingContext context, IDictionary defaults) - { - IDictionary currentValues = context.AmbientValues; - IDictionary values = context.Values; - - IDictionary constraints = null; - - if (currentValues == null) - { - currentValues = new Dictionary(); - } - - if (values == null) - { - values = new Dictionary(); - } - - if (defaults == null) - { - defaults = new Dictionary(); - } - - // The set of values we should be using when generating the URI in this route - Dictionary acceptedValues = new Dictionary(); - - // Keep track of which new values have been used - HashSet unusedNewValues = new HashSet(values.Keys, StringComparer.OrdinalIgnoreCase); - - // Step 1: Get the list of values we're going to try to use to match and generate this URI - - // Find out which entries in the URI are valid for the URI we want to generate. - // If the URI had ordered parameters a="1", b="2", c="3" and the new values - // specified that b="9", then we need to invalidate everything after it. The new - // values should then be a="1", b="9", c=. - for (int i = 0; i < this.ParameterSegments.Count; i++) - { - PathParameterSubsegment parameter = this.ParameterSegments[i]; - - // If it's a parameter subsegment, examine the current value to see if it matches the new value - string parameterName = parameter.ParameterName; - - object newParameterValue; - bool hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue); - if (hasNewParameterValue) - { - unusedNewValues.Remove(parameterName); - } - - object currentParameterValue; - bool hasCurrentParameterValue = currentValues.TryGetValue(parameterName, out currentParameterValue); - - if (hasNewParameterValue && hasCurrentParameterValue) - { - if (!RoutePartsEqual(currentParameterValue, newParameterValue)) - { - // Stop copying current values when we find one that doesn't match - break; - } - } - - // If the parameter is a match, add it to the list of values we will use for URI generation - if (hasNewParameterValue) - { - if (IsRoutePartNonEmpty(newParameterValue)) - { - acceptedValues.Add(parameterName, newParameterValue); - } - } - else - { - if (hasCurrentParameterValue) - { - acceptedValues.Add(parameterName, currentParameterValue); - } - } - } - - - // Add all remaining new values to the list of values we will use for URI generation - foreach (var newValue in values) - { - if (IsRoutePartNonEmpty(newValue.Value)) - { - if (!acceptedValues.ContainsKey(newValue.Key)) - { - acceptedValues.Add(newValue.Key, newValue.Value); - } - } - } - - // Add all current values that aren't in the URI at all - foreach (var currentValue in currentValues) - { - string parameterName = currentValue.Key; - if (!acceptedValues.ContainsKey(parameterName)) - { - PathParameterSubsegment parameterSubsegment = GetParameterSubsegment(PathSegments, parameterName); - if (parameterSubsegment == null) - { - acceptedValues.Add(parameterName, currentValue.Value); - } - } - } - - // Add all remaining default values from the route to the list of values we will use for URI generation - for (int i = 0; i < this.ParameterSegments.Count; i++) - { - PathParameterSubsegment parameter = this.ParameterSegments[i]; - if (!acceptedValues.ContainsKey(parameter.ParameterName)) - { - object defaultValue; - if (!IsParameterRequired(parameter, defaults, out defaultValue)) - { - // Add the default value only if there isn't already a new value for it and - // only if it actually has a default value, which we determine based on whether - // the parameter value is required. - acceptedValues.Add(parameter.ParameterName, defaultValue); - } - } - } - - // All required parameters in this URI must have values from somewhere (i.e. the accepted values) - bool hasAllRequiredValues = true; - for (int i = 0; i < this.ParameterSegments.Count; i++) - { - PathParameterSubsegment parameter = this.ParameterSegments[i]; - - object defaultValue; - if (IsParameterRequired(parameter, defaults, out defaultValue)) - { - if (!acceptedValues.ContainsKey(parameter.ParameterName)) - { - // If the route parameter value is required that means there's - // no default value, so if there wasn't a new value for it - // either, this route won't match. - hasAllRequiredValues = false; - break; - } - } - } - - if (!hasAllRequiredValues) - { - return null; - } - - // All other default values must match if they are explicitly defined in the new values - Dictionary otherDefaultValues = new Dictionary(defaults, StringComparer.OrdinalIgnoreCase); - - for (int i = 0; i < this.ParameterSegments.Count; i++) - { - PathParameterSubsegment parameter = this.ParameterSegments[i]; - otherDefaultValues.Remove(parameter.ParameterName); - } - - foreach (var defaultValue in otherDefaultValues) - { - object value; - if (values.TryGetValue(defaultValue.Key, out value)) - { - unusedNewValues.Remove(defaultValue.Key); - if (!RoutePartsEqual(value, defaultValue.Value)) - { - // If there is a non-parameterized value in the route and there is a - // new value for it and it doesn't match, this route won't match. - return null; - } - } - } - - // Step 2: If the route is a match generate the appropriate URI - - StringBuilder uri = new StringBuilder(); - StringBuilder pendingParts = new StringBuilder(); - - bool pendingPartsAreAllSafe = false; - bool blockAllUriAppends = false; - - for (int i = 0; i < PathSegments.Count; i++) - { - PathSegment pathSegment = PathSegments[i]; // parsedRouteUriPart - - if (pathSegment is PathSeparatorSegment) - { - if (pendingPartsAreAllSafe) - { - // Accept - if (pendingParts.Length > 0) - { - if (blockAllUriAppends) - { - return null; - } - - // Append any pending literals to the URI - uri.Append(pendingParts.ToString()); - pendingParts.Length = 0; - } - } - pendingPartsAreAllSafe = false; - - // Guard against appending multiple separators for empty segments - if (pendingParts.Length > 0 && pendingParts[pendingParts.Length - 1] == '/') - { - // Dev10 676725: Route should not be matched if that causes mismatched tokens - // Dev11 86819: We will allow empty matches if all subsequent segments are null - if (blockAllUriAppends) - { - return null; - } - - // Append any pending literals to the URI (without the trailing slash) and prevent any future appends - uri.Append(pendingParts.ToString(0, pendingParts.Length - 1)); - pendingParts.Length = 0; - blockAllUriAppends = true; - } - else - { - pendingParts.Append("/"); - } - } - else - { - PathContentSegment contentPathSegment = pathSegment as PathContentSegment; - if (contentPathSegment != null) - { - // Segments are treated as all-or-none. We should never output a partial segment. - // If we add any subsegment of this segment to the generated URI, we have to add - // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we - // used a value for {p1}, we have to output the entire segment up to the next "/". - // Otherwise we could end up with the partial segment "v1" instead of the entire - // segment "v1-v2.xml". - bool addedAnySubsegments = false; - - foreach (PathSubsegment subsegment in contentPathSegment.Subsegments) - { - PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment; - if (literalSubsegment != null) - { - // If it's a literal we hold on to it until we are sure we need to add it - pendingPartsAreAllSafe = true; - pendingParts.Append(literalSubsegment.Literal); - } - else - { - PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment; - if (parameterSubsegment != null) - { - if (pendingPartsAreAllSafe) - { - // Accept - if (pendingParts.Length > 0) - { - if (blockAllUriAppends) - { - return null; - } - - // Append any pending literals to the URI - uri.Append(pendingParts.ToString()); - pendingParts.Length = 0; - - addedAnySubsegments = true; - } - } - pendingPartsAreAllSafe = false; - - // If it's a parameter, get its value - object acceptedParameterValue; - bool hasAcceptedParameterValue = acceptedValues.TryGetValue(parameterSubsegment.ParameterName, out acceptedParameterValue); - if (hasAcceptedParameterValue) - { - unusedNewValues.Remove(parameterSubsegment.ParameterName); - } - - object defaultParameterValue; - defaults.TryGetValue(parameterSubsegment.ParameterName, out defaultParameterValue); - - if (RoutePartsEqual(acceptedParameterValue, defaultParameterValue)) - { - // If the accepted value is the same as the default value, mark it as pending since - // we won't necessarily add it to the URI we generate. - pendingParts.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture)); - } - else - { - if (blockAllUriAppends) - { - return null; - } - - // Add the new part to the URI as well as any pending parts - if (pendingParts.Length > 0) - { - // Append any pending literals to the URI - uri.Append(pendingParts.ToString()); - pendingParts.Length = 0; - } - uri.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture)); - - addedAnySubsegments = true; - } - } - else - { - Contract.Assert(false, "Invalid path subsegment type"); - } - } - } - - if (addedAnySubsegments) - { - // See comment above about why we add the pending parts - if (pendingParts.Length > 0) - { - if (blockAllUriAppends) - { - return null; - } - - // Append any pending literals to the URI - uri.Append(pendingParts.ToString()); - pendingParts.Length = 0; - } - } - } - else - { - Contract.Assert(false, "Invalid path segment type"); - } - } - } - - if (pendingPartsAreAllSafe) - { - // Accept - if (pendingParts.Length > 0) - { - if (blockAllUriAppends) - { - return null; - } - - // Append any pending literals to the URI - uri.Append(pendingParts.ToString()); - } - } - - // Process constraints keys - if (constraints != null) - { - // If there are any constraints, mark all the keys as being used so that we don't - // generate query string items for custom constraints that don't appear as parameters - // in the URI format. - foreach (var constraintsItem in constraints) - { - unusedNewValues.Remove(constraintsItem.Key); - } - } - - // Encode the URI before we append the query string, otherwise we would double encode the query string - StringBuilder encodedUri = new StringBuilder(); - encodedUri.Append(UriEncode(uri.ToString())); - uri = encodedUri; - - // Add remaining new values as query string parameters to the URI - if (unusedNewValues.Count > 0) - { - // Generate the query string - bool firstParam = true; - foreach (string unusedNewValue in unusedNewValues) - { - object value; - if (acceptedValues.TryGetValue(unusedNewValue, out value)) - { - uri.Append(firstParam ? '?' : '&'); - firstParam = false; - uri.Append(Uri.EscapeDataString(unusedNewValue)); - uri.Append('='); - uri.Append(Uri.EscapeDataString(Convert.ToString(value, CultureInfo.InvariantCulture))); - } - } - } - - return new BoundRouteTemplate() - { - BoundTemplate = uri.ToString(), - Values = acceptedValues - }; - } - - private static string EscapeReservedCharacters(Match m) - { -#if NET45 - return Uri.HexEscape(m.Value[0]); -#else - return m.Value; -#endif - } - - private PathParameterSubsegment GetParameterSubsegment(IList pathSegments, string parameterName) - { - for (int i = 0; i < this.ParameterSegments.Count; i++) - { - PathParameterSubsegment parameter = this.ParameterSegments[i]; - if (String.Equals(parameterName, parameter.ParameterName, StringComparison.OrdinalIgnoreCase)) - { - return parameter; - } - } - - return null; - } - - private static bool IsParameterRequired(PathParameterSubsegment parameterSubsegment, IDictionary defaultValues, out object defaultValue) - { - if (parameterSubsegment.IsCatchAll) - { - defaultValue = null; - return false; - } - - return !defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultValue); - } - - private static bool IsRoutePartNonEmpty(object routePart) - { - string routePartString = routePart as string; - if (routePartString != null) - { - return routePartString.Length > 0; - } - return routePart != null; - } - - public IDictionary Match(RoutingContext context, IDictionary defaultValues) - { - List requestPathSegments = RouteParser.SplitUriToPathSegmentStrings(context.RequestPath.Substring(1)); - - if (defaultValues == null) - { - defaultValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - Dictionary matchedValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // This flag gets set once all the data in the URI has been parsed through, but - // the route we're trying to match against still has more parts. At this point - // we'll only continue matching separator characters and parameters that have - // default values. - bool ranOutOfStuffToParse = false; - - // This value gets set once we start processing a catchall parameter (if there is one - // at all). Once we set this value we consume all remaining parts of the URI into its - // parameter value. - bool usedCatchAllParameter = false; - - for (int i = 0; i < PathSegments.Count; i++) - { - PathSegment pathSegment = PathSegments[i]; - - if (requestPathSegments.Count <= i) - { - ranOutOfStuffToParse = true; - } - - string requestPathSegment = ranOutOfStuffToParse ? null : requestPathSegments[i]; - - if (pathSegment is PathSeparatorSegment) - { - if (ranOutOfStuffToParse) - { - // If we're trying to match a separator in the route but there's no more content, that's OK - } - else - { - if (!String.Equals(requestPathSegment, "/", StringComparison.Ordinal)) - { - return null; - } - } - } - else - { - PathContentSegment contentPathSegment = pathSegment as PathContentSegment; - if (contentPathSegment != null) - { - if (contentPathSegment.IsCatchAll) - { - Contract.Assert(i == (PathSegments.Count - 1), "If we're processing a catch-all, we should be on the last route segment."); - MatchCatchAll(contentPathSegment, requestPathSegments.Skip(i), defaultValues, matchedValues); - usedCatchAllParameter = true; - } - else - { - if (!MatchContentPathSegment(contentPathSegment, requestPathSegment, defaultValues, matchedValues)) - { - return null; - } - } - } - else - { - Contract.Assert(false, "Invalid path segment type"); - } - } - } - - if (!usedCatchAllParameter) - { - if (PathSegments.Count < requestPathSegments.Count) - { - // If we've already gone through all the parts defined in the route but the URI - // still contains more content, check that the remaining content is all separators. - for (int i = PathSegments.Count; i < requestPathSegments.Count; i++) - { - if (!RouteParser.IsSeparator(requestPathSegments[i])) - { - return null; - } - } - } - } - - // Copy all remaining default values to the route data - if (defaultValues != null) - { - foreach (var defaultValue in defaultValues) - { - if (!matchedValues.ContainsKey(defaultValue.Key)) - { - matchedValues.Add(defaultValue.Key, defaultValue.Value); - } - } - } - - return matchedValues; - } - - private static void MatchCatchAll(PathContentSegment contentPathSegment, IEnumerable remainingRequestSegments, IDictionary defaultValues, IDictionary matchedValues) - { - string remainingRequest = String.Join(String.Empty, remainingRequestSegments.ToArray()); - - PathParameterSubsegment catchAllSegment = contentPathSegment.Subsegments[0] as PathParameterSubsegment; - - object catchAllValue; - - if (remainingRequest.Length > 0) - { - catchAllValue = remainingRequest; - } - else - { - defaultValues.TryGetValue(catchAllSegment.ParameterName, out catchAllValue); - } - - matchedValues.Add(catchAllSegment.ParameterName, catchAllValue); - } - - private static bool MatchContentPathSegment(PathContentSegment routeSegment, string requestPathSegment, IDictionary defaultValues, IDictionary matchedValues) - { - if (String.IsNullOrEmpty(requestPathSegment)) - { - // If there's no data to parse, we must have exactly one parameter segment and no other segments - otherwise no match - - if (routeSegment.Subsegments.Count > 1) - { - return false; - } - - PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[0] as PathParameterSubsegment; - if (parameterSubsegment == null) - { - return false; - } - - // We must have a default value since there's no value in the request URI - object parameterValue; - if (defaultValues.TryGetValue(parameterSubsegment.ParameterName, out parameterValue)) - { - // If there's a default value for this parameter, use that default value - matchedValues.Add(parameterSubsegment.ParameterName, parameterValue); - return true; - } - else - { - // If there's no default value, this segment doesn't match - return false; - } - } - - // Optimize for the common case where there is only one subsegment in the segment - either a parameter or a literal - if (routeSegment.Subsegments.Count == 1) - { - return MatchSingleContentPathSegment(routeSegment.Subsegments[0], requestPathSegment, matchedValues); - } - - // Find last literal segment and get its last index in the string - - int lastIndex = requestPathSegment.Length; - int indexOfLastSegmentUsed = routeSegment.Subsegments.Count - 1; - - PathParameterSubsegment parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value - PathLiteralSubsegment lastLiteral = null; // Keeps track of the left-most literal we've encountered - - while (indexOfLastSegmentUsed >= 0) - { - int newLastIndex = lastIndex; - - PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathParameterSubsegment; - if (parameterSubsegment != null) - { - // Hold on to the parameter so that we can fill it in when we locate the next literal - parameterNeedsValue = parameterSubsegment; - } - else - { - PathLiteralSubsegment literalSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathLiteralSubsegment; - if (literalSubsegment != null) - { - lastLiteral = literalSubsegment; - - int startIndex = lastIndex - 1; - // If we have a pending parameter subsegment, we must leave at least one character for that - if (parameterNeedsValue != null) - { - startIndex--; - } - - if (startIndex < 0) - { - return false; - } - - int indexOfLiteral = requestPathSegment.LastIndexOf(literalSubsegment.Literal, startIndex, StringComparison.OrdinalIgnoreCase); - if (indexOfLiteral == -1) - { - // If we couldn't find this literal index, this segment cannot match - return false; - } - - // If the first subsegment is a literal, it must match at the right-most extent of the request URI. - // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/". - // This check is related to the check we do at the very end of this function. - if (indexOfLastSegmentUsed == (routeSegment.Subsegments.Count - 1)) - { - if ((indexOfLiteral + literalSubsegment.Literal.Length) != requestPathSegment.Length) - { - return false; - } - } - - newLastIndex = indexOfLiteral; - } - else - { - Contract.Assert(false, "Invalid path segment type"); - } - } - - if ((parameterNeedsValue != null) && (((lastLiteral != null) && (parameterSubsegment == null)) || (indexOfLastSegmentUsed == 0))) - { - // If we have a pending parameter that needs a value, grab that value - - int parameterStartIndex; - int parameterTextLength; - - if (lastLiteral == null) - { - if (indexOfLastSegmentUsed == 0) - { - parameterStartIndex = 0; - } - else - { - parameterStartIndex = newLastIndex; - Contract.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above"); - } - parameterTextLength = lastIndex; - } - else - { - // If we're getting a value for a parameter that is somewhere in the middle of the segment - if ((indexOfLastSegmentUsed == 0) && (parameterSubsegment != null)) - { - parameterStartIndex = 0; - parameterTextLength = lastIndex; - } - else - { - parameterStartIndex = newLastIndex + lastLiteral.Literal.Length; - parameterTextLength = lastIndex - parameterStartIndex; - } - } - - string parameterValueString = requestPathSegment.Substring(parameterStartIndex, parameterTextLength); - - if (String.IsNullOrEmpty(parameterValueString)) - { - // If we're here that means we have a segment that contains multiple sub-segments. - // For these segments all parameters must have non-empty values. If the parameter - // has an empty value it's not a match. - return false; - } - else - { - // If there's a value in the segment for this parameter, use the subsegment value - matchedValues.Add(parameterNeedsValue.ParameterName, parameterValueString); - } - - parameterNeedsValue = null; - lastLiteral = null; - } - - lastIndex = newLastIndex; - indexOfLastSegmentUsed--; - } - - // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of - // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment - // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching - // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire* - // request URI in order for it to be a match. - // This check is related to the check we do earlier in this function for LiteralSubsegments. - return (lastIndex == 0) || (routeSegment.Subsegments[0] is PathParameterSubsegment); - } - - private static bool MatchSingleContentPathSegment(PathSubsegment pathSubsegment, string requestPathSegment, IDictionary matchedValues) - { - PathParameterSubsegment parameterSubsegment = pathSubsegment as PathParameterSubsegment; - if (parameterSubsegment == null) - { - // Handle a single literal segment - PathLiteralSubsegment literalSubsegment = pathSubsegment as PathLiteralSubsegment; - Contract.Assert(literalSubsegment != null, "Invalid path segment type"); - return literalSubsegment.Literal.Equals(requestPathSegment, StringComparison.OrdinalIgnoreCase); - } - else - { - // Handle a single parameter segment - matchedValues.Add(parameterSubsegment.ParameterName, requestPathSegment); - return true; - } - } - - private static bool RoutePartsEqual(object a, object b) - { - string sa = a as string; - string sb = b as string; - if (sa != null && sb != null) - { - // For strings do a case-insensitive comparison - return String.Equals(sa, sb, StringComparison.OrdinalIgnoreCase); - } - else - { - if (a != null && b != null) - { - // Explicitly call .Equals() in case it is overridden in the type - return a.Equals(b); - } - else - { - // At least one of them is null. Return true if they both are - return a == b; - } - } - } - - private static string UriEncode(string str) - { - string escape = Uri.EscapeUriString(str); - return Regex.Replace(escape, "([#?])", new MatchEvaluator(EscapeReservedCharacters)); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Legacy/HttpRouteValueDictionary.cs b/src/Microsoft.AspNet.Routing/Legacy/HttpRouteValueDictionary.cs deleted file mode 100644 index f6109cbe33..0000000000 --- a/src/Microsoft.AspNet.Routing/Legacy/HttpRouteValueDictionary.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -#if NET45 -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNet.Routing.Legacy -{ - [SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable", Justification = "This class will never be serialized.")] - public class HttpRouteValueDictionary : Dictionary - { - public HttpRouteValueDictionary() - : base(StringComparer.OrdinalIgnoreCase) - { - } - - public HttpRouteValueDictionary(IDictionary dictionary) - : base(StringComparer.OrdinalIgnoreCase) - { - if (dictionary != null) - { - foreach (KeyValuePair current in dictionary) - { - Add(current.Key, current.Value); - } - } - } - - public HttpRouteValueDictionary(object values) - : base(StringComparer.OrdinalIgnoreCase) - { - IDictionary valuesAsDictionary = values as IDictionary; - if (valuesAsDictionary != null) - { - foreach (KeyValuePair current in valuesAsDictionary) - { - Add(current.Key, current.Value); - } - } - else if (values != null) - { - foreach (PropertyHelper property in PropertyHelper.GetProperties(values)) - { - // Extract the property values from the property helper - // The advantage here is that the property helper caches fast accessors. - Add(property.Name, property.GetValue(values)); - } - } - } - } -} -#endif diff --git a/src/Microsoft.AspNet.Routing/Legacy/PathSegment.cs b/src/Microsoft.AspNet.Routing/Legacy/PathSegment.cs deleted file mode 100644 index db9605b06e..0000000000 --- a/src/Microsoft.AspNet.Routing/Legacy/PathSegment.cs +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNet.Routing.Legacy -{ - // Represents a segment of a URI such as a separator or content - internal abstract class PathSegment - { -#if ROUTE_DEBUGGING - public abstract string LiteralText - { - get; - } -#endif - } - - // Represents a segment of a URI that is not a separator. It contains subsegments such as literals and parameters. - internal sealed class PathContentSegment : PathSegment - { - public PathContentSegment(List subsegments) - { - Subsegments = subsegments; - } - - [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm.")] - public bool IsCatchAll - { - get - { - // TODO: Verify this is correct. Maybe add an assert. - // Performance sensitive - // Caching count is faster for IList - int subsegmentCount = Subsegments.Count; - for (int i = 0; i < subsegmentCount; i++) - { - PathSubsegment seg = Subsegments[i]; - PathParameterSubsegment paramterSubSegment = seg as PathParameterSubsegment; - if (paramterSubSegment != null && paramterSubSegment.IsCatchAll) - { - return true; - } - } - return false; - } - } - - public List Subsegments { get; private set; } - -#if ROUTE_DEBUGGING - public override string LiteralText - { - get - { - List s = new List(); - foreach (PathSubsegment subsegment in Subsegments) - { - s.Add(subsegment.LiteralText); - } - return String.Join(String.Empty, s.ToArray()); - } - } - - public override string ToString() - { - List s = new List(); - foreach (PathSubsegment subsegment in Subsegments) - { - s.Add(subsegment.ToString()); - } - return "[ " + String.Join(", ", s.ToArray()) + " ]"; - } -#endif - } - - // Represents a literal subsegment of a ContentPathSegment - internal sealed class PathLiteralSubsegment : PathSubsegment - { - public PathLiteralSubsegment(string literal) - { - Literal = literal; - } - - public string Literal { get; private set; } - -#if ROUTE_DEBUGGING - public override string LiteralText - { - get - { - return Literal; - } - } - - public override string ToString() - { - return "\"" + Literal + "\""; - } -#endif - } - - // Represents a parameter subsegment of a ContentPathSegment - internal sealed class PathParameterSubsegment : PathSubsegment - { - public PathParameterSubsegment(string parameterName) - { - if (parameterName.StartsWith("*", StringComparison.Ordinal)) - { - ParameterName = parameterName.Substring(1); - IsCatchAll = true; - } - else - { - ParameterName = parameterName; - } - } - - public bool IsCatchAll { get; private set; } - - public string ParameterName { get; private set; } - -#if ROUTE_DEBUGGING - public override string LiteralText - { - get - { - return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}"; - } - } - - public override string ToString() - { - return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}"; - } -#endif - } - - // Represents a "/" separator in a URI - internal sealed class PathSeparatorSegment : PathSegment - { -#if ROUTE_DEBUGGING - public override string LiteralText - { - get - { - return "/"; - } - } - - public override string ToString() - { - return "\"/\""; - } -#endif - } - - // Represents a subsegment of a ContentPathSegment such as a parameter or a literal. - internal abstract class PathSubsegment - { -#if ROUTE_DEBUGGING - public abstract string LiteralText - { - get; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.Routing/Legacy/PropertyHelper.cs b/src/Microsoft.AspNet.Routing/Legacy/PropertyHelper.cs deleted file mode 100644 index ba18b342d6..0000000000 --- a/src/Microsoft.AspNet.Routing/Legacy/PropertyHelper.cs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -#if NET45 -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Linq; -using System.Reflection; - -namespace Microsoft.AspNet.Routing.Legacy -{ - internal class PropertyHelper - { - private static ConcurrentDictionary _reflectionCache = new ConcurrentDictionary(); - - private Func _valueGetter; - - /// - /// Initializes a fast property helper. This constructor does not cache the helper. - /// - [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "This is intended the Name is auto set differently per type and the type is internal")] - public PropertyHelper(PropertyInfo property) - { - Contract.Assert(property != null); - - Name = property.Name; - _valueGetter = MakeFastPropertyGetter(property); - } - - /// - /// Creates a single fast property setter. The result is not cached. - /// - /// propertyInfo to extract the getter for. - /// a fast setter. - /// This method is more memory efficient than a dynamically compiled lambda, and about the same speed. - public static Action MakeFastPropertySetter(PropertyInfo propertyInfo) - where TDeclaringType : class - { - Contract.Assert(propertyInfo != null); - - MethodInfo setMethod = propertyInfo.GetSetMethod(); - - Contract.Assert(setMethod != null); - Contract.Assert(!setMethod.IsStatic); - Contract.Assert(setMethod.GetParameters().Length == 1); - Contract.Assert(!propertyInfo.ReflectedType.IsValueType); - - // Instance methods in the CLR can be turned into static methods where the first parameter - // is open over "this". This parameter is always passed by reference, so we have a code - // path for value types and a code path for reference types. - Type typeInput = propertyInfo.ReflectedType; - Type typeValue = setMethod.GetParameters()[0].ParameterType; - - Delegate callPropertySetterDelegate; - - // Create a delegate TValue -> "TDeclaringType.Property" - var propertySetterAsAction = setMethod.CreateDelegate(typeof(Action<,>).MakeGenericType(typeInput, typeValue)); - var callPropertySetterClosedGenericMethod = _callPropertySetterOpenGenericMethod.MakeGenericMethod(typeInput, typeValue); - callPropertySetterDelegate = Delegate.CreateDelegate(typeof(Action), propertySetterAsAction, callPropertySetterClosedGenericMethod); - - return (Action)callPropertySetterDelegate; - } - - public virtual string Name { get; protected set; } - - public object GetValue(object instance) - { - Contract.Assert(_valueGetter != null, "Must call Initialize before using this object"); - - return _valueGetter(instance); - } - - /// - /// Creates and caches fast property helpers that expose getters for every public get property on the underlying type. - /// - /// the instance to extract property accessors for. - /// a cached array of all public property getters from the underlying type of this instance. - public static PropertyHelper[] GetProperties(object instance) - { - return GetProperties(instance, CreateInstance, _reflectionCache); - } - - /// - /// Creates a single fast property getter. The result is not cached. - /// - /// propertyInfo to extract the getter for. - /// a fast getter. - /// This method is more memory efficient than a dynamically compiled lambda, and about the same speed. - public static Func MakeFastPropertyGetter(PropertyInfo propertyInfo) - { - Contract.Assert(propertyInfo != null); - - MethodInfo getMethod = propertyInfo.GetGetMethod(); - Contract.Assert(getMethod != null); - Contract.Assert(!getMethod.IsStatic); - Contract.Assert(getMethod.GetParameters().Length == 0); - - // Instance methods in the CLR can be turned into static methods where the first parameter - // is open over "this". This parameter is always passed by reference, so we have a code - // path for value types and a code path for reference types. - Type typeInput = getMethod.ReflectedType; - Type typeOutput = getMethod.ReturnType; - - Delegate callPropertyGetterDelegate; - if (typeInput.IsValueType) - { - // Create a delegate (ref TDeclaringType) -> TValue - Delegate propertyGetterAsFunc = getMethod.CreateDelegate(typeof(ByRefFunc<,>).MakeGenericType(typeInput, typeOutput)); - MethodInfo callPropertyGetterClosedGenericMethod = _callPropertyGetterByReferenceOpenGenericMethod.MakeGenericMethod(typeInput, typeOutput); - callPropertyGetterDelegate = Delegate.CreateDelegate(typeof(Func), propertyGetterAsFunc, callPropertyGetterClosedGenericMethod); - } - else - { - // Create a delegate TDeclaringType -> TValue - Delegate propertyGetterAsFunc = getMethod.CreateDelegate(typeof(Func<,>).MakeGenericType(typeInput, typeOutput)); - MethodInfo callPropertyGetterClosedGenericMethod = _callPropertyGetterOpenGenericMethod.MakeGenericMethod(typeInput, typeOutput); - callPropertyGetterDelegate = Delegate.CreateDelegate(typeof(Func), propertyGetterAsFunc, callPropertyGetterClosedGenericMethod); - } - - return (Func)callPropertyGetterDelegate; - } - - private static PropertyHelper CreateInstance(PropertyInfo property) - { - return new PropertyHelper(property); - } - - // Implementation of the fast getter. - private delegate TValue ByRefFunc(ref TDeclaringType arg); - - private static readonly MethodInfo _callPropertyGetterOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertyGetter", BindingFlags.NonPublic | BindingFlags.Static); - private static readonly MethodInfo _callPropertyGetterByReferenceOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertyGetterByReference", BindingFlags.NonPublic | BindingFlags.Static); - - private static object CallPropertyGetter(Func getter, object @this) - { - return getter((TDeclaringType)@this); - } - - private static object CallPropertyGetterByReference(ByRefFunc getter, object @this) - { - TDeclaringType unboxed = (TDeclaringType)@this; - return getter(ref unboxed); - } - - // Implementation of the fast setter. - private static readonly MethodInfo _callPropertySetterOpenGenericMethod = typeof(PropertyHelper).GetMethod("CallPropertySetter", BindingFlags.NonPublic | BindingFlags.Static); - - private static void CallPropertySetter(Action setter, object @this, object value) - { - setter((TDeclaringType)@this, (TValue)value); - } - - protected static PropertyHelper[] GetProperties(object instance, - Func createPropertyHelper, - ConcurrentDictionary cache) - { - // Using an array rather than IEnumerable, as this will be called on the hot path numerous times. - PropertyHelper[] helpers; - - Type type = instance.GetType(); - - if (!cache.TryGetValue(type, out helpers)) - { - // We avoid loading indexed properties using the where statement. - // Indexed properties are not useful (or valid) for grabbing properties off an anonymous object. - IEnumerable properties = type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance) - .Where(prop => prop.GetIndexParameters().Length == 0 && - prop.GetMethod != null); - - var newHelpers = new List(); - - foreach (PropertyInfo property in properties) - { - PropertyHelper propertyHelper = createPropertyHelper(property); - - newHelpers.Add(propertyHelper); - } - - helpers = newHelpers.ToArray(); - cache.TryAdd(type, helpers); - } - - return helpers; - } - } -} -#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Legacy/RouteParser.cs b/src/Microsoft.AspNet.Routing/Legacy/RouteParser.cs deleted file mode 100644 index bfdce5aa83..0000000000 --- a/src/Microsoft.AspNet.Routing/Legacy/RouteParser.cs +++ /dev/null @@ -1,377 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -#define ASPNETWEBAPI - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Linq; - -#if ASPNETWEBAPI -using TParsedRoute = Microsoft.AspNet.Routing.Legacy.HttpParsedRoute; -#else -using ErrorResources = System.Web.Mvc.Properties.MvcResources; -using TParsedRoute = System.Web.Mvc.Routing.ParsedRoute; -#endif - -#if ASPNETWEBAPI -namespace Microsoft.AspNet.Routing.Legacy -#else -namespace System.Web.Mvc.Routing -#endif -{ - // in the MVC case, route parsing is done for AttributeRouting's sake, so that - // it could order the discovered routes before pushing them into the routeCollection, - // where, unfortunately, they would be parsed again. - internal static class RouteParser - { - private static string GetLiteral(string segmentLiteral) - { - // Scan for errant single { and } and convert double {{ to { and double }} to } - - // First we eliminate all escaped braces and then check if any other braces are remaining - string newLiteral = segmentLiteral.Replace("{{", String.Empty).Replace("}}", String.Empty); - if (newLiteral.Contains("{") || newLiteral.Contains("}")) - { - return null; - } - - // If it's a valid format, we unescape the braces - return segmentLiteral.Replace("{{", "{").Replace("}}", "}"); - } - - private static int IndexOfFirstOpenParameter(string segment, int startIndex) - { - // Find the first unescaped open brace - while (true) - { - startIndex = segment.IndexOf('{', startIndex); - if (startIndex == -1) - { - // If there are no more open braces, stop - return -1; - } - if ((startIndex + 1 == segment.Length) || - ((startIndex + 1 < segment.Length) && (segment[startIndex + 1] != '{'))) - { - // If we found an open brace that is followed by a non-open brace, it's - // a parameter delimiter. - // It's also a delimiter if the open brace is the last character - though - // it ends up being being called out as invalid later on. - return startIndex; - } - // Increment by two since we want to skip both the open brace that - // we're on as well as the subsequent character since we know for - // sure that it is part of an escape sequence. - startIndex += 2; - } - } - - internal static bool IsSeparator(string s) - { - return String.Equals(s, "/", StringComparison.Ordinal); - } - - private static bool IsValidParameterName(string parameterName) - { - if (parameterName.Length == 0) - { - return false; - } - - for (int i = 0; i < parameterName.Length; i++) - { - char c = parameterName[i]; - if (c == '/' || c == '{' || c == '}') - { - return false; - } - } - - return true; - } - - internal static bool IsInvalidRouteTemplate(string routeTemplate) - { - return routeTemplate.StartsWith("~", StringComparison.Ordinal) || - routeTemplate.StartsWith("/", StringComparison.Ordinal) || - (routeTemplate.IndexOf('?') != -1); - } - - public static TParsedRoute Parse(string routeTemplate) - { - if (routeTemplate == null) - { - routeTemplate = String.Empty; - } - - if (IsInvalidRouteTemplate(routeTemplate)) - { - throw new ArgumentException(); - } - - IList uriParts = SplitUriToPathSegmentStrings(routeTemplate); - Exception ex = ValidateUriParts(uriParts); - if (ex != null) - { - throw ex; - } - - List pathSegments = SplitUriToPathSegments(uriParts); - - Contract.Assert(uriParts.Count == pathSegments.Count, "The number of string segments should be the same as the number of path segments"); - - return new TParsedRoute(pathSegments); - } - - [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", - Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] - private static List ParseUriSegment(string segment, out Exception exception) - { - int startIndex = 0; - - List pathSubsegments = new List(); - - while (startIndex < segment.Length) - { - int nextParameterStart = IndexOfFirstOpenParameter(segment, startIndex); - if (nextParameterStart == -1) - { - // If there are no more parameters in the segment, capture the remainder as a literal and stop - string lastLiteralPart = GetLiteral(segment.Substring(startIndex)); - if (lastLiteralPart == null) - { - exception = new ArgumentException(); - return null; - } - - if (lastLiteralPart.Length > 0) - { - pathSubsegments.Add(new PathLiteralSubsegment(lastLiteralPart)); - } - break; - } - - int nextParameterEnd = segment.IndexOf('}', nextParameterStart + 1); - if (nextParameterEnd == -1) - { - exception = new ArgumentException(); - return null; - } - - string literalPart = GetLiteral(segment.Substring(startIndex, nextParameterStart - startIndex)); - if (literalPart == null) - { - exception = new ArgumentException(); - return null; - } - - if (literalPart.Length > 0) - { - pathSubsegments.Add(new PathLiteralSubsegment(literalPart)); - } - - string parameterName = segment.Substring(nextParameterStart + 1, nextParameterEnd - nextParameterStart - 1); - pathSubsegments.Add(new PathParameterSubsegment(parameterName)); - - startIndex = nextParameterEnd + 1; - } - - exception = null; - return pathSubsegments; - } - - private static List SplitUriToPathSegments(IList uriParts) - { - List pathSegments = new List(); - - foreach (string pathSegment in uriParts) - { - bool isCurrentPartSeparator = IsSeparator(pathSegment); - if (isCurrentPartSeparator) - { - pathSegments.Add(new PathSeparatorSegment()); - } - else - { - Exception exception; - List subsegments = ParseUriSegment(pathSegment, out exception); - Contract.Assert(exception == null, "This only gets called after the path has been validated, so there should never be an exception here"); - pathSegments.Add(new PathContentSegment(subsegments)); - } - } - return pathSegments; - } - - internal static List SplitUriToPathSegmentStrings(string uri) - { - List parts = new List(); - - if (String.IsNullOrEmpty(uri)) - { - return parts; - } - - int currentIndex = 0; - - // Split the incoming URI into individual parts - while (currentIndex < uri.Length) - { - int indexOfNextSeparator = uri.IndexOf('/', currentIndex); - if (indexOfNextSeparator == -1) - { - // If there are no more separators, the rest of the string is the last part - string finalPart = uri.Substring(currentIndex); - if (finalPart.Length > 0) - { - parts.Add(finalPart); - } - break; - } - - string nextPart = uri.Substring(currentIndex, indexOfNextSeparator - currentIndex); - if (nextPart.Length > 0) - { - parts.Add(nextPart); - } - - Contract.Assert(uri[indexOfNextSeparator] == '/', "The separator char itself should always be a '/'."); - parts.Add("/"); - currentIndex = indexOfNextSeparator + 1; - } - - return parts; - } - - [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm")] - [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", - Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] - private static Exception ValidateUriParts(IList pathSegments) - { - Contract.Assert(pathSegments != null, "The value should always come from SplitUri(), and that function should never return null."); - - HashSet usedParameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); - bool? isPreviousPartSeparator = null; - - bool foundCatchAllParameter = false; - - foreach (string pathSegment in pathSegments) - { - if (foundCatchAllParameter) - { - // If we ever start an iteration of the loop and we've already found a - // catchall parameter then we have an invalid URI format. - return new ArgumentException(); - } - - bool isCurrentPartSeparator; - if (isPreviousPartSeparator == null) - { - // Prime the loop with the first value - isPreviousPartSeparator = IsSeparator(pathSegment); - isCurrentPartSeparator = isPreviousPartSeparator.Value; - } - else - { - isCurrentPartSeparator = IsSeparator(pathSegment); - - // If both the previous part and the current part are separators, it's invalid - if (isCurrentPartSeparator && isPreviousPartSeparator.Value) - { - return new ArgumentException(); - } - - Contract.Assert(isCurrentPartSeparator != isPreviousPartSeparator.Value, "This assert should only happen if both the current and previous parts are non-separators. This should never happen because consecutive non-separators are always parsed as a single part."); - isPreviousPartSeparator = isCurrentPartSeparator; - } - - // If it's not a separator, parse the segment for parameters and validate it - if (!isCurrentPartSeparator) - { - Exception exception; - IList subsegments = ParseUriSegment(pathSegment, out exception); - if (exception != null) - { - return exception; - } - - exception = ValidateUriSegment(subsegments, usedParameterNames); - if (exception != null) - { - return exception; - } - - foundCatchAllParameter = subsegments.Any(seg => (seg is PathParameterSubsegment) && ((PathParameterSubsegment)seg).IsCatchAll); - } - } - return null; - } - - [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", - Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] - private static Exception ValidateUriSegment(IList pathSubsegments, HashSet usedParameterNames) - { - bool segmentContainsCatchAll = false; - - Type previousSegmentType = null; - - foreach (PathSubsegment subsegment in pathSubsegments) - { - if (previousSegmentType != null) - { - if (previousSegmentType == subsegment.GetType()) - { - return new ArgumentException(); - } - } - previousSegmentType = subsegment.GetType(); - - PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment; - if (literalSubsegment != null) - { - // Nothing to validate for literals - everything is valid - } - else - { - PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment; - if (parameterSubsegment != null) - { - string parameterName = parameterSubsegment.ParameterName; - - if (parameterSubsegment.IsCatchAll) - { - segmentContainsCatchAll = true; - } - - // Check for valid characters in the parameter name - if (!IsValidParameterName(parameterName)) - { - return new ArgumentException(); - } - - if (usedParameterNames.Contains(parameterName)) - { - return new ArgumentException(); - } - else - { - usedParameterNames.Add(parameterName); - } - } - else - { - Contract.Assert(false, "Invalid path subsegment type"); - } - } - } - - if (segmentContainsCatchAll && (pathSubsegments.Count != 1)) - { - return new ArgumentException(); - } - - return null; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs b/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs deleted file mode 100644 index 03e32fcd9e..0000000000 --- a/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ - -#if NET45 - -using Owin; - -namespace Microsoft.AspNet.Routing.Owin -{ - public static class AppBuilderExtensions - { - public static IRouteBuilder UseRouter(this IAppBuilder builder) - { - var routes = new RouteTable(); - var engine = new RouteEngine(routes); - - var next = builder.Use(typeof(RouterMiddleware), engine, routes); - - return new RouteBuilder(next, engine, routes); - } - } -} - -#endif diff --git a/src/Microsoft.AspNet.Routing/Owin/AppFuncMiddleware.cs b/src/Microsoft.AspNet.Routing/Owin/AppFuncMiddleware.cs deleted file mode 100644 index fd555067bf..0000000000 --- a/src/Microsoft.AspNet.Routing/Owin/AppFuncMiddleware.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Routing.Owin -{ - internal class AppFuncMiddleware - { - public AppFuncMiddleware(Func, Task> next, Func, Task> appFunc) - { - this.Next = next; - this.AppFunc = appFunc; - } - - private Func, Task> AppFunc - { - get; - set; - } - - private Func, Task> Next - { - get; - set; - } - - public Task Invoke(IDictionary context) - { - return this.AppFunc(context); - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Owin/RouteBuilder.cs b/src/Microsoft.AspNet.Routing/Owin/RouteBuilder.cs deleted file mode 100644 index b6c62d6303..0000000000 --- a/src/Microsoft.AspNet.Routing/Owin/RouteBuilder.cs +++ /dev/null @@ -1,53 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -#if NET45 -using Owin; -#endif - -namespace Microsoft.AspNet.Routing.Owin -{ - internal class RouteBuilder : IRouteBuilder - { -#if NET45 - public RouteBuilder(IAppBuilder builder, IRouteEngine engine, RouteTable routes) - { - this.AppBuilder = builder; - this.Engine = engine; - this.Routes = routes; - } -#else - public RouteBuilder(IRouteEngine engine, RouteTable routes) - { - this.Engine = engine; - this.Routes = routes; - } -#endif - -#if NET45 - public IAppBuilder AppBuilder - { - get; - private set; - } -#endif - - public IRouteEngine Engine - { - get; - private set; - } - - private RouteTable Routes - { - get; - set; - } - - public IRouteEndpoint ForApp(Func, Task>> handlerFactory) - { - return new RouteEndpoint(handlerFactory(), this.Routes); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Owin/RouteEndpoint.cs b/src/Microsoft.AspNet.Routing/Owin/RouteEndpoint.cs deleted file mode 100644 index 36c645c70f..0000000000 --- a/src/Microsoft.AspNet.Routing/Owin/RouteEndpoint.cs +++ /dev/null @@ -1,34 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Routing.Owin -{ - internal class RouteEndpoint : IRouteEndpoint - { - public RouteEndpoint(Func, Task> appFunc, RouteTable routes) - { - this.AppFunc = appFunc; - this.Routes = routes; - } - - public Func, Task> AppFunc - { - get; - private set; - } - - private RouteTable Routes - { - get; - set; - } - - public IRouteEndpoint AddRoute(string name, IRoute route) - { - this.Routes.Add(name, route); - return this; - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs b/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs deleted file mode 100644 index 389e0f146a..0000000000 --- a/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs +++ /dev/null @@ -1,48 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Routing.Owin -{ - public class RouterMiddleware - { - public RouterMiddleware(Func, Task> next, IRouteEngine engine, RouteTable routes) - { - this.Next = next; - this.Engine = engine; - this.Routes = routes; - } - - private IRouteEngine Engine - { - get; - set; - } - - private Func, Task> Next - { - get; - set; - } - - private RouteTable Routes - { - get; - set; - } - - public Task Invoke(IDictionary context) - { - var match = this.Engine.GetMatch(context); - if (match == null) - { - return Next.Invoke(context); - } - else - { - return match.Destination.Invoke(context); - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/PrefixRoute.cs b/src/Microsoft.AspNet.Routing/PrefixRoute.cs deleted file mode 100644 index 9154990ddf..0000000000 --- a/src/Microsoft.AspNet.Routing/PrefixRoute.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; - -namespace Microsoft.AspNet.Routing -{ - public class PrefixRoute : IRoute - { - public PrefixRoute(IRouteEndpoint endpoint, string prefix) - { - this.Endpoint = endpoint; - - if (prefix != null) - { - if (prefix.Length == 0 || prefix[0] != '/') - { - prefix = '/' + prefix; - } - - if (prefix.Length > 1 && prefix[prefix.Length - 1] == '/') - { - prefix = prefix.Substring(0, prefix.Length - 1); - } - - this.Prefix = prefix; - } - } - - private IRouteEndpoint Endpoint - { - get; - set; - } - - private string Prefix - { - get; - set; - } - - public virtual BoundRoute Bind(RouteBindingContext context) - { - return null; - } - - public virtual RouteMatch GetMatch(RoutingContext context) - { - if (this.Prefix == null) - { - return new RouteMatch(this.Endpoint.AppFunc); - } - else if (context.RequestPath.StartsWith(this.Prefix, StringComparison.OrdinalIgnoreCase)) - { - if (context.RequestPath.Length > this.Prefix.Length) - { - char next = context.RequestPath[this.Prefix.Length]; - if (next != '/' && next != '#' && next != '?') - { - return null; - } - } - - return new RouteMatch(this.Endpoint.AppFunc); - } - - return null; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Routing/Properties/AssemblyInfo.cs deleted file mode 100644 index 78c63bee70..0000000000 --- a/src/Microsoft.AspNet.Routing/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Microsoft.AspNet.Routing")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Microsoft.AspNet.Routing")] -[assembly: AssemblyCopyright("Copyright © 2014")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("625c3577-6c55-4ed0-80cc-3b3481955bd0")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Microsoft.AspNet.Routing/RouteBindingContext.cs b/src/Microsoft.AspNet.Routing/RouteBindingContext.cs deleted file mode 100644 index b250b3773c..0000000000 --- a/src/Microsoft.AspNet.Routing/RouteBindingContext.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Microsoft.AspNet.Routing -{ - public class RouteBindingContext - { - public RouteBindingContext(IDictionary context, IDictionary values) - { - this.Context = context; - this.Values = values; - - this.AmbientValues = context.GetRouteMatchValues(); - } - - public IDictionary AmbientValues - { - get; - private set; - } - - public IDictionary Context - { - get; - private set; - } - - public IDictionary Values - { - get; - private set; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs b/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs deleted file mode 100644 index 68efb000fd..0000000000 --- a/src/Microsoft.AspNet.Routing/RouteBuilderExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ - -using System; -#if NET45 -using Owin; -#endif - -using AppFunc = System.Func, System.Threading.Tasks.Task>; - -namespace Microsoft.AspNet.Routing -{ - public static class RouteBuilderExtensions - { -#if NET45 - public static IRouteEndpoint ForApp(this IRouteBuilder routeBuilder, Func handlerBuilder) - { - var builder = handlerBuilder.Invoke(routeBuilder.AppBuilder.New(), routeBuilder.Engine); - var appFunc = (AppFunc)builder.Build(typeof(AppFunc)); - return routeBuilder.ForApp(() => appFunc); - } - - public static IRouteEndpoint ForApp(this IRouteBuilder routeBuilder, Action handlerBuilder) - { - return routeBuilder.ForApp((builder, engine) => { handlerBuilder(builder, engine); return builder; }); - } - - public static IRouteEndpoint ForApp(this IRouteBuilder routeBuilder, Action handlerBuilder) - { - return routeBuilder.ForApp((builder, engine) => { handlerBuilder(builder); return builder; }); - } - - public static IRouteEndpoint ForApp(this IRouteBuilder routeBuilder, Func handlerBuilder) - { - return routeBuilder.ForApp((builder, engine) => handlerBuilder(builder)); - } -#endif - public static IRouteEndpoint ForApp(this IRouteBuilder routeBuilder, Func handlerBuilder) - { - return routeBuilder.ForApp(() => handlerBuilder(null)); - } - - public static IRouteEndpoint ForApp(this IRouteBuilder routeBuilder, AppFunc handler) - { - return routeBuilder.ForApp((Func)((next) => handler)); - } - } -} diff --git a/src/Microsoft.AspNet.Routing/RouteEngine.cs b/src/Microsoft.AspNet.Routing/RouteEngine.cs deleted file mode 100644 index cda55a4067..0000000000 --- a/src/Microsoft.AspNet.Routing/RouteEngine.cs +++ /dev/null @@ -1,72 +0,0 @@ - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; - -namespace Microsoft.AspNet.Routing -{ - public class RouteEngine : IRouteEngine - { - public RouteEngine(RouteTable routes) - { - this.Routes = routes; - } - - public RouteTable Routes - { - get; - private set; - } - - public RouteMatch GetMatch(IDictionary context) - { - var routingContext = new RoutingContext(context); - - for (int i = 0; i < this.Routes.Routes.Count; i++) - { - var route = this.Routes.Routes[i]; - - var match = route.GetMatch(routingContext); - if (match == null) - { - continue; - } - - context.SetRouteMatchValues(match.Values); - context.SetRouteEngine(this); - - return match; - } - - return null; - } - - public BoundRoute GetUrl(string name, IDictionary context, IDictionary values) - { - var bindingContext = new RouteBindingContext(context, values); - - IRoute route; - - if (!String.IsNullOrWhiteSpace(name)) - { - if (Routes.NamedRoutes.TryGetValue(name, out route)) - { - return route.Bind(bindingContext); - } - } - - for (int j = 0; j < this.Routes.Routes.Count; j++) - { - route = this.Routes.Routes[j]; - - var result = route.Bind(bindingContext); - if (result != null) - { - return result; - } - } - - return null; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/RouteEngineExtensions.cs b/src/Microsoft.AspNet.Routing/RouteEngineExtensions.cs deleted file mode 100644 index cf76ec68e5..0000000000 --- a/src/Microsoft.AspNet.Routing/RouteEngineExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ - -using System.Collections.Generic; - -namespace Microsoft.AspNet.Routing -{ - public static class RouteEngineExtensions - { - public static BoundRoute GetUrl(this IRouteEngine engine, IDictionary context, IDictionary values) - { - return engine.GetUrl(null, context, values); - } - - public static BoundRoute GetUrl(this IRouteEngine engine, string name, IDictionary context, IDictionary values) - { - return engine.GetUrl(name, context, values); - } - } -} diff --git a/src/Microsoft.AspNet.Routing/RouteMatch.cs b/src/Microsoft.AspNet.Routing/RouteMatch.cs deleted file mode 100644 index 271bbe9d1d..0000000000 --- a/src/Microsoft.AspNet.Routing/RouteMatch.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Routing -{ - public class RouteMatch - { - public RouteMatch(Func, Task> destination) - : this(destination, null) - { - } - - public RouteMatch(Func, Task> destination, IDictionary values) - { - this.Destination = destination; - this.Values = values; - } - - public Func, Task> Destination - { - get; - private set; - } - - public IDictionary Values - { - get; - private set; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/RouteTable.cs b/src/Microsoft.AspNet.Routing/RouteTable.cs deleted file mode 100644 index 01a3a85560..0000000000 --- a/src/Microsoft.AspNet.Routing/RouteTable.cs +++ /dev/null @@ -1,44 +0,0 @@ - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Routing -{ - public class RouteTable - { - public RouteTable() - { - this.Routes = new List(); - - this.NamedRoutes = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - public List Routes - { - get; - private set; - } - - public IDictionary NamedRoutes - { - get; - private set; - } - - public void Add(IRoute route) - { - this.Add(null, route); - } - - public void Add(string name, IRoute route) - { - this.Routes.Add(route); - - if (!String.IsNullOrEmpty(name)) - { - this.NamedRoutes.Add(name, route); - } - } - } -} diff --git a/src/Microsoft.AspNet.Routing/RoutingContext.cs b/src/Microsoft.AspNet.Routing/RoutingContext.cs deleted file mode 100644 index 029f86e8d0..0000000000 --- a/src/Microsoft.AspNet.Routing/RoutingContext.cs +++ /dev/null @@ -1,34 +0,0 @@ - -using System.Collections.Generic; - -namespace Microsoft.AspNet.Routing -{ - public class RoutingContext - { - public RoutingContext(IDictionary context) - { - this.Context = context; - - this.RequestMethod = (string)context["owin.RequestMethod"]; - this.RequestPath = (string)context["owin.RequestPath"]; - } - - public IDictionary Context - { - get; - private set; - } - - public string RequestMethod - { - get; - private set; - } - - public string RequestPath - { - get; - private set; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs deleted file mode 100644 index 7b34b994a4..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.AspNet.Routing.Legacy; - -namespace Microsoft.AspNet.Routing.Template -{ - public class TemplateRoute : IRoute - { - public TemplateRoute(IRouteEndpoint destination, string template) - : this(destination, template, null, null, null) - { - } - - public TemplateRoute(IRouteEndpoint destination, string template, IDictionary defaults, IDictionary constraints, IDictionary data) - { - this.Destination = destination; - this.Template = template; - this.Defaults = defaults; - this.Constraints = constraints; - this.Data = data; - - this.ParsedRoute = RouteParser.Parse(template); - - this.Initialize(); - } - - private IDictionary Constraints - { - get; - set; - } - - private IDictionary Data - { - get; - set; - } - - private IRouteEndpoint Destination - { - get; - set; - } - - public IDictionary FilterValues - { - get - { - return this.Defaults; - } - } - - private IDictionary Defaults - { - get; - set; - } - - private List> ConstraintsInternal - { - get; - set; - } - - private HttpParsedRoute ParsedRoute - { - get; - set; - } - - private string Template - { - get; - set; - } - - public RouteMatch GetMatch(RoutingContext context) - { - var match = this.ParsedRoute.Match(context, this.Defaults); - if (match == null) - { - return null; - } - - for (int i = 0; i < this.ConstraintsInternal.Count; i++) - { - var kvp = this.ConstraintsInternal[i]; - - object value = null; - if (!String.IsNullOrEmpty(kvp.Key)) - { - match.TryGetValue(kvp.Key, out value); - } - - if (!kvp.Value.MatchInbound(context, match, value)) - { - return null; - } - } - - return new RouteMatch(this.Destination.AppFunc, match); - } - - public bool OnSelected(IDictionary context, RouteMatch match) - { - if (this.Data != null) - { - foreach (var kvp in this.Data) - { - context[kvp.Key] = kvp.Value; - } - } - - return true; - } - - public BoundRoute Bind(RouteBindingContext context) - { - var template = this.ParsedRoute.Bind(context, this.Defaults); - if (template == null) - { - return null; - } - - for (int i = 0; i < this.ConstraintsInternal.Count; i++) - { - var kvp = this.ConstraintsInternal[i]; - - object value = null; - if (!String.IsNullOrEmpty(kvp.Key)) - { - template.Values.TryGetValue(kvp.Key, out value); - } - - if (!kvp.Value.MatchOutbound(context, template.Values, value)) - { - return null; - } - } - - return new BoundRoute(template.BoundTemplate, template.Values); - } - - private void Initialize() - { - this.ConstraintsInternal = new List>(); - - if (this.Constraints == null) - { - return; - } - - foreach (var kvp in this.Constraints) - { - string constraintString; - IConstraint constraint; - - if ((constraintString = kvp.Value as string) != null) - { - // TODO regex constraints - } - else if ((constraint = kvp.Value as IConstraint) != null) - { - this.ConstraintsInternal.Add(new KeyValuePair(kvp.Key, constraint)); - } - else - { - throw new ArgumentException(); - } - } - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRouteExtensions.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRouteExtensions.cs deleted file mode 100644 index dd8968542a..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRouteExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ - -using System.Collections.Generic; - -namespace Microsoft.AspNet.Routing.Template -{ - public static class HttpMethodRouteExtensions - { - public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string template) - { - return endpoint.AddTemplateRoute(null, template, null, null, null); - } - - public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string template, IDictionary defaults) - { - return endpoint.AddTemplateRoute(null, template, defaults, null, null); - } - - public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string template, IDictionary defaults, IDictionary constraints) - { - return endpoint.AddTemplateRoute(null, template, defaults, constraints, null); - } - - public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string template, IDictionary defaults, IDictionary constraints, IDictionary data) - { - return endpoint.AddTemplateRoute(null, template, defaults, constraints, data); - } - - public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string name, string template) - { - return endpoint.AddTemplateRoute(name, template, null, null, null); - } - - public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string name, string template, IDictionary defaults) - { - return endpoint.AddTemplateRoute(name, template, defaults, null, null); - } - - public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string name, string template, IDictionary defaults, IDictionary constraints) - { - return endpoint.AddTemplateRoute(name, template, defaults, constraints, null); - } - - public static IRouteEndpoint AddTemplateRoute(this IRouteEndpoint endpoint, string name, string template, IDictionary defaults, IDictionary constraints, IDictionary data) - { - return endpoint.AddRoute(name, new TemplateRoute(endpoint, template, defaults, constraints, data)); - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Tree/ITreeRouteBuilder.cs b/src/Microsoft.AspNet.Routing/Tree/ITreeRouteBuilder.cs deleted file mode 100644 index 8dac83ac0a..0000000000 --- a/src/Microsoft.AspNet.Routing/Tree/ITreeRouteBuilder.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Routing.Tree -{ - public interface ITreeRouteBuilder - { - void Build(); - - ITreeRouteBuilder Endpoint(IRouteEndpoint endpoint); - - ITreeRouteBuilder Segment(Func segmentBuilder); - } -} diff --git a/src/Microsoft.AspNet.Routing/Tree/ITreeSegment.cs b/src/Microsoft.AspNet.Routing/Tree/ITreeSegment.cs deleted file mode 100644 index 18ef83203c..0000000000 --- a/src/Microsoft.AspNet.Routing/Tree/ITreeSegment.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Routing.Tree -{ - public interface ITreeSegment - { - } -} diff --git a/src/Microsoft.AspNet.Routing/Tree/ParameterSegment.cs b/src/Microsoft.AspNet.Routing/Tree/ParameterSegment.cs deleted file mode 100644 index 60be40c4d5..0000000000 --- a/src/Microsoft.AspNet.Routing/Tree/ParameterSegment.cs +++ /dev/null @@ -1,17 +0,0 @@ - -namespace Microsoft.AspNet.Routing.Tree -{ - internal class ParameterSegment : ITreeSegment - { - public ParameterSegment(string name) - { - this.Name = name; - } - - public string Name - { - get; - private set; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Tree/PathSegment.cs b/src/Microsoft.AspNet.Routing/Tree/PathSegment.cs deleted file mode 100644 index 11c53b3acb..0000000000 --- a/src/Microsoft.AspNet.Routing/Tree/PathSegment.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Microsoft.AspNet.Routing.Tree -{ - internal class PathSegment : ITreeSegment - { - public PathSegment(string path) - { - this.Path = path; - } - - private string Path - { - get; - set; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Tree/TreeRoute.cs b/src/Microsoft.AspNet.Routing/Tree/TreeRoute.cs deleted file mode 100644 index 4d9374e79a..0000000000 --- a/src/Microsoft.AspNet.Routing/Tree/TreeRoute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Routing.Tree -{ - public class TreeRoute : IRoute - { - public BoundRoute Bind(RouteBindingContext context) - { - throw new NotImplementedException(); - } - - public RouteMatch GetMatch(RoutingContext context) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilder.cs b/src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilder.cs deleted file mode 100644 index 01da5be476..0000000000 --- a/src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilder.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Routing.Tree -{ - internal class TreeRouteBuilder : ITreeRouteBuilder - { - public TreeRouteBuilder(IRouteBuilder routeBuilder) - { - this.RouteBuilder = routeBuilder; - } - - public TreeRouteBuilder(Func current, TreeRouteBuilder parent) - { - this.Current = current; - this.Parent = parent; - - this.RouteBuilder = parent.RouteBuilder; - this.RouteEndpoint = parent.RouteEndpoint; - } - - private Func Current - { - get; - set; - } - - private TreeRouteBuilder Parent - { - get; - set; - } - - private IRouteBuilder RouteBuilder - { - get; - set; - } - - private IRouteEndpoint RouteEndpoint - { - get; - set; - } - - public void Build() - { - throw new NotImplementedException(); - } - - public ITreeRouteBuilder Endpoint(IRouteEndpoint endpoint) - { - this.RouteEndpoint = endpoint; - return this; - } - - public ITreeRouteBuilder Segment(Func segmentBuilder) - { - return new TreeRouteBuilder(segmentBuilder, this); - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilderExtensions.cs b/src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilderExtensions.cs deleted file mode 100644 index da457ab33f..0000000000 --- a/src/Microsoft.AspNet.Routing/Tree/TreeRouteBuilderExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ - -using System; - -namespace Microsoft.AspNet.Routing.Tree -{ - public static class TreeRouteBuilderExtensions - { - public static ITreeRouteBuilder Path(this ITreeRouteBuilder routeBuilder, string path) - { - ITreeRouteBuilder current = routeBuilder; - foreach (var segment in path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries)) - { - current = current.Segment(() => new PathSegment(segment)); - } - - return current; - } - - public static ITreeRouteBuilder Parameter(this ITreeRouteBuilder routeBuilder, string name) - { - return routeBuilder.Segment(() => new ParameterSegment(name)); - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Tree/TreeRouteExtensions.cs b/src/Microsoft.AspNet.Routing/Tree/TreeRouteExtensions.cs deleted file mode 100644 index 8540c7006f..0000000000 --- a/src/Microsoft.AspNet.Routing/Tree/TreeRouteExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.Routing.Tree -{ - public static class TreeRouteExtensions - { - public static ITreeRouteBuilder AddTreeRoute(this IRouteBuilder routeBuilder) - { - return new TreeRouteBuilder(routeBuilder); - } - } -} diff --git a/src/Microsoft.AspNet.Routing/project.json b/src/Microsoft.AspNet.Routing/project.json index f0cefe6d12..5f23498788 100644 --- a/src/Microsoft.AspNet.Routing/project.json +++ b/src/Microsoft.AspNet.Routing/project.json @@ -2,12 +2,7 @@ "version": "0.1-alpha-*", "dependencies": {}, "configurations": { - "net45": { - "dependencies": { - "Owin": "1.0", - "Microsoft.Owin": "2.1.0" - } - }, + "net45": { }, "k10" : { } } } \ No newline at end of file From 34af14b07f52754182c445c4190595c49068ea8b Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 28 Jan 2014 14:44:34 -0800 Subject: [PATCH 006/616] Adding solution file --- Routing.sln | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Routing.sln diff --git a/Routing.sln b/Routing.sln new file mode 100644 index 0000000000..66ffeac137 --- /dev/null +++ b/Routing.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.21005.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Routing.net45", "src\Microsoft.AspNet.Routing\Microsoft.AspNet.Routing.net45.csproj", "{16537084-C16D-4885-B90F-18BAC218F85B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0E966C37-7334-4D96-AAF6-9F49FBD166E3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Routing.k10", "src\Microsoft.AspNet.Routing\Microsoft.AspNet.Routing.k10.csproj", "{1366B89C-F603-4088-B313-FA053C51BAC6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {16537084-C16D-4885-B90F-18BAC218F85B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16537084-C16D-4885-B90F-18BAC218F85B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16537084-C16D-4885-B90F-18BAC218F85B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16537084-C16D-4885-B90F-18BAC218F85B}.Release|Any CPU.Build.0 = Release|Any CPU + {1366B89C-F603-4088-B313-FA053C51BAC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1366B89C-F603-4088-B313-FA053C51BAC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1366B89C-F603-4088-B313-FA053C51BAC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1366B89C-F603-4088-B313-FA053C51BAC6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {16537084-C16D-4885-B90F-18BAC218F85B} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3} + {1366B89C-F603-4088-B313-FA053C51BAC6} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3} + EndGlobalSection +EndGlobal From 456067868c53f799dc695f2721ed7d60d34dc5e7 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Tue, 28 Jan 2014 22:20:43 -0800 Subject: [PATCH 007/616] Updated build files. --- .gitattributes | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ .gitignore | 27 +++++++++------------------ build.cmd | 7 +++---- 3 files changed, 62 insertions(+), 22 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..bdaa5ba982 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,50 @@ +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +*.jpg binary +*.png binary +*.gif binary + +*.cs text=auto diff=csharp +*.vb text=auto +*.resx text=auto +*.c text=auto +*.cpp text=auto +*.cxx text=auto +*.h text=auto +*.hxx text=auto +*.py text=auto +*.rb text=auto +*.java text=auto +*.html text=auto +*.htm text=auto +*.css text=auto +*.scss text=auto +*.sass text=auto +*.less text=auto +*.js text=auto +*.lisp text=auto +*.clj text=auto +*.sql text=auto +*.php text=auto +*.lua text=auto +*.m text=auto +*.asm text=auto +*.erl text=auto +*.fs text=auto +*.fsx text=auto +*.hs text=auto + +*.csproj text=auto +*.vbproj text=auto +*.fsproj text=auto +*.dbproj text=auto +*.sln text=auto eol=crlf diff --git a/.gitignore b/.gitignore index 5cf7c13c7e..2554a1fc23 100644 --- a/.gitignore +++ b/.gitignore @@ -1,31 +1,22 @@ [Oo]bj/ [Bb]in/ -*.xap +TestResults/ +.nuget/ +_ReSharper.*/ +packages/ +artifacts/ +PublishProfiles/ *.user -/TestResults -*.vspscc -*.vssscc *.suo *.cache *.docstates _ReSharper.* -*.csproj.user -*[Rr]e[Ss]harper.user -_ReSharper.*/ -packages/* -artifacts/* -msbuild.log -PublishProfiles/ +nuget.exe +*net45.csproj +*k10.csproj *.psess *.vsp *.pidb *.userprefs *DS_Store *.ncrunchsolution -*.log -*.vspx -/.symbols -nuget.exe -build/ -*net45.csproj -*k10.csproj \ No newline at end of file diff --git a/build.cmd b/build.cmd index c3b2462019..d54931bc8f 100644 --- a/build.cmd +++ b/build.cmd @@ -7,10 +7,9 @@ md .nuget @powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://www.nuget.org/nuget.exe' -OutFile '.nuget\NuGet.exe'" :restore -IF EXIST build goto run +IF EXIST packages\KoreBuild goto run .nuget\NuGet.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre -xcopy packages\KoreBuild\build build\ /Y -.nuget\NuGet.exe install Sake -version 0.2 -o packages +.nuget\NuGet.exe install Sake -version 0.2 -o packages -ExcludeVersion :run -packages\Sake.0.2\tools\Sake.exe -I build -f makefile.shade %* +packages\Sake\tools\Sake.exe -I packages\KoreBuild\build -f makefile.shade %* From 9f9f92d18a672ea86639b26afe856ed8bbf520cc Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 29 Jan 2014 11:42:13 -0800 Subject: [PATCH 008/616] Adding skeleton sample and test project --- Routing.sln | 18 ++++++++++++++++++ samples/RoutingSample/project.json | 13 +++++++++++++ src/Microsoft.AspNet.Routing/project.json | 6 +++++- .../project.json | 13 +++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 samples/RoutingSample/project.json create mode 100644 test/Microsoft.AspNet.Routing.Tests/project.json diff --git a/Routing.sln b/Routing.sln index 66ffeac137..9f44962866 100644 --- a/Routing.sln +++ b/Routing.sln @@ -9,6 +9,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0E966C37-733 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Routing.k10", "src\Microsoft.AspNet.Routing\Microsoft.AspNet.Routing.k10.csproj", "{1366B89C-F603-4088-B313-FA053C51BAC6}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{C3ADD55B-B9C7-4061-8AD4-6A70D1AE3B2E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoutingSample.net45", "samples\RoutingSample\RoutingSample.net45.csproj", "{121DC7B4-E29B-45E1-BF7E-314842F99A0D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{95359B4B-4C85-4B44-A75B-0621905C4CF6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Routing.Tests.net45", "test\Microsoft.AspNet.Routing.Tests\Microsoft.AspNet.Routing.Tests.net45.csproj", "{3775A966-0876-45C3-A67F-0E544BC48D55}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -23,6 +31,14 @@ Global {1366B89C-F603-4088-B313-FA053C51BAC6}.Debug|Any CPU.Build.0 = Debug|Any CPU {1366B89C-F603-4088-B313-FA053C51BAC6}.Release|Any CPU.ActiveCfg = Release|Any CPU {1366B89C-F603-4088-B313-FA053C51BAC6}.Release|Any CPU.Build.0 = Release|Any CPU + {121DC7B4-E29B-45E1-BF7E-314842F99A0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {121DC7B4-E29B-45E1-BF7E-314842F99A0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {121DC7B4-E29B-45E1-BF7E-314842F99A0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {121DC7B4-E29B-45E1-BF7E-314842F99A0D}.Release|Any CPU.Build.0 = Release|Any CPU + {3775A966-0876-45C3-A67F-0E544BC48D55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3775A966-0876-45C3-A67F-0E544BC48D55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3775A966-0876-45C3-A67F-0E544BC48D55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3775A966-0876-45C3-A67F-0E544BC48D55}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -30,5 +46,7 @@ Global GlobalSection(NestedProjects) = preSolution {16537084-C16D-4885-B90F-18BAC218F85B} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3} {1366B89C-F603-4088-B313-FA053C51BAC6} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3} + {121DC7B4-E29B-45E1-BF7E-314842F99A0D} = {C3ADD55B-B9C7-4061-8AD4-6A70D1AE3B2E} + {3775A966-0876-45C3-A67F-0E544BC48D55} = {95359B4B-4C85-4B44-A75B-0621905C4CF6} EndGlobalSection EndGlobal diff --git a/samples/RoutingSample/project.json b/samples/RoutingSample/project.json new file mode 100644 index 0000000000..5168efba28 --- /dev/null +++ b/samples/RoutingSample/project.json @@ -0,0 +1,13 @@ +{ + "version": "0.1-alpha-*", + "dependencies": { + "Microsoft.AspNet.Routing" : "" + }, + "configurations": { + "net45": { + "dependencies": { + "Owin": "1.0" + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/project.json b/src/Microsoft.AspNet.Routing/project.json index 5f23498788..9fdf6df436 100644 --- a/src/Microsoft.AspNet.Routing/project.json +++ b/src/Microsoft.AspNet.Routing/project.json @@ -2,7 +2,11 @@ "version": "0.1-alpha-*", "dependencies": {}, "configurations": { - "net45": { }, + "net45": { + "dependencies": { + "Owin": "1.0" + } + }, "k10" : { } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Routing.Tests/project.json b/test/Microsoft.AspNet.Routing.Tests/project.json new file mode 100644 index 0000000000..5168efba28 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/project.json @@ -0,0 +1,13 @@ +{ + "version": "0.1-alpha-*", + "dependencies": { + "Microsoft.AspNet.Routing" : "" + }, + "configurations": { + "net45": { + "dependencies": { + "Owin": "1.0" + } + } + } +} \ No newline at end of file From 84f4a2d047893ff9c141e93c83fb64e3c2fee6a9 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 29 Jan 2014 14:29:10 -0800 Subject: [PATCH 009/616] Initial mock up of key interfaces for routing. This is heavily based right now on raw Owin as the request/response model since that will work for building samples/tests, and doesn't require ifdefs in the code for k10. Once it's feasible, I'll port this to use the Microsoft.AspNet.Abstractions types. The sample here includes some basic route/endpoint implementations just to prove that the route engine works in naive scenarios. These might live on in the future in some form, but for now it's just part of the sample. The next step is to integrate this with WebFX and start developing the integration/configuration story for WebFX. --- samples/RoutingSample/OwinRouteEndpoint.cs | 24 +++++++++ samples/RoutingSample/PrefixRoute.cs | 54 +++++++++++++++++++ samples/RoutingSample/Program.cs | 22 ++++++++ samples/RoutingSample/Startup.cs | 35 ++++++++++++ samples/RoutingSample/project.json | 9 ++-- .../DefaultRouteCollection.cs | 26 +++++++++ .../DefaultRouteEngine.cs | 41 ++++++++++++++ src/Microsoft.AspNet.Routing/IRoute.cs | 9 ++++ .../IRouteCollection.cs | 19 +++++++ .../IRouteEndpoint.cs | 12 +++++ src/Microsoft.AspNet.Routing/IRouteEngine.cs | 12 +++++ .../Owin/AppBuilderExtensions.cs | 23 ++++++++ .../Owin/RouterMiddleware.cs | 41 ++++++++++++++ src/Microsoft.AspNet.Routing/RouteContext.cs | 28 ++++++++++ src/Microsoft.AspNet.Routing/RouteMatch.cs | 36 +++++++++++++ src/Microsoft.AspNet.Routing/project.json | 7 +-- 16 files changed, 392 insertions(+), 6 deletions(-) create mode 100644 samples/RoutingSample/OwinRouteEndpoint.cs create mode 100644 samples/RoutingSample/PrefixRoute.cs create mode 100644 samples/RoutingSample/Program.cs create mode 100644 samples/RoutingSample/Startup.cs create mode 100644 src/Microsoft.AspNet.Routing/DefaultRouteCollection.cs create mode 100644 src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs create mode 100644 src/Microsoft.AspNet.Routing/IRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/IRouteCollection.cs create mode 100644 src/Microsoft.AspNet.Routing/IRouteEndpoint.cs create mode 100644 src/Microsoft.AspNet.Routing/IRouteEngine.cs create mode 100644 src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs create mode 100644 src/Microsoft.AspNet.Routing/RouteContext.cs create mode 100644 src/Microsoft.AspNet.Routing/RouteMatch.cs diff --git a/samples/RoutingSample/OwinRouteEndpoint.cs b/samples/RoutingSample/OwinRouteEndpoint.cs new file mode 100644 index 0000000000..f903bfee2f --- /dev/null +++ b/samples/RoutingSample/OwinRouteEndpoint.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.Routing; + +namespace RoutingSample +{ + internal class OwinRouteEndpoint : IRouteEndpoint + { + private readonly Func, Task> _appFunc; + + public OwinRouteEndpoint(Func, Task> appFunc) + { + _appFunc = appFunc; + } + + public Task Invoke(IDictionary context) + { + return _appFunc(context); + } + } +} diff --git a/samples/RoutingSample/PrefixRoute.cs b/samples/RoutingSample/PrefixRoute.cs new file mode 100644 index 0000000000..fe1f4f092a --- /dev/null +++ b/samples/RoutingSample/PrefixRoute.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Routing; + +namespace RoutingSample +{ + internal class PrefixRoute : IRoute + { + private readonly IRouteEndpoint _endpoint; + private readonly string _prefix; + + public PrefixRoute(IRouteEndpoint endpoint, string prefix) + { + _endpoint = endpoint; + + if (prefix == null) + { + prefix = "/"; + } + else if (prefix.Length > 0 && prefix[0] != '/') + { + // owin.RequestPath starts with a / + prefix = "/" + prefix; + } + + if (prefix.Length > 1 && prefix[prefix.Length - 1] == '/') + { + prefix = prefix.Substring(0, prefix.Length - 1); + } + + _prefix = prefix; + } + + public RouteMatch Match(RouteContext context) + { + if (context.RequestPath.StartsWith(_prefix, StringComparison.OrdinalIgnoreCase)) + { + if (context.RequestPath.Length > _prefix.Length) + { + char next = context.RequestPath[_prefix.Length]; + if (next != '/' && next != '#' && next != '?') + { + return null; + } + } + + return new RouteMatch(_endpoint); + } + + return null; + } + } +} diff --git a/samples/RoutingSample/Program.cs b/samples/RoutingSample/Program.cs new file mode 100644 index 0000000000..a5b500e9e1 --- /dev/null +++ b/samples/RoutingSample/Program.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.Owin.Hosting; + +namespace RoutingSample +{ + internal static class Program + { + public static void Main(string[] args) + { + string url = "http://localhost:30000/"; + using (WebApp.Start(url)) + { + Console.WriteLine("Listening on {0}", url); + Console.WriteLine("Press ENTER to quit"); + + Console.ReadLine(); + } + } + } +} diff --git a/samples/RoutingSample/Startup.cs b/samples/RoutingSample/Startup.cs new file mode 100644 index 0000000000..2f6f4d2d1a --- /dev/null +++ b/samples/RoutingSample/Startup.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Routing.Owin; +using Owin; + +namespace RoutingSample +{ + internal class Startup + { + public void Configuration(IAppBuilder appBuilder) + { + var routes = appBuilder.UseRouter(); + + OwinRouteEndpoint endpoint1 = new OwinRouteEndpoint(async (context) => await WriteToBodyAsync(context, "match1")); + OwinRouteEndpoint endpoint2 = new OwinRouteEndpoint(async (context) => await WriteToBodyAsync(context, "Hello, World!")); + + routes.Add(new PrefixRoute(endpoint1, "api/store")); + routes.Add(new PrefixRoute(endpoint1, "api/checkout")); + routes.Add(new PrefixRoute(endpoint2, "hello/world")); + routes.Add(new PrefixRoute(endpoint1, "")); + } + + private static async Task WriteToBodyAsync(IDictionary context, string text) + { + var stream = (Stream)context["owin.ResponseBody"]; + + byte[] bytes = Encoding.UTF8.GetBytes(text); + await stream.WriteAsync(bytes, 0, bytes.Length); + } + } +} diff --git a/samples/RoutingSample/project.json b/samples/RoutingSample/project.json index 5168efba28..71c3fc2226 100644 --- a/samples/RoutingSample/project.json +++ b/samples/RoutingSample/project.json @@ -5,9 +5,12 @@ }, "configurations": { "net45": { - "dependencies": { - "Owin": "1.0" - } + "dependencies": { + "Owin": "1.0", + "Microsoft.Owin" : "2.1.0", + "Microsoft.Owin.Host.HttpListener" : "2.1.0", + "Microsoft.Owin.Hosting" : "2.1.0" + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/DefaultRouteCollection.cs b/src/Microsoft.AspNet.Routing/DefaultRouteCollection.cs new file mode 100644 index 0000000000..81616ed126 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/DefaultRouteCollection.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public class DefaultRouteCollection : IRouteCollection + { + private readonly List _routes = new List(); + + public IRoute this[int index] + { + get { return _routes[index]; } + } + + public int Count + { + get { return _routes.Count; } + } + + public void Add(IRoute route) + { + _routes.Add(route); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs b/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs new file mode 100644 index 0000000000..c4a56b458b --- /dev/null +++ b/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing +{ + internal class DefaultRouteEngine : IRouteEngine + { + public DefaultRouteEngine(IRouteCollection routes) + { + Routes = routes; + } + + public IRouteCollection Routes + { + get; + private set; + } + + public async Task Invoke(IDictionary context) + { + RouteContext routeContext = new RouteContext(context); + + for (int i = 0; i < Routes.Count; i++) + { + IRoute route = Routes[i]; + + RouteMatch match = route.Match(routeContext); + if (match != null) + { + await match.Endpoint.Invoke(context); + return true; + } + } + + return false; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/IRoute.cs b/src/Microsoft.AspNet.Routing/IRoute.cs new file mode 100644 index 0000000000..561299ca4e --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRoute.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing +{ + public interface IRoute + { + RouteMatch Match(RouteContext context); + } +} diff --git a/src/Microsoft.AspNet.Routing/IRouteCollection.cs b/src/Microsoft.AspNet.Routing/IRouteCollection.cs new file mode 100644 index 0000000000..85b9faf950 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRouteCollection.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing +{ + public interface IRouteCollection + { + IRoute this[int index] + { + get; + } + + int Count + { + get; + } + + void Add(IRoute route); + } +} diff --git a/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs b/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs new file mode 100644 index 0000000000..badb71f73b --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing +{ + public interface IRouteEndpoint + { + Task Invoke(IDictionary context); + } +} diff --git a/src/Microsoft.AspNet.Routing/IRouteEngine.cs b/src/Microsoft.AspNet.Routing/IRouteEngine.cs new file mode 100644 index 0000000000..e813f79b42 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRouteEngine.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing +{ + public interface IRouteEngine + { + Task Invoke(IDictionary context); + } +} diff --git a/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs b/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs new file mode 100644 index 0000000000..06e70a5ca1 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +#if NET45 + +using Owin; + +namespace Microsoft.AspNet.Routing.Owin +{ + public static class AppBuilderExtensions + { + public static IRouteCollection UseRouter(this IAppBuilder builder) + { + var routes = new DefaultRouteCollection(); + var engine = new DefaultRouteEngine(routes); + + builder.Use(typeof(RouterMiddleware), engine); + + return routes; + } + } +} + +#endif diff --git a/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs b/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs new file mode 100644 index 0000000000..21e4cf6904 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +#if NET45 + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing.Owin +{ + public class RouterMiddleware + { + public RouterMiddleware(Func, Task> next, IRouteEngine engine) + { + Next = next; + Engine = engine; + } + + private IRouteEngine Engine + { + get; + set; + } + + private Func, Task> Next + { + get; + set; + } + + public async Task Invoke(IDictionary context) + { + if (!(await Engine.Invoke(context))) + { + await Next.Invoke(context); + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/RouteContext.cs b/src/Microsoft.AspNet.Routing/RouteContext.cs new file mode 100644 index 0000000000..fb45e7dd6d --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteContext.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public sealed class RouteContext + { + public RouteContext(IDictionary context) + { + Context = context; + + RequestPath = (string)context["owin.RequestPath"]; + } + + public IDictionary Context + { + get; + private set; + } + + public string RequestPath + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RouteMatch.cs b/src/Microsoft.AspNet.Routing/RouteMatch.cs new file mode 100644 index 0000000000..3bea932dd2 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteMatch.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + /// + /// The result of matching a route. Includes an to invoke and an optional collection of + /// captured values. + /// + public sealed class RouteMatch + { + public RouteMatch(IRouteEndpoint endpoint) + : this(endpoint, null) + { + } + + public RouteMatch(IRouteEndpoint endpoint, IDictionary values) + { + Endpoint = endpoint; + Values = values; + } + + public IRouteEndpoint Endpoint + { + get; + private set; + } + + public IDictionary Values + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/project.json b/src/Microsoft.AspNet.Routing/project.json index 9fdf6df436..9d789b9e65 100644 --- a/src/Microsoft.AspNet.Routing/project.json +++ b/src/Microsoft.AspNet.Routing/project.json @@ -1,12 +1,13 @@ { "version": "0.1-alpha-*", - "dependencies": {}, + "dependencies": { + }, "configurations": { "net45": { - "dependencies": { + "dependencies": { "Owin": "1.0" } - }, + }, "k10" : { } } } \ No newline at end of file From a86624d3869d5e55b2886c8a5dd80e07a36bd3f2 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 30 Jan 2014 11:22:18 -0800 Subject: [PATCH 010/616] change to non-static entrypoint, which klr is looking for --- samples/RoutingSample/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/RoutingSample/Program.cs b/samples/RoutingSample/Program.cs index a5b500e9e1..9ae2ef4a73 100644 --- a/samples/RoutingSample/Program.cs +++ b/samples/RoutingSample/Program.cs @@ -5,7 +5,7 @@ using Microsoft.Owin.Hosting; namespace RoutingSample { - internal static class Program + public class Program { public static void Main(string[] args) { From d737f1541394d38c582c741b33aaaeefd9979fde Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sat, 1 Feb 2014 00:51:44 -0800 Subject: [PATCH 011/616] Added global.json --- global.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 global.json diff --git a/global.json b/global.json new file mode 100644 index 0000000000..840c36f6ad --- /dev/null +++ b/global.json @@ -0,0 +1,3 @@ +{ + "sources": ["src"] +} \ No newline at end of file From 5676745562537c854e82429063abfa1603781cc0 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Sun, 2 Feb 2014 08:25:47 -0800 Subject: [PATCH 012/616] Updatng build.cmd to use cached NuGet.exe --- build.cmd | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/build.cmd b/build.cmd index d54931bc8f..7045ee1f84 100644 --- a/build.cmd +++ b/build.cmd @@ -1,10 +1,18 @@ @echo off cd %~dp0 -IF EXIST .nuget\NuGet.exe goto restore +SETLOCAL +SET CACHED_NUGET=%LocalAppData%\NuGet\NuGet.exe + +IF EXIST %CACHED_NUGET% goto copynuget echo Downloading latest version of NuGet.exe... +IF NOT EXIST %LocalAppData%\NuGet md %LocalAppData%\NuGet +@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://www.nuget.org/nuget.exe' -OutFile '%CACHED_NUGET%'" + +:copynuget +IF EXIST .nuget\nuget.exe goto restore md .nuget -@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://www.nuget.org/nuget.exe' -OutFile '.nuget\NuGet.exe'" +copy %CACHED_NUGET% .nuget\nuget.exe > nul :restore IF EXIST packages\KoreBuild goto run From c796188231edb9cef7178cc6ee52e742e3231bdd Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 3 Feb 2014 15:56:44 -0800 Subject: [PATCH 013/616] integrating abstractions with routing --- samples/RoutingSample/AppBuilderExtensions.cs | 18 +++++++++++++++++ samples/RoutingSample/OwinRouteEndpoint.cs | 7 +++++-- samples/RoutingSample/Startup.cs | 5 +++++ samples/RoutingSample/project.json | 17 ++++++++++------ .../DefaultRouteEngine.cs | 9 +++++---- .../IRouteEndpoint.cs | 4 ++-- src/Microsoft.AspNet.Routing/IRouteEngine.cs | 4 ++-- src/Microsoft.AspNet.Routing/IRouteValues.cs | 14 +++++++++++++ ...lderExtensions.cs => BuilderExtensions.cs} | 11 ++++------ .../Owin/RouterMiddleware.cs | 13 ++++-------- src/Microsoft.AspNet.Routing/RouteContext.cs | 8 ++++---- src/Microsoft.AspNet.Routing/RouteValues.cs | 20 +++++++++++++++++++ src/Microsoft.AspNet.Routing/project.json | 7 ++----- 13 files changed, 96 insertions(+), 41 deletions(-) create mode 100644 samples/RoutingSample/AppBuilderExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/IRouteValues.cs rename src/Microsoft.AspNet.Routing/Owin/{AppBuilderExtensions.cs => BuilderExtensions.cs} (61%) create mode 100644 src/Microsoft.AspNet.Routing/RouteValues.cs diff --git a/samples/RoutingSample/AppBuilderExtensions.cs b/samples/RoutingSample/AppBuilderExtensions.cs new file mode 100644 index 0000000000..bd27fdde8f --- /dev/null +++ b/samples/RoutingSample/AppBuilderExtensions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Routing.Owin; +using Owin; + +namespace RoutingSample +{ + public static class AppBuilderExtensions + { + public static IRouteCollection UseRouter(this IAppBuilder app) + { + IRouteCollection routes = null; + app.UseBuilder((b) => routes = b.UseRouter()); + return routes; + } + } +} diff --git a/samples/RoutingSample/OwinRouteEndpoint.cs b/samples/RoutingSample/OwinRouteEndpoint.cs index f903bfee2f..734a81b9da 100644 --- a/samples/RoutingSample/OwinRouteEndpoint.cs +++ b/samples/RoutingSample/OwinRouteEndpoint.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.PipelineCore.Owin; using Microsoft.AspNet.Routing; namespace RoutingSample @@ -16,9 +18,10 @@ namespace RoutingSample _appFunc = appFunc; } - public Task Invoke(IDictionary context) + public Task Invoke(HttpContext context) { - return _appFunc(context); + var owinContext = context.GetFeature().Environment; + return _appFunc(owinContext); } } } diff --git a/samples/RoutingSample/Startup.cs b/samples/RoutingSample/Startup.cs index 2f6f4d2d1a..610d56f619 100644 --- a/samples/RoutingSample/Startup.cs +++ b/samples/RoutingSample/Startup.cs @@ -6,6 +6,10 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Routing.Owin; using Owin; +using Microsoft.AspNet.PipelineCore.Owin; +using Microsoft.AspNet.Routing; +using System; +using Microsoft.AspNet.Abstractions; namespace RoutingSample { @@ -14,6 +18,7 @@ namespace RoutingSample public void Configuration(IAppBuilder appBuilder) { var routes = appBuilder.UseRouter(); + OwinRouteEndpoint endpoint1 = new OwinRouteEndpoint(async (context) => await WriteToBodyAsync(context, "match1")); OwinRouteEndpoint endpoint2 = new OwinRouteEndpoint(async (context) => await WriteToBodyAsync(context, "Hello, World!")); diff --git a/samples/RoutingSample/project.json b/samples/RoutingSample/project.json index 71c3fc2226..35f33aab61 100644 --- a/samples/RoutingSample/project.json +++ b/samples/RoutingSample/project.json @@ -1,16 +1,21 @@ { "version": "0.1-alpha-*", "dependencies": { + "Microsoft.AspNet.Abstractions" : "0.1-alpha-*", + "Microsoft.AspNet.AppBuilderSupport": "0.1-alpha-*", + "Microsoft.AspNet.FeatureModel": "0.1-alpha-*", + "Microsoft.AspNet.HttpFeature": "0.1-alpha-*", + "Microsoft.AspNet.PipelineCore": "0.1-alpha-*", "Microsoft.AspNet.Routing" : "" }, "configurations": { "net45": { - "dependencies": { - "Owin": "1.0", - "Microsoft.Owin" : "2.1.0", - "Microsoft.Owin.Host.HttpListener" : "2.1.0", - "Microsoft.Owin.Hosting" : "2.1.0" - } + "dependencies": { + "Owin": "1.0", + "Microsoft.Owin" : "2.1.0", + "Microsoft.Owin.Host.HttpListener" : "2.1.0", + "Microsoft.Owin.Hosting" : "2.1.0" + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs b/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs index c4a56b458b..07c4a3f517 100644 --- a/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs +++ b/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; namespace Microsoft.AspNet.Routing { - internal class DefaultRouteEngine : IRouteEngine + public class DefaultRouteEngine : IRouteEngine { public DefaultRouteEngine(IRouteCollection routes) { @@ -19,7 +18,7 @@ namespace Microsoft.AspNet.Routing private set; } - public async Task Invoke(IDictionary context) + public async Task Invoke(HttpContext context) { RouteContext routeContext = new RouteContext(context); @@ -30,6 +29,8 @@ namespace Microsoft.AspNet.Routing RouteMatch match = route.Match(routeContext); if (match != null) { + context.SetFeature(new RouteValues(match.Values)); + await match.Endpoint.Invoke(context); return true; } diff --git a/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs b/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs index badb71f73b..b5ebcc284d 100644 --- a/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs +++ b/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. -using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; namespace Microsoft.AspNet.Routing { public interface IRouteEndpoint { - Task Invoke(IDictionary context); + Task Invoke(HttpContext context); } } diff --git a/src/Microsoft.AspNet.Routing/IRouteEngine.cs b/src/Microsoft.AspNet.Routing/IRouteEngine.cs index e813f79b42..d54c7dc581 100644 --- a/src/Microsoft.AspNet.Routing/IRouteEngine.cs +++ b/src/Microsoft.AspNet.Routing/IRouteEngine.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. -using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; namespace Microsoft.AspNet.Routing { public interface IRouteEngine { - Task Invoke(IDictionary context); + Task Invoke(HttpContext context); } } diff --git a/src/Microsoft.AspNet.Routing/IRouteValues.cs b/src/Microsoft.AspNet.Routing/IRouteValues.cs new file mode 100644 index 0000000000..7e44c9fc86 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRouteValues.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public interface IRouteValues + { + IDictionary Values + { + get; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs b/src/Microsoft.AspNet.Routing/Owin/BuilderExtensions.cs similarity index 61% rename from src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs rename to src/Microsoft.AspNet.Routing/Owin/BuilderExtensions.cs index 06e70a5ca1..999517009b 100644 --- a/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs +++ b/src/Microsoft.AspNet.Routing/Owin/BuilderExtensions.cs @@ -1,23 +1,20 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. -#if NET45 - -using Owin; +using Microsoft.AspNet.Abstractions; namespace Microsoft.AspNet.Routing.Owin { - public static class AppBuilderExtensions + public static class BuilderExtensions { - public static IRouteCollection UseRouter(this IAppBuilder builder) + public static IRouteCollection UseRouter(this IBuilder builder) { var routes = new DefaultRouteCollection(); var engine = new DefaultRouteEngine(routes); - builder.Use(typeof(RouterMiddleware), engine); + builder.Use((next) => new RouterMiddleware(next, engine).Invoke); return routes; } } } -#endif diff --git a/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs b/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs index 21e4cf6904..d1298faf09 100644 --- a/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs +++ b/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs @@ -1,16 +1,13 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. -#if NET45 - -using System; -using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; namespace Microsoft.AspNet.Routing.Owin { public class RouterMiddleware { - public RouterMiddleware(Func, Task> next, IRouteEngine engine) + public RouterMiddleware(RequestDelegate next, IRouteEngine engine) { Next = next; Engine = engine; @@ -22,13 +19,13 @@ namespace Microsoft.AspNet.Routing.Owin set; } - private Func, Task> Next + private RequestDelegate Next { get; set; } - public async Task Invoke(IDictionary context) + public async Task Invoke(HttpContext context) { if (!(await Engine.Invoke(context))) { @@ -37,5 +34,3 @@ namespace Microsoft.AspNet.Routing.Owin } } } - -#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/RouteContext.cs b/src/Microsoft.AspNet.Routing/RouteContext.cs index fb45e7dd6d..2b8df23106 100644 --- a/src/Microsoft.AspNet.Routing/RouteContext.cs +++ b/src/Microsoft.AspNet.Routing/RouteContext.cs @@ -1,19 +1,19 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. -using System.Collections.Generic; +using Microsoft.AspNet.Abstractions; namespace Microsoft.AspNet.Routing { public sealed class RouteContext { - public RouteContext(IDictionary context) + public RouteContext(HttpContext context) { Context = context; - RequestPath = (string)context["owin.RequestPath"]; + RequestPath = context.Request.Path.Value; } - public IDictionary Context + public HttpContext Context { get; private set; diff --git a/src/Microsoft.AspNet.Routing/RouteValues.cs b/src/Microsoft.AspNet.Routing/RouteValues.cs new file mode 100644 index 0000000000..da7dc4c85f --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteValues.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + internal class RouteValues : IRouteValues + { + public RouteValues(IDictionary values) + { + Values = values; + } + + public IDictionary Values + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/project.json b/src/Microsoft.AspNet.Routing/project.json index 9d789b9e65..ac508feb48 100644 --- a/src/Microsoft.AspNet.Routing/project.json +++ b/src/Microsoft.AspNet.Routing/project.json @@ -1,13 +1,10 @@ { "version": "0.1-alpha-*", "dependencies": { + "Microsoft.AspNet.Abstractions" : "0.1-alpha-*" }, "configurations": { - "net45": { - "dependencies": { - "Owin": "1.0" - } - }, + "net45": { }, "k10" : { } } } \ No newline at end of file From 0eb5ff0b87d15acc5ac9bcd6679a9367bc8dca3e Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 3 Feb 2014 15:56:44 -0800 Subject: [PATCH 014/616] integrating abstractions with routing --- Routing.sln | 7 ++++++ .../RoutingSample/HttpContextRouteEndpoint.cs | 25 +++++++++++++++++++ samples/RoutingSample/OwinRouteEndpoint.cs | 8 +++++- samples/RoutingSample/Program.cs | 2 ++ samples/RoutingSample/Startup.cs | 21 ++++++++++++---- samples/RoutingSample/project.json | 9 +++---- .../DefaultRouteEngine.cs | 7 ++++-- .../IRouteEndpoint.cs | 2 +- src/Microsoft.AspNet.Routing/RouteContext.cs | 2 +- src/Microsoft.AspNet.Routing/RouteMatch.cs | 2 +- src/Microsoft.AspNet.Routing/RouteValues.cs | 2 +- 11 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 samples/RoutingSample/HttpContextRouteEndpoint.cs diff --git a/Routing.sln b/Routing.sln index 9f44962866..3009f0f199 100644 --- a/Routing.sln +++ b/Routing.sln @@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{95359B4B-4 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Routing.Tests.net45", "test\Microsoft.AspNet.Routing.Tests\Microsoft.AspNet.Routing.Tests.net45.csproj", "{3775A966-0876-45C3-A67F-0E544BC48D55}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoutingSample.k10", "samples\RoutingSample\RoutingSample.k10.csproj", "{1BB31C1A-C6F8-4C00-BD30-92EF775276BE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {3775A966-0876-45C3-A67F-0E544BC48D55}.Debug|Any CPU.Build.0 = Debug|Any CPU {3775A966-0876-45C3-A67F-0E544BC48D55}.Release|Any CPU.ActiveCfg = Release|Any CPU {3775A966-0876-45C3-A67F-0E544BC48D55}.Release|Any CPU.Build.0 = Release|Any CPU + {1BB31C1A-C6F8-4C00-BD30-92EF775276BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BB31C1A-C6F8-4C00-BD30-92EF775276BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BB31C1A-C6F8-4C00-BD30-92EF775276BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BB31C1A-C6F8-4C00-BD30-92EF775276BE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -47,6 +53,7 @@ Global {16537084-C16D-4885-B90F-18BAC218F85B} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3} {1366B89C-F603-4088-B313-FA053C51BAC6} = {0E966C37-7334-4D96-AAF6-9F49FBD166E3} {121DC7B4-E29B-45E1-BF7E-314842F99A0D} = {C3ADD55B-B9C7-4061-8AD4-6A70D1AE3B2E} + {1BB31C1A-C6F8-4C00-BD30-92EF775276BE} = {C3ADD55B-B9C7-4061-8AD4-6A70D1AE3B2E} {3775A966-0876-45C3-A67F-0E544BC48D55} = {95359B4B-4C85-4B44-A75B-0621905C4CF6} EndGlobalSection EndGlobal diff --git a/samples/RoutingSample/HttpContextRouteEndpoint.cs b/samples/RoutingSample/HttpContextRouteEndpoint.cs new file mode 100644 index 0000000000..8e38c61cb2 --- /dev/null +++ b/samples/RoutingSample/HttpContextRouteEndpoint.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.Routing; + +namespace RoutingSample +{ + public class HttpContextRouteEndpoint : IRouteEndpoint + { + private readonly Func _appFunc; + + public HttpContextRouteEndpoint(Func appFunc) + { + _appFunc = appFunc; + } + + public async Task Send(HttpContext context) + { + await _appFunc(context); + return true; + } + } +} diff --git a/samples/RoutingSample/OwinRouteEndpoint.cs b/samples/RoutingSample/OwinRouteEndpoint.cs index 734a81b9da..fab5337248 100644 --- a/samples/RoutingSample/OwinRouteEndpoint.cs +++ b/samples/RoutingSample/OwinRouteEndpoint.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +#if NET45 + using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -18,10 +20,14 @@ namespace RoutingSample _appFunc = appFunc; } - public Task Invoke(HttpContext context) + public async Task Send(HttpContext context) { var owinContext = context.GetFeature().Environment; return _appFunc(owinContext); + await _appFunc(owinContext); + return true; } } } + +#endif diff --git a/samples/RoutingSample/Program.cs b/samples/RoutingSample/Program.cs index 9ae2ef4a73..b39d0aefbe 100644 --- a/samples/RoutingSample/Program.cs +++ b/samples/RoutingSample/Program.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +#if NET45 using System; using Microsoft.Owin.Hosting; @@ -20,3 +21,4 @@ namespace RoutingSample } } } +#endif \ No newline at end of file diff --git a/samples/RoutingSample/Startup.cs b/samples/RoutingSample/Startup.cs index 610d56f619..8a8eea7dc3 100644 --- a/samples/RoutingSample/Startup.cs +++ b/samples/RoutingSample/Startup.cs @@ -1,9 +1,12 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. +#if NET45 + using System.Collections.Generic; using System.IO; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.Routing.Owin; using Owin; using Microsoft.AspNet.PipelineCore.Owin; @@ -15,13 +18,19 @@ namespace RoutingSample { internal class Startup { - public void Configuration(IAppBuilder appBuilder) + public void Configuration(IAppBuilder builder) { - var routes = appBuilder.UseRouter(); - + builder.UseErrorPage(); - OwinRouteEndpoint endpoint1 = new OwinRouteEndpoint(async (context) => await WriteToBodyAsync(context, "match1")); - OwinRouteEndpoint endpoint2 = new OwinRouteEndpoint(async (context) => await WriteToBodyAsync(context, "Hello, World!")); + builder.UseBuilder(ConfigureRoutes); + } + + private void ConfigureRoutes(IBuilder builder) + { + var routes = builder.UseRouter(); + + var endpoint1 = new OwinRouteEndpoint(async (context) => await WriteToBodyAsync(context, "match1")); + var endpoint2 = new HttpContextRouteEndpoint(async (context) => await context.Response.WriteAsync("Hello, World!")); routes.Add(new PrefixRoute(endpoint1, "api/store")); routes.Add(new PrefixRoute(endpoint1, "api/checkout")); @@ -38,3 +47,5 @@ namespace RoutingSample } } } + +#endif diff --git a/samples/RoutingSample/project.json b/samples/RoutingSample/project.json index 35f33aab61..eff9d5df3e 100644 --- a/samples/RoutingSample/project.json +++ b/samples/RoutingSample/project.json @@ -3,9 +3,6 @@ "dependencies": { "Microsoft.AspNet.Abstractions" : "0.1-alpha-*", "Microsoft.AspNet.AppBuilderSupport": "0.1-alpha-*", - "Microsoft.AspNet.FeatureModel": "0.1-alpha-*", - "Microsoft.AspNet.HttpFeature": "0.1-alpha-*", - "Microsoft.AspNet.PipelineCore": "0.1-alpha-*", "Microsoft.AspNet.Routing" : "" }, "configurations": { @@ -13,9 +10,11 @@ "dependencies": { "Owin": "1.0", "Microsoft.Owin" : "2.1.0", + "Microsoft.Owin.Diagnostics" : "2.1.0", "Microsoft.Owin.Host.HttpListener" : "2.1.0", "Microsoft.Owin.Hosting" : "2.1.0" } - } + }, + "k10": {} } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs b/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs index 07c4a3f517..2f76d5822c 100644 --- a/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs +++ b/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs @@ -31,8 +31,11 @@ namespace Microsoft.AspNet.Routing { context.SetFeature(new RouteValues(match.Values)); - await match.Endpoint.Invoke(context); - return true; + var accepted = await match.Endpoint.Send(context); + if (accepted) + { + return true; + } } } diff --git a/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs b/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs index b5ebcc284d..df74ee0259 100644 --- a/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs +++ b/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs @@ -7,6 +7,6 @@ namespace Microsoft.AspNet.Routing { public interface IRouteEndpoint { - Task Invoke(HttpContext context); + Task Send(HttpContext context); } } diff --git a/src/Microsoft.AspNet.Routing/RouteContext.cs b/src/Microsoft.AspNet.Routing/RouteContext.cs index 2b8df23106..48eb5157aa 100644 --- a/src/Microsoft.AspNet.Routing/RouteContext.cs +++ b/src/Microsoft.AspNet.Routing/RouteContext.cs @@ -4,7 +4,7 @@ using Microsoft.AspNet.Abstractions; namespace Microsoft.AspNet.Routing { - public sealed class RouteContext + public class RouteContext { public RouteContext(HttpContext context) { diff --git a/src/Microsoft.AspNet.Routing/RouteMatch.cs b/src/Microsoft.AspNet.Routing/RouteMatch.cs index 3bea932dd2..92d5ce20c0 100644 --- a/src/Microsoft.AspNet.Routing/RouteMatch.cs +++ b/src/Microsoft.AspNet.Routing/RouteMatch.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNet.Routing /// The result of matching a route. Includes an to invoke and an optional collection of /// captured values. /// - public sealed class RouteMatch + public class RouteMatch { public RouteMatch(IRouteEndpoint endpoint) : this(endpoint, null) diff --git a/src/Microsoft.AspNet.Routing/RouteValues.cs b/src/Microsoft.AspNet.Routing/RouteValues.cs index da7dc4c85f..01acc8e991 100644 --- a/src/Microsoft.AspNet.Routing/RouteValues.cs +++ b/src/Microsoft.AspNet.Routing/RouteValues.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Microsoft.AspNet.Routing { - internal class RouteValues : IRouteValues + public class RouteValues : IRouteValues { public RouteValues(IDictionary values) { From cd0b68409448ae7fd15a799a2a2c540505397791 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 4 Feb 2014 13:54:16 -0800 Subject: [PATCH 015/616] cr feedback --- samples/RoutingSample/AppBuilderExtensions.cs | 18 ------------------ .../RoutingSample/HttpContextRouteEndpoint.cs | 4 ++-- samples/RoutingSample/OwinRouteEndpoint.cs | 1 - samples/RoutingSample/project.json | 4 ++-- 4 files changed, 4 insertions(+), 23 deletions(-) delete mode 100644 samples/RoutingSample/AppBuilderExtensions.cs diff --git a/samples/RoutingSample/AppBuilderExtensions.cs b/samples/RoutingSample/AppBuilderExtensions.cs deleted file mode 100644 index bd27fdde8f..0000000000 --- a/samples/RoutingSample/AppBuilderExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using Microsoft.AspNet.Routing; -using Microsoft.AspNet.Routing.Owin; -using Owin; - -namespace RoutingSample -{ - public static class AppBuilderExtensions - { - public static IRouteCollection UseRouter(this IAppBuilder app) - { - IRouteCollection routes = null; - app.UseBuilder((b) => routes = b.UseRouter()); - return routes; - } - } -} diff --git a/samples/RoutingSample/HttpContextRouteEndpoint.cs b/samples/RoutingSample/HttpContextRouteEndpoint.cs index 8e38c61cb2..87511eaacf 100644 --- a/samples/RoutingSample/HttpContextRouteEndpoint.cs +++ b/samples/RoutingSample/HttpContextRouteEndpoint.cs @@ -9,9 +9,9 @@ namespace RoutingSample { public class HttpContextRouteEndpoint : IRouteEndpoint { - private readonly Func _appFunc; + private readonly RequestDelegate _appFunc; - public HttpContextRouteEndpoint(Func appFunc) + public HttpContextRouteEndpoint(RequestDelegate appFunc) { _appFunc = appFunc; } diff --git a/samples/RoutingSample/OwinRouteEndpoint.cs b/samples/RoutingSample/OwinRouteEndpoint.cs index fab5337248..97b08013f6 100644 --- a/samples/RoutingSample/OwinRouteEndpoint.cs +++ b/samples/RoutingSample/OwinRouteEndpoint.cs @@ -23,7 +23,6 @@ namespace RoutingSample public async Task Send(HttpContext context) { var owinContext = context.GetFeature().Environment; - return _appFunc(owinContext); await _appFunc(owinContext); return true; } diff --git a/samples/RoutingSample/project.json b/samples/RoutingSample/project.json index eff9d5df3e..595e5694e5 100644 --- a/samples/RoutingSample/project.json +++ b/samples/RoutingSample/project.json @@ -10,11 +10,11 @@ "dependencies": { "Owin": "1.0", "Microsoft.Owin" : "2.1.0", - "Microsoft.Owin.Diagnostics" : "2.1.0", + "Microsoft.Owin.Diagnostics" : "2.1.0", "Microsoft.Owin.Host.HttpListener" : "2.1.0", "Microsoft.Owin.Hosting" : "2.1.0" } }, - "k10": {} + "k10": {} } } From 856c09ae1086de70c00ad91f2f5f840781e4fbc3 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 4 Feb 2014 16:24:17 -0800 Subject: [PATCH 016/616] Removing OwinRouteEndpoint --- samples/RoutingSample/OwinRouteEndpoint.cs | 32 ---------------------- samples/RoutingSample/Startup.cs | 18 +----------- 2 files changed, 1 insertion(+), 49 deletions(-) delete mode 100644 samples/RoutingSample/OwinRouteEndpoint.cs diff --git a/samples/RoutingSample/OwinRouteEndpoint.cs b/samples/RoutingSample/OwinRouteEndpoint.cs deleted file mode 100644 index 97b08013f6..0000000000 --- a/samples/RoutingSample/OwinRouteEndpoint.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -#if NET45 - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNet.Abstractions; -using Microsoft.AspNet.PipelineCore.Owin; -using Microsoft.AspNet.Routing; - -namespace RoutingSample -{ - internal class OwinRouteEndpoint : IRouteEndpoint - { - private readonly Func, Task> _appFunc; - - public OwinRouteEndpoint(Func, Task> appFunc) - { - _appFunc = appFunc; - } - - public async Task Send(HttpContext context) - { - var owinContext = context.GetFeature().Environment; - await _appFunc(owinContext); - return true; - } - } -} - -#endif diff --git a/samples/RoutingSample/Startup.cs b/samples/RoutingSample/Startup.cs index 8a8eea7dc3..6c87a888b9 100644 --- a/samples/RoutingSample/Startup.cs +++ b/samples/RoutingSample/Startup.cs @@ -2,17 +2,9 @@ #if NET45 -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading.Tasks; using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.Routing.Owin; using Owin; -using Microsoft.AspNet.PipelineCore.Owin; -using Microsoft.AspNet.Routing; -using System; -using Microsoft.AspNet.Abstractions; namespace RoutingSample { @@ -29,7 +21,7 @@ namespace RoutingSample { var routes = builder.UseRouter(); - var endpoint1 = new OwinRouteEndpoint(async (context) => await WriteToBodyAsync(context, "match1")); + var endpoint1 = new HttpContextRouteEndpoint(async (context) => await context.Response.WriteAsync("match1")); var endpoint2 = new HttpContextRouteEndpoint(async (context) => await context.Response.WriteAsync("Hello, World!")); routes.Add(new PrefixRoute(endpoint1, "api/store")); @@ -37,14 +29,6 @@ namespace RoutingSample routes.Add(new PrefixRoute(endpoint2, "hello/world")); routes.Add(new PrefixRoute(endpoint1, "")); } - - private static async Task WriteToBodyAsync(IDictionary context, string text) - { - var stream = (Stream)context["owin.ResponseBody"]; - - byte[] bytes = Encoding.UTF8.GetBytes(text); - await stream.WriteAsync(bytes, 0, bytes.Length); - } } } From 7cfcdcebdc1b0a756cbe5198f3e6db9429ac9665 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 7 Feb 2014 10:09:33 -0800 Subject: [PATCH 017/616] Workaround for myget being down. --- NuGet.Config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NuGet.Config b/NuGet.Config index ab583b0ff7..a059188b09 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,7 +1,7 @@  - + From d4904e870174cfe31f0799741299efa18f228c8b Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 6 Feb 2014 11:26:25 -0800 Subject: [PATCH 018/616] Adding legacy rounting code - mostly unmodified to get it into history --- .../Resources.Designer.cs | 145 + src/Microsoft.AspNet.Routing/Resources.resx | 147 + .../Template/BoundRouteTemplate.cs | 16 + .../Template/ITemplateRouteConstraint.cs | 12 + .../Template/IVirtualPathData.cs | 11 + .../Template/PathContentSegment.cs | 65 + .../Template/PathLiteralSubsegment.cs | 30 + .../Template/PathParameterSubsegment.cs | 42 + .../Template/PathSegment.cs | 15 + .../Template/PathSeparatorSegment.cs | 23 + .../Template/PathSubsegment.cs | 15 + .../Template/RouteDirection.cs | 10 + .../Template/TemplateParsedRoute.cs | 842 ++++++ .../Template/TemplateRoute.cs | 229 ++ .../Template/TemplateRouteParser.cs | 373 +++ .../Template/VirtualPathData.cs | 42 + .../Template/Assert.cs | 25 + .../Template/TemplateRouteParserTests.cs | 198 ++ .../Template/TemplateRouteTests.cs | 2595 +++++++++++++++++ .../project.json | 5 +- 20 files changed, 4839 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.AspNet.Routing/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.Routing/Resources.resx create mode 100644 src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/ITemplateRouteConstraint.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/IVirtualPathData.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/PathSegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/RouteDirection.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/VirtualPathData.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs diff --git a/src/Microsoft.AspNet.Routing/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Resources.Designer.cs new file mode 100644 index 0000000000..87554390e5 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Resources.Designer.cs @@ -0,0 +1,145 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34003 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Routing { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.. + /// + internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment { + get { + return ResourceManager.GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.. + /// + internal static string TemplateRoute_CannotHaveConsecutiveParameters { + get { + return ResourceManager.GetString("TemplateRoute_CannotHaveConsecutiveParameters", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.. + /// + internal static string TemplateRoute_CannotHaveConsecutiveSeparators { + get { + return ResourceManager.GetString("TemplateRoute_CannotHaveConsecutiveSeparators", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A catch-all parameter can only appear as the last segment of the route template.. + /// + internal static string TemplateRoute_CatchAllMustBeLast { + get { + return ResourceManager.GetString("TemplateRoute_CatchAllMustBeLast", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?". + /// + internal static string TemplateRoute_InvalidParameterName { + get { + return ResourceManager.GetString("TemplateRoute_InvalidParameterName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route template cannot start with a '/' or '~' character and it cannot contain a '?' character.. + /// + internal static string TemplateRoute_InvalidRouteTemplate { + get { + return ResourceManager.GetString("TemplateRoute_InvalidRouteTemplate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character.. + /// + internal static string TemplateRoute_MismatchedParameter { + get { + return ResourceManager.GetString("TemplateRoute_MismatchedParameter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The route parameter name '{0}' appears more than one time in the route template.. + /// + internal static string TemplateRoute_RepeatedParameter { + get { + return ResourceManager.GetString("TemplateRoute_RepeatedParameter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'.. + /// + internal static string TemplateRoute_ValidationMustBeStringOrCustomConstraint { + get { + return ResourceManager.GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Resources.resx b/src/Microsoft.AspNet.Routing/Resources.resx new file mode 100644 index 0000000000..76c3adf227 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Resources.resx @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. + + + A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. + + + The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. + + + A catch-all parameter can only appear as the last segment of the route template. + + + The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?" + + + The route template cannot start with a '/' or '~' character and it cannot contain a '?' character. + + + There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character. + + + The route parameter name '{0}' appears more than one time in the route template. + + + The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs b/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs new file mode 100644 index 0000000000..8d73a43dec --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing.Template +{ + /// + /// Represents a URI generated from a . + /// + public class BoundRouteTemplate + { + public string BoundTemplate { get; set; } + + public IDictionary Values { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/ITemplateRouteConstraint.cs b/src/Microsoft.AspNet.Routing/Template/ITemplateRouteConstraint.cs new file mode 100644 index 0000000000..4f70d7306b --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/ITemplateRouteConstraint.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.Routing.Template +{ + public interface ITemplateRouteConstraint + { + bool Match(HttpContext context, IRoute route, string parameterName, IDictionary values, RouteDirection routeDirection); + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/IVirtualPathData.cs b/src/Microsoft.AspNet.Routing/Template/IVirtualPathData.cs new file mode 100644 index 0000000000..f7f2cafd19 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/IVirtualPathData.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing.Template +{ + public interface IVirtualPathData + { + IRoute Route { get; } + + string VirtualPath { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs b/src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs new file mode 100644 index 0000000000..e0382db613 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.Routing.Template +{ + // Represents a segment of a URI that is not a separator. It contains subsegments such as literals and parameters. + internal sealed class PathContentSegment : PathSegment + { + public PathContentSegment(IList subsegments) + { + Subsegments = subsegments; + } + + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm.")] + public bool IsCatchAll + { + get + { + // TODO: Verify this is correct. Maybe add an assert. + // Performance sensitive + // Caching count is faster for IList + int subsegmentCount = Subsegments.Count; + for (int i = 0; i < subsegmentCount; i++) + { + PathSubsegment seg = Subsegments[i]; + PathParameterSubsegment paramterSubSegment = seg as PathParameterSubsegment; + if (paramterSubSegment != null && paramterSubSegment.IsCatchAll) + { + return true; + } + } + return false; + } + } + + public IList Subsegments { get; private set; } + +#if ROUTE_DEBUGGING + public override string LiteralText + { + get + { + List s = new List(); + foreach (PathSubsegment subsegment in Subsegments) + { + s.Add(subsegment.LiteralText); + } + return String.Join(String.Empty, s.ToArray()); + } + } + + public override string ToString() + { + List s = new List(); + foreach (PathSubsegment subsegment in Subsegments) + { + s.Add(subsegment.ToString()); + } + return "[ " + String.Join(", ", s.ToArray()) + " ]"; + } +#endif + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs b/src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs new file mode 100644 index 0000000000..312597a387 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing.Template +{ + // Represents a literal subsegment of a ContentPathSegment + internal sealed class PathLiteralSubsegment : PathSubsegment + { + public PathLiteralSubsegment(string literal) + { + Literal = literal; + } + + public string Literal { get; private set; } + +#if ROUTE_DEBUGGING + public override string LiteralText + { + get + { + return Literal; + } + } + + public override string ToString() + { + return "\"" + Literal + "\""; + } +#endif + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs b/src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs new file mode 100644 index 0000000000..2edf14ccef --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Routing.Template +{ + // Represents a parameter subsegment of a ContentPathSegment + internal sealed class PathParameterSubsegment : PathSubsegment + { + public PathParameterSubsegment(string parameterName) + { + if (parameterName.StartsWith("*", StringComparison.Ordinal)) + { + ParameterName = parameterName.Substring(1); + IsCatchAll = true; + } + else + { + ParameterName = parameterName; + } + } + + public bool IsCatchAll { get; private set; } + + public string ParameterName { get; private set; } + +#if ROUTE_DEBUGGING + public override string LiteralText + { + get + { + return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}"; + } + } + + public override string ToString() + { + return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}"; + } +#endif + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/PathSegment.cs b/src/Microsoft.AspNet.Routing/Template/PathSegment.cs new file mode 100644 index 0000000000..f58279ef0f --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/PathSegment.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing.Template +{ + // Represents a segment of a URI such as a separator or content + public abstract class PathSegment + { +#if ROUTE_DEBUGGING + public abstract string LiteralText + { + get; + } +#endif + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs b/src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs new file mode 100644 index 0000000000..88cb876f70 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing.Template +{ + // Represents a "/" separator in a URI + internal sealed class PathSeparatorSegment : PathSegment + { +#if ROUTE_DEBUGGING + public override string LiteralText + { + get + { + return "/"; + } + } + + public override string ToString() + { + return "\"/\""; + } +#endif + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs b/src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs new file mode 100644 index 0000000000..60e0175f50 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing.Template +{ + // Represents a subsegment of a ContentPathSegment such as a parameter or a literal. + internal abstract class PathSubsegment + { +#if ROUTE_DEBUGGING + public abstract string LiteralText + { + get; + } +#endif + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/RouteDirection.cs b/src/Microsoft.AspNet.Routing/Template/RouteDirection.cs new file mode 100644 index 0000000000..7f9bcc6c33 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/RouteDirection.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing.Template +{ + public enum RouteDirection + { + UriResolution = 0, + UriGeneration + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs new file mode 100644 index 0000000000..9b65710400 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs @@ -0,0 +1,842 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.AspNet.Routing.Template +{ + public sealed class TemplateParsedRoute + { + public TemplateParsedRoute(IList pathSegments) + { + Contract.Assert(pathSegments != null); + PathSegments = pathSegments; + } + + internal IList PathSegments { get; private set; } + + [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Not changing original algorithm")] + [SuppressMessage("Microsoft.Maintainability", "CA1505:AvoidUnmaintainableCode", Justification = "Not changing original algorithm")] + public BoundRouteTemplate Bind(IDictionary currentValues, IDictionary values, IDictionary defaultValues, IDictionary constraints) + { + if (currentValues == null) + { + currentValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + if (values == null) + { + values = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + if (defaultValues == null) + { + defaultValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + // The set of values we should be using when generating the URI in this route + IDictionary acceptedValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Keep track of which new values have been used + HashSet unusedNewValues = new HashSet(values.Keys, StringComparer.OrdinalIgnoreCase); + + // Step 1: Get the list of values we're going to try to use to match and generate this URI + + // Find out which entries in the URI are valid for the URI we want to generate. + // If the URI had ordered parameters a="1", b="2", c="3" and the new values + // specified that b="9", then we need to invalidate everything after it. The new + // values should then be a="1", b="9", c=. + ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + // If it's a parameter subsegment, examine the current value to see if it matches the new value + string parameterName = parameterSubsegment.ParameterName; + + object newParameterValue; + bool hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue); + if (hasNewParameterValue) + { + unusedNewValues.Remove(parameterName); + } + + object currentParameterValue; + bool hasCurrentParameterValue = currentValues.TryGetValue(parameterName, out currentParameterValue); + + if (hasNewParameterValue && hasCurrentParameterValue) + { + if (!RoutePartsEqual(currentParameterValue, newParameterValue)) + { + // Stop copying current values when we find one that doesn't match + return false; + } + } + + // If the parameter is a match, add it to the list of values we will use for URI generation + if (hasNewParameterValue) + { + if (IsRoutePartNonEmpty(newParameterValue)) + { + acceptedValues.Add(parameterName, newParameterValue); + } + } + else + { + if (hasCurrentParameterValue) + { + acceptedValues.Add(parameterName, currentParameterValue); + } + } + return true; + }); + + // Add all remaining new values to the list of values we will use for URI generation + foreach (var newValue in values) + { + if (IsRoutePartNonEmpty(newValue.Value)) + { + if (!acceptedValues.ContainsKey(newValue.Key)) + { + acceptedValues.Add(newValue.Key, newValue.Value); + } + } + } + + // Add all current values that aren't in the URI at all + foreach (var currentValue in currentValues) + { + string parameterName = currentValue.Key; + if (!acceptedValues.ContainsKey(parameterName)) + { + PathParameterSubsegment parameterSubsegment = GetParameterSubsegment(PathSegments, parameterName); + if (parameterSubsegment == null) + { + acceptedValues.Add(parameterName, currentValue.Value); + } + } + } + + // Add all remaining default values from the route to the list of values we will use for URI generation + ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName)) + { + object defaultValue; + if (!IsParameterRequired(parameterSubsegment, defaultValues, out defaultValue)) + { + // Add the default value only if there isn't already a new value for it and + // only if it actually has a default value, which we determine based on whether + // the parameter value is required. + acceptedValues.Add(parameterSubsegment.ParameterName, defaultValue); + } + } + return true; + }); + + // All required parameters in this URI must have values from somewhere (i.e. the accepted values) + bool hasAllRequiredValues = ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + object defaultValue; + if (IsParameterRequired(parameterSubsegment, defaultValues, out defaultValue)) + { + if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName)) + { + // If the route parameter value is required that means there's + // no default value, so if there wasn't a new value for it + // either, this route won't match. + return false; + } + } + return true; + }); + if (!hasAllRequiredValues) + { + return null; + } + + // All other default values must match if they are explicitly defined in the new values + IDictionary otherDefaultValues = new Dictionary(defaultValues, StringComparer.OrdinalIgnoreCase); + ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + otherDefaultValues.Remove(parameterSubsegment.ParameterName); + return true; + }); + + foreach (var defaultValue in otherDefaultValues) + { + object value; + if (values.TryGetValue(defaultValue.Key, out value)) + { + unusedNewValues.Remove(defaultValue.Key); + if (!RoutePartsEqual(value, defaultValue.Value)) + { + // If there is a non-parameterized value in the route and there is a + // new value for it and it doesn't match, this route won't match. + return null; + } + } + } + + // Step 2: If the route is a match generate the appropriate URI + + StringBuilder uri = new StringBuilder(); + StringBuilder pendingParts = new StringBuilder(); + + bool pendingPartsAreAllSafe = false; + bool blockAllUriAppends = false; + + for (int i = 0; i < PathSegments.Count; i++) + { + PathSegment pathSegment = PathSegments[i]; // parsedRouteUriPart + + if (pathSegment is PathSeparatorSegment) + { + if (pendingPartsAreAllSafe) + { + // Accept + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + pendingParts.Length = 0; + } + } + pendingPartsAreAllSafe = false; + + // Guard against appending multiple separators for empty segments + if (pendingParts.Length > 0 && pendingParts[pendingParts.Length - 1] == '/') + { + // Dev10 676725: Route should not be matched if that causes mismatched tokens + // Dev11 86819: We will allow empty matches if all subsequent segments are null + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI (without the trailing slash) and prevent any future appends + uri.Append(pendingParts.ToString(0, pendingParts.Length - 1)); + pendingParts.Length = 0; + blockAllUriAppends = true; + } + else + { + pendingParts.Append("/"); + } + } + else + { + PathContentSegment contentPathSegment = pathSegment as PathContentSegment; + if (contentPathSegment != null) + { + // Segments are treated as all-or-none. We should never output a partial segment. + // If we add any subsegment of this segment to the generated URI, we have to add + // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we + // used a value for {p1}, we have to output the entire segment up to the next "/". + // Otherwise we could end up with the partial segment "v1" instead of the entire + // segment "v1-v2.xml". + bool addedAnySubsegments = false; + + foreach (PathSubsegment subsegment in contentPathSegment.Subsegments) + { + PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment; + if (literalSubsegment != null) + { + // If it's a literal we hold on to it until we are sure we need to add it + pendingPartsAreAllSafe = true; + pendingParts.Append(literalSubsegment.Literal); + } + else + { + PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment; + if (parameterSubsegment != null) + { + if (pendingPartsAreAllSafe) + { + // Accept + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + pendingParts.Length = 0; + + addedAnySubsegments = true; + } + } + pendingPartsAreAllSafe = false; + + // If it's a parameter, get its value + object acceptedParameterValue; + bool hasAcceptedParameterValue = acceptedValues.TryGetValue(parameterSubsegment.ParameterName, out acceptedParameterValue); + if (hasAcceptedParameterValue) + { + unusedNewValues.Remove(parameterSubsegment.ParameterName); + } + + object defaultParameterValue; + defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultParameterValue); + + if (RoutePartsEqual(acceptedParameterValue, defaultParameterValue)) + { + // If the accepted value is the same as the default value, mark it as pending since + // we won't necessarily add it to the URI we generate. + pendingParts.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture)); + } + else + { + if (blockAllUriAppends) + { + return null; + } + + // Add the new part to the URI as well as any pending parts + if (pendingParts.Length > 0) + { + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + pendingParts.Length = 0; + } + uri.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture)); + + addedAnySubsegments = true; + } + } + else + { + Contract.Assert(false, "Invalid path subsegment type"); + } + } + } + + if (addedAnySubsegments) + { + // See comment above about why we add the pending parts + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + pendingParts.Length = 0; + } + } + } + else + { + Contract.Assert(false, "Invalid path segment type"); + } + } + } + + if (pendingPartsAreAllSafe) + { + // Accept + if (pendingParts.Length > 0) + { + if (blockAllUriAppends) + { + return null; + } + + // Append any pending literals to the URI + uri.Append(pendingParts.ToString()); + } + } + + // Process constraints keys + if (constraints != null) + { + // If there are any constraints, mark all the keys as being used so that we don't + // generate query string items for custom constraints that don't appear as parameters + // in the URI format. + foreach (var constraintsItem in constraints) + { + unusedNewValues.Remove(constraintsItem.Key); + } + } + + // Encode the URI before we append the query string, otherwise we would double encode the query string + StringBuilder encodedUri = new StringBuilder(); + encodedUri.Append(UriEncode(uri.ToString())); + uri = encodedUri; + + // Add remaining new values as query string parameters to the URI + if (unusedNewValues.Count > 0) + { + // Generate the query string + bool firstParam = true; + foreach (string unusedNewValue in unusedNewValues) + { + object value; + if (acceptedValues.TryGetValue(unusedNewValue, out value)) + { + uri.Append(firstParam ? '?' : '&'); + firstParam = false; + uri.Append(Uri.EscapeDataString(unusedNewValue)); + uri.Append('='); + uri.Append(Uri.EscapeDataString(Convert.ToString(value, CultureInfo.InvariantCulture))); + } + } + } + + return new BoundRouteTemplate + { + BoundTemplate = uri.ToString(), + Values = acceptedValues + }; + } + + private static string EscapeReservedCharacters(Match m) + { + return "%" + Convert.ToUInt16(m.Value[0]).ToString("x2", CultureInfo.InvariantCulture); + } + + private static bool ForEachParameter(IList pathSegments, Func action) + { + for (int i = 0; i < pathSegments.Count; i++) + { + PathSegment pathSegment = pathSegments[i]; + + if (pathSegment is PathSeparatorSegment) + { + // We only care about parameter subsegments, so skip this + continue; + } + else + { + PathContentSegment contentPathSegment = pathSegment as PathContentSegment; + if (contentPathSegment != null) + { + foreach (PathSubsegment subsegment in contentPathSegment.Subsegments) + { + PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment; + if (literalSubsegment != null) + { + // We only care about parameter subsegments, so skip this + continue; + } + else + { + PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment; + if (parameterSubsegment != null) + { + if (!action(parameterSubsegment)) + { + return false; + } + } + else + { + Contract.Assert(false, "Invalid path subsegment type"); + } + } + } + } + else + { + Contract.Assert(false, "Invalid path segment type"); + } + } + } + + return true; + } + + private static PathParameterSubsegment GetParameterSubsegment(IList pathSegments, string parameterName) + { + PathParameterSubsegment foundParameterSubsegment = null; + + ForEachParameter(pathSegments, delegate(PathParameterSubsegment parameterSubsegment) + { + if (String.Equals(parameterName, parameterSubsegment.ParameterName, StringComparison.OrdinalIgnoreCase)) + { + foundParameterSubsegment = parameterSubsegment; + return false; + } + else + { + return true; + } + }); + + return foundParameterSubsegment; + } + + private static bool IsParameterRequired(PathParameterSubsegment parameterSubsegment, IDictionary defaultValues, out object defaultValue) + { + if (parameterSubsegment.IsCatchAll) + { + defaultValue = null; + return false; + } + + return !defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultValue); + } + + private static bool IsRoutePartNonEmpty(object routePart) + { + string routePartString = routePart as string; + if (routePartString != null) + { + return routePartString.Length > 0; + } + return routePart != null; + } + + public IDictionary Match(string virtualPath, IDictionary defaultValues) + { + IList requestPathSegments = TemplateRouteParser.SplitUriToPathSegmentStrings(virtualPath); + + if (defaultValues == null) + { + defaultValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + IDictionary matchedValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // This flag gets set once all the data in the URI has been parsed through, but + // the route we're trying to match against still has more parts. At this point + // we'll only continue matching separator characters and parameters that have + // default values. + bool ranOutOfStuffToParse = false; + + // This value gets set once we start processing a catchall parameter (if there is one + // at all). Once we set this value we consume all remaining parts of the URI into its + // parameter value. + bool usedCatchAllParameter = false; + + for (int i = 0; i < PathSegments.Count; i++) + { + PathSegment pathSegment = PathSegments[i]; + + if (requestPathSegments.Count <= i) + { + ranOutOfStuffToParse = true; + } + + string requestPathSegment = ranOutOfStuffToParse ? null : requestPathSegments[i]; + + if (pathSegment is PathSeparatorSegment) + { + if (ranOutOfStuffToParse) + { + // If we're trying to match a separator in the route but there's no more content, that's OK + } + else + { + if (!String.Equals(requestPathSegment, "/", StringComparison.Ordinal)) + { + return null; + } + } + } + else + { + PathContentSegment contentPathSegment = pathSegment as PathContentSegment; + if (contentPathSegment != null) + { + if (contentPathSegment.IsCatchAll) + { + Contract.Assert(i == (PathSegments.Count - 1), "If we're processing a catch-all, we should be on the last route segment."); + MatchCatchAll(contentPathSegment, requestPathSegments.Skip(i), defaultValues, matchedValues); + usedCatchAllParameter = true; + } + else + { + if (!MatchContentPathSegment(contentPathSegment, requestPathSegment, defaultValues, matchedValues)) + { + return null; + } + } + } + else + { + Contract.Assert(false, "Invalid path segment type"); + } + } + } + + if (!usedCatchAllParameter) + { + if (PathSegments.Count < requestPathSegments.Count) + { + // If we've already gone through all the parts defined in the route but the URI + // still contains more content, check that the remaining content is all separators. + for (int i = PathSegments.Count; i < requestPathSegments.Count; i++) + { + if (!TemplateRouteParser.IsSeparator(requestPathSegments[i])) + { + return null; + } + } + } + } + + // Copy all remaining default values to the route data + if (defaultValues != null) + { + foreach (var defaultValue in defaultValues) + { + if (!matchedValues.ContainsKey(defaultValue.Key)) + { + matchedValues.Add(defaultValue.Key, defaultValue.Value); + } + } + } + + return matchedValues; + } + + private static void MatchCatchAll(PathContentSegment contentPathSegment, IEnumerable remainingRequestSegments, IDictionary defaultValues, IDictionary matchedValues) + { + string remainingRequest = String.Join(String.Empty, remainingRequestSegments.ToArray()); + + PathParameterSubsegment catchAllSegment = contentPathSegment.Subsegments[0] as PathParameterSubsegment; + + object catchAllValue; + + if (remainingRequest.Length > 0) + { + catchAllValue = remainingRequest; + } + else + { + defaultValues.TryGetValue(catchAllSegment.ParameterName, out catchAllValue); + } + + matchedValues.Add(catchAllSegment.ParameterName, catchAllValue); + } + + private static bool MatchContentPathSegment(PathContentSegment routeSegment, string requestPathSegment, IDictionary defaultValues, IDictionary matchedValues) + { + if (String.IsNullOrEmpty(requestPathSegment)) + { + // If there's no data to parse, we must have exactly one parameter segment and no other segments - otherwise no match + + if (routeSegment.Subsegments.Count > 1) + { + return false; + } + + PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[0] as PathParameterSubsegment; + if (parameterSubsegment == null) + { + return false; + } + + // We must have a default value since there's no value in the request URI + object parameterValue; + if (defaultValues.TryGetValue(parameterSubsegment.ParameterName, out parameterValue)) + { + // If there's a default value for this parameter, use that default value + matchedValues.Add(parameterSubsegment.ParameterName, parameterValue); + return true; + } + else + { + // If there's no default value, this segment doesn't match + return false; + } + } + + // Optimize for the common case where there is only one subsegment in the segment - either a parameter or a literal + if (routeSegment.Subsegments.Count == 1) + { + return MatchSingleContentPathSegment(routeSegment.Subsegments[0], requestPathSegment, matchedValues); + } + + // Find last literal segment and get its last index in the string + + int lastIndex = requestPathSegment.Length; + int indexOfLastSegmentUsed = routeSegment.Subsegments.Count - 1; + + PathParameterSubsegment parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value + PathLiteralSubsegment lastLiteral = null; // Keeps track of the left-most literal we've encountered + + while (indexOfLastSegmentUsed >= 0) + { + int newLastIndex = lastIndex; + + PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathParameterSubsegment; + if (parameterSubsegment != null) + { + // Hold on to the parameter so that we can fill it in when we locate the next literal + parameterNeedsValue = parameterSubsegment; + } + else + { + PathLiteralSubsegment literalSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathLiteralSubsegment; + if (literalSubsegment != null) + { + lastLiteral = literalSubsegment; + + int startIndex = lastIndex - 1; + // If we have a pending parameter subsegment, we must leave at least one character for that + if (parameterNeedsValue != null) + { + startIndex--; + } + + if (startIndex < 0) + { + return false; + } + + int indexOfLiteral = requestPathSegment.LastIndexOf(literalSubsegment.Literal, startIndex, StringComparison.OrdinalIgnoreCase); + if (indexOfLiteral == -1) + { + // If we couldn't find this literal index, this segment cannot match + return false; + } + + // If the first subsegment is a literal, it must match at the right-most extent of the request URI. + // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/". + // This check is related to the check we do at the very end of this function. + if (indexOfLastSegmentUsed == (routeSegment.Subsegments.Count - 1)) + { + if ((indexOfLiteral + literalSubsegment.Literal.Length) != requestPathSegment.Length) + { + return false; + } + } + + newLastIndex = indexOfLiteral; + } + else + { + Contract.Assert(false, "Invalid path segment type"); + } + } + + if ((parameterNeedsValue != null) && (((lastLiteral != null) && (parameterSubsegment == null)) || (indexOfLastSegmentUsed == 0))) + { + // If we have a pending parameter that needs a value, grab that value + + int parameterStartIndex; + int parameterTextLength; + + if (lastLiteral == null) + { + if (indexOfLastSegmentUsed == 0) + { + parameterStartIndex = 0; + } + else + { + parameterStartIndex = newLastIndex; + Contract.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above"); + } + parameterTextLength = lastIndex; + } + else + { + // If we're getting a value for a parameter that is somewhere in the middle of the segment + if ((indexOfLastSegmentUsed == 0) && (parameterSubsegment != null)) + { + parameterStartIndex = 0; + parameterTextLength = lastIndex; + } + else + { + parameterStartIndex = newLastIndex + lastLiteral.Literal.Length; + parameterTextLength = lastIndex - parameterStartIndex; + } + } + + string parameterValueString = requestPathSegment.Substring(parameterStartIndex, parameterTextLength); + + if (String.IsNullOrEmpty(parameterValueString)) + { + // If we're here that means we have a segment that contains multiple sub-segments. + // For these segments all parameters must have non-empty values. If the parameter + // has an empty value it's not a match. + return false; + } + else + { + // If there's a value in the segment for this parameter, use the subsegment value + matchedValues.Add(parameterNeedsValue.ParameterName, parameterValueString); + } + + parameterNeedsValue = null; + lastLiteral = null; + } + + lastIndex = newLastIndex; + indexOfLastSegmentUsed--; + } + + // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of + // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment + // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching + // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire* + // request URI in order for it to be a match. + // This check is related to the check we do earlier in this function for LiteralSubsegments. + return (lastIndex == 0) || (routeSegment.Subsegments[0] is PathParameterSubsegment); + } + + private static bool MatchSingleContentPathSegment(PathSubsegment pathSubsegment, string requestPathSegment, IDictionary matchedValues) + { + PathParameterSubsegment parameterSubsegment = pathSubsegment as PathParameterSubsegment; + if (parameterSubsegment == null) + { + // Handle a single literal segment + PathLiteralSubsegment literalSubsegment = pathSubsegment as PathLiteralSubsegment; + Contract.Assert(literalSubsegment != null, "Invalid path segment type"); + return literalSubsegment.Literal.Equals(requestPathSegment, StringComparison.OrdinalIgnoreCase); + } + else + { + // Handle a single parameter segment + matchedValues.Add(parameterSubsegment.ParameterName, requestPathSegment); + return true; + } + } + + private static bool RoutePartsEqual(object a, object b) + { + string sa = a as string; + string sb = b as string; + if (sa != null && sb != null) + { + // For strings do a case-insensitive comparison + return String.Equals(sa, sb, StringComparison.OrdinalIgnoreCase); + } + else + { + if (a != null && b != null) + { + // Explicitly call .Equals() in case it is overridden in the type + return a.Equals(b); + } + else + { + // At least one of them is null. Return true if they both are + return a == b; + } + } + } + + private static string UriEncode(string str) + { + string escape = Uri.EscapeUriString(str); + return Regex.Replace(escape, "([#?])", new MatchEvaluator(EscapeReservedCharacters)); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs new file mode 100644 index 0000000000..c0ab8dcb44 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.AspNet.Abstractions; + +namespace Microsoft.AspNet.Routing.Template +{ + /// + /// Route class for self-host (i.e. hosted outside of ASP.NET). This class is mostly the + /// same as the System.Web.Routing.Route implementation. + /// This class has the same URL matching functionality as System.Web.Routing.Route. However, + /// in order for this route to match when generating URLs, a special "httproute" key must be + /// specified when generating the URL. + /// + public class TemplateRoute : IRoute + { + /// + /// Key used to signify that a route URL generation request should include HTTP routes (e.g. Web API). + /// If this key is not specified then no HTTP routes will match. + /// + public static readonly string HttpRouteKey = "httproute"; + + private string _routeTemplate; + private IDictionary _defaults; + private IDictionary _constraints; + private IDictionary _dataTokens; + + public TemplateRoute() + : this(routeTemplate: null, defaults: null, constraints: null, dataTokens: null, handler: null) + { + } + + public TemplateRoute(string routeTemplate) + : this(routeTemplate, defaults: null, constraints: null, dataTokens: null, handler: null) + { + } + + public TemplateRoute(string routeTemplate, IDictionary defaults) + : this(routeTemplate, defaults, constraints: null, dataTokens: null, handler: null) + { + } + + public TemplateRoute(string routeTemplate, IDictionary defaults, IDictionary constraints) + : this(routeTemplate, defaults, constraints, dataTokens: null, handler: null) + { + } + + public TemplateRoute(string routeTemplate, IDictionary defaults, IDictionary constraints, IDictionary dataTokens) + : this(routeTemplate, defaults, constraints, dataTokens, handler: null) + { + } + + public TemplateRoute(string routeTemplate, IDictionary defaults, IDictionary constraints, IDictionary dataTokens, IRouteEndpoint handler) + { + _routeTemplate = routeTemplate == null ? String.Empty : routeTemplate; + _defaults = defaults ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + _constraints = constraints ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + _dataTokens = dataTokens ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + Handler = handler; + + // The parser will throw for invalid routes. + ParsedRoute = TemplateRouteParser.Parse(RouteTemplate); + } + + public IDictionary Defaults + { + get { return _defaults; } + } + + public IDictionary Constraints + { + get { return _constraints; } + } + + public IDictionary DataTokens + { + get { return _dataTokens; } + } + + public IRouteEndpoint Handler { get; private set; } + + public string RouteTemplate + { + get { return _routeTemplate; } + } + + internal TemplateParsedRoute ParsedRoute { get; private set; } + + public virtual RouteMatch GetRouteData(HttpContext request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + var requestPath = request.Request.Path.Value; + if (!String.IsNullOrEmpty(requestPath) && requestPath[0] == '/') + { + requestPath = requestPath.Substring(1); + } + + IDictionary values = ParsedRoute.Match(requestPath, _defaults); + if (values == null) + { + // If we got back a null value set, that means the URI did not match + return null; + } + + // Validate the values + if (!ProcessConstraints(request, values, RouteDirection.UriResolution)) + { + return null; + } + + return new RouteMatch(null, values); + } + + /// + /// Attempt to generate a URI that represents the values passed in based on current + /// values from the and new values using the specified . + /// + /// The HTTP request message. + /// The route values. + /// A instance or null if URI cannot be generated. + public virtual IVirtualPathData GetVirtualPath(HttpContext request, IDictionary values) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + // Only perform URL generation if the "httproute" key was specified. This allows these + // routes to be ignored when a regular MVC app tries to generate URLs. Without this special + // key an HTTP route used for Web API would normally take over almost all the routes in a + // typical app. + if (values != null && !values.Keys.Contains(HttpRouteKey, StringComparer.OrdinalIgnoreCase)) + { + return null; + } + // Remove the value from the collection so that it doesn't affect the generated URL + var newValues = GetRouteDictionaryWithoutHttpRouteKey(values); + + IRouteValues routeData = request.GetFeature(); + IDictionary requestValues = routeData == null ? null : routeData.Values; + + BoundRouteTemplate result = ParsedRoute.Bind(requestValues, newValues, _defaults, _constraints); + if (result == null) + { + return null; + } + + // Assert that the route matches the validation rules + if (!ProcessConstraints(request, result.Values, RouteDirection.UriGeneration)) + { + return null; + } + + return new VirtualPathData(this, result.BoundTemplate); + } + + private static IDictionary GetRouteDictionaryWithoutHttpRouteKey(IDictionary routeValues) + { + var newRouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (routeValues != null) + { + foreach (var routeValue in routeValues) + { + if (!String.Equals(routeValue.Key, HttpRouteKey, StringComparison.OrdinalIgnoreCase)) + { + newRouteValues.Add(routeValue.Key, routeValue.Value); + } + } + } + return newRouteValues; + } + + protected virtual bool ProcessConstraint(HttpContext request, object constraint, string parameterName, IDictionary values, RouteDirection routeDirection) + { + ITemplateRouteConstraint customConstraint = constraint as ITemplateRouteConstraint; + if (customConstraint != null) + { + return customConstraint.Match(request, this, parameterName, values, routeDirection); + } + + // If there was no custom constraint, then treat the constraint as a string which represents a Regex. + string constraintsRule = constraint as string; + if (constraintsRule == null) + { + throw new InvalidOperationException(String.Format( + CultureInfo.CurrentCulture, + Resources.TemplateRoute_ValidationMustBeStringOrCustomConstraint, + parameterName, + RouteTemplate, + typeof(ITemplateRouteConstraint).Name)); + } + + object parameterValue; + values.TryGetValue(parameterName, out parameterValue); + string parameterValueString = Convert.ToString(parameterValue, CultureInfo.InvariantCulture); + string constraintsRegEx = "^(" + constraintsRule + ")$"; + return Regex.IsMatch(parameterValueString, constraintsRegEx, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + } + + private bool ProcessConstraints(HttpContext request, IDictionary values, RouteDirection routeDirection) + { + if (Constraints != null) + { + foreach (KeyValuePair constraintsItem in Constraints) + { + if (!ProcessConstraint(request, constraintsItem.Value, constraintsItem.Key, values, routeDirection)) + { + return false; + } + } + } + + return true; + } + + public RouteMatch Match(RouteContext context) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs new file mode 100644 index 0000000000..d82a7b3ec6 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs @@ -0,0 +1,373 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Linq; + +namespace Microsoft.AspNet.Routing.Template +{ + public static class TemplateRouteParser + { + private static string GetLiteral(string segmentLiteral) + { + // Scan for errant single { and } and convert double {{ to { and double }} to } + + // First we eliminate all escaped braces and then check if any other braces are remaining + string newLiteral = segmentLiteral.Replace("{{", String.Empty).Replace("}}", String.Empty); + if (newLiteral.Contains("{") || newLiteral.Contains("}")) + { + return null; + } + + // If it's a valid format, we unescape the braces + return segmentLiteral.Replace("{{", "{").Replace("}}", "}"); + } + + private static int IndexOfFirstOpenParameter(string segment, int startIndex) + { + // Find the first unescaped open brace + while (true) + { + startIndex = segment.IndexOf('{', startIndex); + if (startIndex == -1) + { + // If there are no more open braces, stop + return -1; + } + if ((startIndex + 1 == segment.Length) || + ((startIndex + 1 < segment.Length) && (segment[startIndex + 1] != '{'))) + { + // If we found an open brace that is followed by a non-open brace, it's + // a parameter delimiter. + // It's also a delimiter if the open brace is the last character - though + // it ends up being being called out as invalid later on. + return startIndex; + } + // Increment by two since we want to skip both the open brace that + // we're on as well as the subsequent character since we know for + // sure that it is part of an escape sequence. + startIndex += 2; + } + } + + internal static bool IsSeparator(string s) + { + return String.Equals(s, "/", StringComparison.Ordinal); + } + + private static bool IsValidParameterName(string parameterName) + { + if (parameterName.Length == 0) + { + return false; + } + + for (int i = 0; i < parameterName.Length; i++) + { + char c = parameterName[i]; + if (c == '/' || c == '{' || c == '}') + { + return false; + } + } + + return true; + } + + internal static bool IsInvalidRouteTemplate(string routeTemplate) + { + return routeTemplate.StartsWith("~", StringComparison.Ordinal) || + routeTemplate.StartsWith("/", StringComparison.Ordinal) || + (routeTemplate.IndexOf('?') != -1); + } + + public static TemplateParsedRoute Parse(string routeTemplate) + { + if (routeTemplate == null) + { + routeTemplate = String.Empty; + } + + if (IsInvalidRouteTemplate(routeTemplate)) + { + throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, "routeTemplate"); + } + + IList uriParts = SplitUriToPathSegmentStrings(routeTemplate); + Exception ex = ValidateUriParts(uriParts); + if (ex != null) + { + throw ex; + } + + IList pathSegments = SplitUriToPathSegments(uriParts); + + Contract.Assert(uriParts.Count == pathSegments.Count, "The number of string segments should be the same as the number of path segments"); + + return new TemplateParsedRoute(pathSegments); + } + + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", + Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] + private static IList ParseUriSegment(string segment, out Exception exception) + { + int startIndex = 0; + + List pathSubsegments = new List(); + + while (startIndex < segment.Length) + { + int nextParameterStart = IndexOfFirstOpenParameter(segment, startIndex); + if (nextParameterStart == -1) + { + // If there are no more parameters in the segment, capture the remainder as a literal and stop + string lastLiteralPart = GetLiteral(segment.Substring(startIndex)); + if (lastLiteralPart == null) + { + exception = new ArgumentException( + String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment), + "routeTemplate"); + + return null; + } + + if (lastLiteralPart.Length > 0) + { + pathSubsegments.Add(new PathLiteralSubsegment(lastLiteralPart)); + } + break; + } + + int nextParameterEnd = segment.IndexOf('}', nextParameterStart + 1); + if (nextParameterEnd == -1) + { + exception = new ArgumentException( + String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment), + "routeTemplate"); + return null; + } + + string literalPart = GetLiteral(segment.Substring(startIndex, nextParameterStart - startIndex)); + if (literalPart == null) + { + exception = new ArgumentException( + String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment), + "routeTemplate"); + return null; + } + + if (literalPart.Length > 0) + { + pathSubsegments.Add(new PathLiteralSubsegment(literalPart)); + } + + string parameterName = segment.Substring(nextParameterStart + 1, nextParameterEnd - nextParameterStart - 1); + pathSubsegments.Add(new PathParameterSubsegment(parameterName)); + + startIndex = nextParameterEnd + 1; + } + + exception = null; + return pathSubsegments; + } + + private static IList SplitUriToPathSegments(IList uriParts) + { + List pathSegments = new List(); + + foreach (string pathSegment in uriParts) + { + bool isCurrentPartSeparator = IsSeparator(pathSegment); + if (isCurrentPartSeparator) + { + pathSegments.Add(new PathSeparatorSegment()); + } + else + { + Exception exception; + IList subsegments = ParseUriSegment(pathSegment, out exception); + Contract.Assert(exception == null, "This only gets called after the path has been validated, so there should never be an exception here"); + pathSegments.Add(new PathContentSegment(subsegments)); + } + } + return pathSegments; + } + + internal static IList SplitUriToPathSegmentStrings(string uri) + { + List parts = new List(); + + if (String.IsNullOrEmpty(uri)) + { + return parts; + } + + int currentIndex = 0; + + // Split the incoming URI into individual parts + while (currentIndex < uri.Length) + { + int indexOfNextSeparator = uri.IndexOf('/', currentIndex); + if (indexOfNextSeparator == -1) + { + // If there are no more separators, the rest of the string is the last part + string finalPart = uri.Substring(currentIndex); + if (finalPart.Length > 0) + { + parts.Add(finalPart); + } + break; + } + + string nextPart = uri.Substring(currentIndex, indexOfNextSeparator - currentIndex); + if (nextPart.Length > 0) + { + parts.Add(nextPart); + } + + Contract.Assert(uri[indexOfNextSeparator] == '/', "The separator char itself should always be a '/'."); + parts.Add("/"); + currentIndex = indexOfNextSeparator + 1; + } + + return parts; + } + + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm")] + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", + Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] + private static Exception ValidateUriParts(IList pathSegments) + { + Contract.Assert(pathSegments != null, "The value should always come from SplitUri(), and that function should never return null."); + + HashSet usedParameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); + bool? isPreviousPartSeparator = null; + + bool foundCatchAllParameter = false; + + foreach (string pathSegment in pathSegments) + { + if (foundCatchAllParameter) + { + // If we ever start an iteration of the loop and we've already found a + // catchall parameter then we have an invalid URI format. + return new ArgumentException(Resources.TemplateRoute_CatchAllMustBeLast, "routeTemplate"); + } + + bool isCurrentPartSeparator; + if (isPreviousPartSeparator == null) + { + // Prime the loop with the first value + isPreviousPartSeparator = IsSeparator(pathSegment); + isCurrentPartSeparator = isPreviousPartSeparator.Value; + } + else + { + isCurrentPartSeparator = IsSeparator(pathSegment); + + // If both the previous part and the current part are separators, it's invalid + if (isCurrentPartSeparator && isPreviousPartSeparator.Value) + { + return new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, "routeTemplate"); + } + + Contract.Assert(isCurrentPartSeparator != isPreviousPartSeparator.Value, "This assert should only happen if both the current and previous parts are non-separators. This should never happen because consecutive non-separators are always parsed as a single part."); + isPreviousPartSeparator = isCurrentPartSeparator; + } + + // If it's not a separator, parse the segment for parameters and validate it + if (!isCurrentPartSeparator) + { + Exception exception; + IList subsegments = ParseUriSegment(pathSegment, out exception); + if (exception != null) + { + return exception; + } + + exception = ValidateUriSegment(subsegments, usedParameterNames); + if (exception != null) + { + return exception; + } + + foundCatchAllParameter = subsegments.Any(seg => (seg is PathParameterSubsegment) && ((PathParameterSubsegment)seg).IsCatchAll); + } + } + return null; + } + + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", + Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] + private static Exception ValidateUriSegment(IList pathSubsegments, HashSet usedParameterNames) + { + bool segmentContainsCatchAll = false; + + Type previousSegmentType = null; + + foreach (PathSubsegment subsegment in pathSubsegments) + { + if (previousSegmentType != null) + { + if (previousSegmentType == subsegment.GetType()) + { + return new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveParameters, "routeTemplate"); + } + } + previousSegmentType = subsegment.GetType(); + + PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment; + if (literalSubsegment != null) + { + // Nothing to validate for literals - everything is valid + } + else + { + PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment; + if (parameterSubsegment != null) + { + string parameterName = parameterSubsegment.ParameterName; + + if (parameterSubsegment.IsCatchAll) + { + segmentContainsCatchAll = true; + } + + // Check for valid characters in the parameter name + if (!IsValidParameterName(parameterName)) + { + return new ArgumentException( + String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName), + "routeTemplate"); + } + + if (usedParameterNames.Contains(parameterName)) + { + return new ArgumentException( + String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_RepeatedParameter, parameterName), + "routeTemplate"); + } + else + { + usedParameterNames.Add(parameterName); + } + } + else + { + Contract.Assert(false, "Invalid path subsegment type"); + } + } + } + + if (segmentContainsCatchAll && (pathSubsegments.Count != 1)) + { + return new ArgumentException(Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment, "routeTemplate"); + } + + return null; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/VirtualPathData.cs b/src/Microsoft.AspNet.Routing/Template/VirtualPathData.cs new file mode 100644 index 0000000000..e6ec047dcd --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/VirtualPathData.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +namespace Microsoft.AspNet.Routing.Template +{ + public class VirtualPathData : IVirtualPathData + { + private string _virtualPath; + + public VirtualPathData(IRoute route, string virtualPath) + { + if (route == null) + { + throw new ArgumentNullException("route"); + } + + if (virtualPath == null) + { + throw new ArgumentNullException("virtualPath"); + } + + Route = route; + VirtualPath = virtualPath; + } + + public IRoute Route { get; private set; } + + public string VirtualPath + { + get { return _virtualPath; } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + _virtualPath = value; + } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs b/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs new file mode 100644 index 0000000000..927b39a679 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing.Template.Tests +{ + public class Assert : Xunit.Assert + { + public static T Throws(Assert.ThrowsDelegate action, string message) where T : Exception + { + T exception = Assert.Throws(action); + Assert.Equal(message, exception.Message); + return exception; + } + + public static T Throws(Assert.ThrowsDelegateWithReturn action, string message) where T : Exception + { + T exception = Assert.Throws(action); + Assert.Equal(message, exception.Message); + return exception; + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs new file mode 100644 index 0000000000..5096687d93 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Extensions; + +namespace Microsoft.AspNet.Routing.Template.Tests +{ + public class TemplateRouteParserTests + { + [Fact] + public void InvalidTemplate_WithRepeatedParameter() + { + var ex = Assert.Throws( + () => TemplateRouteParser.Parse("{Controller}.mvc/{id}/{controller}"), + "The route parameter name 'controller' appears more than one time in the route template." + Environment.NewLine + "Parameter name: routeTemplate"); + } + + [Theory] + [InlineData("123{a}abc{")] + [InlineData("123{a}abc}")] + [InlineData("xyz}123{a}abc}")] + [InlineData("{{p1}")] + [InlineData("{p1}}")] + [InlineData("p1}}p2{")] + public void InvalidTemplate_WithMismatchedBraces(string template) + { + Assert.Throws( + () => TemplateRouteParser.Parse(template), + @"There is an incomplete parameter in this path segment: '" + template + @"'. Check that each '{' character has a matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveCatchAllInMultiSegment() + { + Assert.Throws( + () => TemplateRouteParser.Parse("123{a}abc{*moo}"), + "A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAll() + { + Assert.Throws( + () => TemplateRouteParser.Parse("{*p1}/{*p2}"), + "A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment() + { + Assert.Throws( + () => TemplateRouteParser.Parse("{*p1}abc{*p2}"), + "A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveCatchAllWithNoName() + { + Assert.Throws( + () => TemplateRouteParser.Parse("foo/{*}"), + @"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveOpenBrace() + { + Assert.Throws( + () => TemplateRouteParser.Parse("foo/{{p1}"), + "There is an incomplete parameter in this path segment: '{{p1}'. Check that each '{' character has a matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveCloseBrace() + { + Assert.Throws( + () => TemplateRouteParser.Parse("foo/{p1}}"), + "There is an incomplete parameter in this path segment: '{p1}}'. Check that each '{' character has a matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_SameParameterTwiceThrows() + { + Assert.Throws( + () => TemplateRouteParser.Parse("{aaa}/{AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows() + { + Assert.Throws( + () => TemplateRouteParser.Parse("{aaa}/{*AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows() + { + Assert.Throws( + () => TemplateRouteParser.Parse("{a}/{aa}a}/{z}"), + "There is an incomplete parameter in this path segment: '{aa}a}'. Check that each '{' character has a matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows() + { + Assert.Throws( + () => TemplateRouteParser.Parse("{a}/{a{aa}/{z}"), + @"The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows() + { + Assert.Throws( + () => TemplateRouteParser.Parse("{a}/{}/{z}"), + @"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithQuestionThrows() + { + Assert.Throws( + () => TemplateRouteParser.Parse("{Controller}.mvc/{?}"), + "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows() + { + Assert.Throws( + () => TemplateRouteParser.Parse("{a}//{z}"), + "The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_WithCatchAllNotAtTheEndThrows() + { + Assert.Throws( + () => TemplateRouteParser.Parse("foo/{p1}/{*p2}/{p3}"), + "A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_RepeatedParametersThrows() + { + Assert.Throws( + () => TemplateRouteParser.Parse("foo/aa{p1}{p2}"), + "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotStartWithSlash() + { + Assert.Throws( + () => TemplateRouteParser.Parse("/foo"), + "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotStartWithTilde() + { + Assert.Throws( + () => TemplateRouteParser.Parse("~foo"), + "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotContainQuestionMark() + { + Assert.Throws( + () => TemplateRouteParser.Parse("foor?bar"), + "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs new file mode 100644 index 0000000000..c8e6c20211 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs @@ -0,0 +1,2595 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading; +using Microsoft.AspNet.Abstractions; +using Xunit; + +namespace Microsoft.AspNet.Routing.Template.Tests +{ + public class TemplateRouteTests + { + [Fact] + public void GetRouteDataWithConstraintsThatIsNotStringThrows() + { + // Arrange + HttpContext context = GetHttpContext("~/category/33"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = 5 }), + null); + + // Act + Assert.Throws(() => r.GetRouteData(context), + "The constraint entry 'category' on the route with route template 'category/{category}' must have a string value or " + + "be of a type which implements 'ITemplateRouteConstraint'."); + } + + + [Fact] + public void MatchSingleRoute() + { + // Arrange + HttpContext context = GetHttpContext("~/Bank/DoAction/123"); + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Equal("Bank", rd.Values["controller"]); + Assert.Equal("DoAction", rd.Values["action"]); + Assert.Equal("123", rd.Values["id"]); + } + + [Fact] + public void NoMatchSingleRoute() + { + // Arrange + HttpContext context = GetHttpContext("~/Bank/DoAction"); + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchSingleRouteWithDefaults() + { + // Arrange + HttpContext context = GetHttpContext("~/Bank/DoAction"); + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { id = "default id" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Equal("Bank", rd.Values["controller"]); + Assert.Equal("DoAction", rd.Values["action"]); + Assert.Equal("default id", rd.Values["id"]); + } + +#if URLGENERATION + + [Fact] + public void MatchSingleRouteWithEmptyDefaults() + { + IHttpVirtualPathData data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}", new RouteValueDictionary(new { val1 = "", val2 = "" }), new RouteValueDictionary(new { val2 = "SomeVal2" })); + Assert.Null(data); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}", new RouteValueDictionary(new { val1 = "", val2 = "" }), new RouteValueDictionary(new { val1 = "a" })); + Assert.Equal("Test/a", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "", val3 = "" }), new RouteValueDictionary(new { val2 = "a" })); + Assert.Null(data); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}", new RouteValueDictionary(new { val1 = "", val2 = "" }), new RouteValueDictionary(new { val1 = "a", val2 = "b" })); + Assert.Equal("Test/a/b", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "", val2 = "", val3 = "" }), new RouteValueDictionary(new { val1 = "a", val2 = "b", val3 = "c" })); + Assert.Equal("Test/a/b/c", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "", val2 = "", val3 = "" }), new RouteValueDictionary(new { val1 = "a", val2 = "b" })); + Assert.Equal("Test/a/b", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "", val2 = "", val3 = "" }), new RouteValueDictionary(new { val1 = "a" })); + Assert.Equal("Test/a", data.VirtualPath); + + } + + private IHttpVirtualPathData GetVirtualPathFromRoute(string path, string template, RouteValueDictionary defaults, RouteValueDictionary values) + { + TemplateRoute r = CreateRoute(template, defaults, null); + + HttpContext context = GetHttpContext(path); + return r.GetVirtualPath(context, values); + } +#endif + + [Fact] + public void NoMatchSingleRouteWithDefaults() + { + // Arrange + HttpContext context = GetHttpContext("~/Bank"); + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { id = "default id" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchRouteWithLiterals() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/111/bar/222"); + TemplateRoute r = CreateRoute("moo/{p1}/bar/{p2}", new RouteValueDictionary(new { p2 = "default p2" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Equal("111", rd.Values["p1"]); + Assert.Equal("222", rd.Values["p2"]); + } + + [Fact] + public void MatchRouteWithLiteralsAndDefaults() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/111/bar/"); + TemplateRoute r = CreateRoute("moo/{p1}/bar/{p2}", new RouteValueDictionary(new { p2 = "default p2" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Equal("111", rd.Values["p1"]); + Assert.Equal("default p2", rd.Values["p2"]); + } + + [Fact] + public void MatchRouteWithOnlyLiterals() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar"); + TemplateRoute r = CreateRoute("moo/bar", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(0, rd.Values.Count); + } + + [Fact] + public void NoMatchRouteWithOnlyLiterals() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar"); + TemplateRoute r = CreateRoute("moo/bars", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchRouteWithExtraSeparators() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar/"); + TemplateRoute r = CreateRoute("moo/bar", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(0, rd.Values.Count); + } + + [Fact] + public void MatchRouteUrlWithExtraSeparators() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar"); + TemplateRoute r = CreateRoute("moo/bar/", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(0, rd.Values.Count); + } + + [Fact] + public void MatchRouteUrlWithParametersAndExtraSeparators() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar"); + TemplateRoute r = CreateRoute("{p1}/{p2}/", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal("moo", rd.Values["p1"]); + Assert.Equal("bar", rd.Values["p2"]); + } + + [Fact] + public void NoMatchRouteUrlWithDifferentLiterals() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar/boo"); + TemplateRoute r = CreateRoute("{p1}/{p2}/baz", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void NoMatchLongerUrl() + { + // Arrange + HttpContext context = GetHttpContext("~/moo/bar"); + TemplateRoute r = CreateRoute("{p1}", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchSimpleFilename() + { + // Arrange + HttpContext context = GetHttpContext("~/default.aspx"); + TemplateRoute r = CreateRoute("DEFAULT.ASPX", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + } + + private void VerifyRouteMatchesWithContext(string route, string requestUrl) + { + HttpContext context = GetHttpContext(requestUrl); + TemplateRoute r = CreateRoute(route, null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + } + + [Fact] + public void MatchEvilRoute() + { + VerifyRouteMatchesWithContext("{prefix}x{suffix}", "~/xxxxxxxxxx"); + VerifyRouteMatchesWithContext("{prefix}xyz{suffix}", "~/xxxxyzxyzxxxxxxyz"); + VerifyRouteMatchesWithContext("{prefix}xyz{suffix}", "~/abcxxxxyzxyzxxxxxxyzxx"); + VerifyRouteMatchesWithContext("{prefix}xyz{suffix}", "~/xyzxyzxyzxyzxyz"); + VerifyRouteMatchesWithContext("{prefix}xyz{suffix}", "~/xyzxyzxyzxyzxyz1"); + VerifyRouteMatchesWithContext("{prefix}xyz{suffix}", "~/xyzxyzxyz"); + VerifyRouteMatchesWithContext("{prefix}aa{suffix}", "~/aaaaa"); + VerifyRouteMatchesWithContext("{prefix}aaa{suffix}", "~/aaaaa"); + } + + [Fact] + public void MatchRouteWithExtraDefaultValues() + { + // Arrange + HttpContext context = GetHttpContext("~/v1"); + TemplateRoute r = CreateRoute("{p1}/{p2}", new RouteValueDictionary(new { p2 = (string)null, foo = "bar" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(3, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Null(rd.Values["p2"]); + Assert.Equal("bar", rd.Values["foo"]); + } + + [Fact] + public void MatchPrettyRouteWithExtraDefaultValues() + { + // Arrange + HttpContext context = GetHttpContext("~/date/2007/08"); + TemplateRoute r = CreateRoute( + "date/{y}/{m}/{d}", + new RouteValueDictionary(new { controller = "blog", action = "showpost", m = (string)null, d = (string)null }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(5, rd.Values.Count); + Assert.Equal("blog", rd.Values["controller"]); + Assert.Equal("showpost", rd.Values["action"]); + Assert.Equal("2007", rd.Values["y"]); + Assert.Equal("08", rd.Values["m"]); + Assert.Null(rd.Values["d"]); + } + + [Fact] + public void GetRouteDataWhenConstraintsMatchesExactlyReturnsMatch() + { + // Arrange + HttpContext context = GetHttpContext("~/category/12"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"\d\d" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(3, rd.Values.Count); + Assert.Equal("store", rd.Values["controller"]); + Assert.Equal("showcat", rd.Values["action"]); + Assert.Equal("12", rd.Values["category"]); + } + + [Fact] + public void GetRouteDataShouldApplyRegExModifiersCorrectly1() + { + // DevDiv Bugs 173408: UrlRouting: Route validation doesn't handle ^ and $ correctly + + // Arrange + HttpContext context = GetHttpContext("~/category/FooBar"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"Foo|Bar" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataShouldApplyRegExModifiersCorrectly2() + { + // DevDiv Bugs 173408: UrlRouting: Route validation doesn't handle ^ and $ correctly + + // Arrange + HttpContext context = GetHttpContext("~/category/Food"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"Foo|Bar" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataShouldApplyRegExModifiersCorrectly3() + { + // DevDiv Bugs 173408: UrlRouting: Route validation doesn't handle ^ and $ correctly + + // Arrange + HttpContext context = GetHttpContext("~/category/Bar"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"Foo|Bar" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(3, rd.Values.Count); + Assert.Equal("store", rd.Values["controller"]); + Assert.Equal("showcat", rd.Values["action"]); + Assert.Equal("Bar", rd.Values["category"]); + } + + [Fact] + public void GetRouteDataWithCaseInsensitiveConstraintsMatches() + { + // Arrange + HttpContext context = GetHttpContext("~/category/aBc"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"[a-z]{3}" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(3, rd.Values.Count); + Assert.Equal("store", rd.Values["controller"]); + Assert.Equal("showcat", rd.Values["action"]); + Assert.Equal("aBc", rd.Values["category"]); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnBothEndsMatches() + { + GetRouteDataHelper( + CreateRoute("language/{lang}-{region}", null), + "language/en-US", + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnLeftEndMatches() + { + GetRouteDataHelper( + CreateRoute("language/{lang}-{region}a", null), + "language/en-USa", + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnRightEndMatches() + { + GetRouteDataHelper( + CreateRoute("language/a{lang}-{region}", null), + "language/aen-US", + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnNeitherEndMatches() + { + GetRouteDataHelper( + CreateRoute("language/a{lang}-{region}a", null), + "language/aen-USa", + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnNeitherEndDoesNotMatch() + { + GetRouteDataHelper( + CreateRoute("language/a{lang}-{region}a", null), + "language/a-USa", + null); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnNeitherEndDoesNotMatch2() + { + GetRouteDataHelper( + CreateRoute("language/a{lang}-{region}a", null), + "language/aen-a", + null); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnBothEndsMatches() + { + GetRouteDataHelper( + CreateRoute("language/{lang}", null), + "language/en", + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnBothEndsTrailingSlashDoesNotMatch() + { + GetRouteDataHelper( + CreateRoute("language/{lang}", null), + "language/", + null); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnBothEndsDoesNotMatch() + { + GetRouteDataHelper( + CreateRoute("language/{lang}", null), + "language", + null); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnLeftEndMatches() + { + GetRouteDataHelper( + CreateRoute("language/{lang}-", null), + "language/en-", + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnRightEndMatches() + { + GetRouteDataHelper( + CreateRoute("language/a{lang}", null), + "language/aen", + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnNeitherEndMatches() + { + GetRouteDataHelper( + CreateRoute("language/a{lang}a", null), + "language/aena", + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentStandardMvcRouteMatches() + { + GetRouteDataHelper( + CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null })), + "home.mvc/index", + new RouteValueDictionary(new { controller = "home", action = "index", id = (string)null })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() + { + GetRouteDataHelper( + CreateRoute("language/{lang}-{region}", new RouteValueDictionary(new { lang = "xx", region = "yy" }), null), + "language/-", + null); + } + +#if URLGENERATION + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnBothEndsMatches() + { + GetVirtualPathHelper( + CreateRoute("language/{lang}-{region}", null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/xx-yy"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnLeftEndMatches() + { + GetVirtualPathHelper( + CreateRoute("language/{lang}-{region}a", null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/xx-yya"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnRightEndMatches() + { + GetVirtualPathHelper( + CreateRoute("language/a{lang}-{region}", null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/axx-yy"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndMatches() + { + GetVirtualPathHelper( + CreateRoute("language/a{lang}-{region}a", null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/axx-yya"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch() + { + GetVirtualPathHelper( + CreateRoute("language/a{lang}-{region}a", null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "", region = "yy" }), + null); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch2() + { + GetVirtualPathHelper( + CreateRoute("language/a{lang}-{region}a", null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "" }), + null); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnBothEndsMatches() + { + GetVirtualPathHelper( + CreateRoute("language/{lang}", null), + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/xx"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnLeftEndMatches() + { + GetVirtualPathHelper( + CreateRoute("language/{lang}-", null), + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/xx-"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnRightEndMatches() + { + GetVirtualPathHelper( + CreateRoute("language/a{lang}", null), + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/axx"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnNeitherEndMatches() + { + GetVirtualPathHelper( + CreateRoute("language/a{lang}a", null), + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/axxa"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentStandardMvcRouteMatches() + { + GetVirtualPathHelper( + CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null })), + new RouteValueDictionary(new { controller = "home", action = "list", id = (string)null }), + new RouteValueDictionary(new { controller = "products" }), + "products.mvc"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() + { + GetVirtualPathHelper( + CreateRoute("language/{lang}-{region}", new RouteValueDictionary(new { lang = "xx", region = "yy" }), null), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "zz" }), + "language/zz-yy"); + } + +#endif + + [Fact] + public void GetRouteDataWithUrlWithMultiSegmentWithRepeatedDots() + { + GetRouteDataHelper( + CreateRoute("{Controller}..mvc/{id}/{Param1}", null), + "Home..mvc/123/p1", + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } + + [Fact] + public void GetRouteDataWithUrlWithTwoRepeatedDots() + { + GetRouteDataHelper( + CreateRoute("{Controller}.mvc/../{action}", null), + "Home.mvc/../index", + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithThreeRepeatedDots() + { + GetRouteDataHelper( + CreateRoute("{Controller}.mvc/.../{action}", null), + "Home.mvc/.../index", + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithManyRepeatedDots() + { + GetRouteDataHelper( + CreateRoute("{Controller}.mvc/../../../{action}", null), + "Home.mvc/../../../index", + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithExclamationPoint() + { + GetRouteDataHelper( + CreateRoute("{Controller}.mvc!/{action}", null), + "Home.mvc!/index", + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithStartingDotDotSlash() + { + GetRouteDataHelper( + CreateRoute("../{Controller}.mvc", null), + "../Home.mvc", + new RouteValueDictionary(new { Controller = "Home" })); + } + + [Fact] + public void GetRouteDataWithUrlWithStartingBackslash() + { + GetRouteDataHelper( + CreateRoute(@"\{Controller}.mvc", null), + @"\Home.mvc", + new RouteValueDictionary(new { Controller = "Home" })); + } + + [Fact] + public void GetRouteDataWithUrlWithBackslashSeparators() + { + GetRouteDataHelper( + CreateRoute(@"{Controller}.mvc\{id}\{Param1}", null), + @"Home.mvc\123\p1", + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } + + [Fact] + public void GetRouteDataWithUrlWithParenthesesLiterals() + { + GetRouteDataHelper( + CreateRoute(@"(Controller).mvc", null), + @"(Controller).mvc", + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithUrlWithTrailingSlashSpace() + { + GetRouteDataHelper( + CreateRoute(@"Controller.mvc/ ", null), + @"Controller.mvc/ ", + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithUrlWithTrailingSpace() + { + GetRouteDataHelper( + CreateRoute(@"Controller.mvc ", null), + @"Controller.mvc ", + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithCatchAllCapturesDots() + { + // DevDiv Bugs 189892: UrlRouting: Catch all parameter cannot capture url segments that contain the "." + GetRouteDataHelper( + CreateRoute( + "Home/ShowPilot/{missionId}/{*name}", + new RouteValueDictionary(new + { + controller = "Home", + action = "ShowPilot", + missionId = (string)null, + name = (string)null + }), + null), + "Home/ShowPilot/777/12345./foobar", + new RouteValueDictionary(new { controller = "Home", action = "ShowPilot", missionId = "777", name = "12345./foobar" })); + } + + [Fact] + public void GetRouteDataWhenConstraintsMatchesPartiallyDoesNotMatch() + { + // Arrange + HttpContext context = GetHttpContext("~/category/a12"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"\d\d" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataWhenConstraintsDoesNotMatch() + { + // Arrange + HttpContext context = GetHttpContext("~/category/ab"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"\d\d" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataWhenOneOfMultipleConstraintsDoesNotMatch() + { + // Arrange + HttpContext context = GetHttpContext("~/category/01/q"); + TemplateRoute r = CreateRoute( + "category/{category}/{sort}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"\d\d", sort = @"a|d" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataWithNonStringValueReturnsTrueIfMatches() + { + // Arrange + HttpContext context = GetHttpContext("~/category"); + TemplateRoute r = CreateRoute( + "category/{foo}", + new RouteValueDictionary(new { controller = "store", action = "showcat", foo = 123 }), + new RouteValueDictionary(new { foo = @"\d{3}" })); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + } + + [Fact] + public void GetRouteDataWithNonStringValueReturnsFalseIfUnmatched() + { + // Arrange + HttpContext context = GetHttpContext("~/category"); + TemplateRoute r = CreateRoute( + "category/{foo}", + new RouteValueDictionary(new { controller = "store", action = "showcat", foo = 123 }), + new RouteValueDictionary(new { foo = @"\d{2}" })); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + +#if URLGENERATION + [Fact] + public void GetUrlWithDefaultValue() + { + // URL should be found but excluding the 'id' parameter, which has only a default value. + GetVirtualPathHelper( + CreateRoute("{controller}/{action}/{id}", + new RouteValueDictionary(new { id = "defaultid" }), null), + new RouteValueDictionary(new { controller = "home", action = "oldaction" }), + new RouteValueDictionary(new { action = "newaction" }), + "home/newaction"); + } + + [Fact] + public void GetVirtualPathWithEmptyStringRequiredValueReturnsNull() + { + GetVirtualPathHelper( + CreateRoute("foo/{controller}", null), + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = "" }), + null); + } + + [Fact] + public void GetVirtualPathWithNullRequiredValueReturnsNull() + { + GetVirtualPathHelper( + CreateRoute("foo/{controller}", null), + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = (string)null }), + null); + } + + [Fact] + public void GetVirtualPathWithRequiredValueReturnsPath() + { + GetVirtualPathHelper( + CreateRoute("foo/{controller}", null), + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = "home" }), + "foo/home"); + } + + [Fact] + public void GetUrlWithNullDefaultValue() + { + // URL should be found but excluding the 'id' parameter, which has only a default value. + GetVirtualPathHelper( + CreateRoute( + "{controller}/{action}/{id}", + new RouteValueDictionary(new { id = (string)null }), + null), + new RouteValueDictionary(new { controller = "home", action = "oldaction", id = (string)null }), + new RouteValueDictionary(new { action = "newaction" }), + "home/newaction"); + } + + [Fact] + public void GetUrlWithMissingValuesDoesntMatch() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", null); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "oldaction"); + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("action", "newaction"); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.Null(vpd); + } + + [Fact] + public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrenIRoute() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + IRouteCollection rt = new DefaultRouteCollection(); + rt.Add(CreateRoute("date/{y}/{m}/{d}", null)); + rt.Add(CreateRoute("{controller}/{action}/{id}", null)); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "dostuff"); + + var values = CreateRouteValueDictionary(); + values.Add("y", "2007"); + values.Add("m", "08"); + values.Add("d", "12"); + + // Act + var vpd = rt.GetVirtualPath(context, values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app/date/2007/08/12", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrentRouteAsSecondRoute() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + + IRouteCollection rt = new DefaultRouteCollection(); + rt.Add(CreateRoute("{controller}/{action}/{id}")); + rt.Add(CreateRoute("date/{y}/{m}/{d}")); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "dostuff"); + + var values = CreateRouteValueDictionary(); + values.Add("y", "2007"); + values.Add("m", "08"); + values.Add("d", "12"); + + // Act + var vpd = rt.GetVirtualPath(context, values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app/date/2007/08/12", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithEmptyRequiredValuesReturnsNull() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{p1}/{p2}/{p3}", new RouteValueDictionary(), null); + + var rd = CreateRouteData(); + rd.Values.Add("p1", "v1"); + + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("p2", ""); + valuesDictionary.Add("p3", ""); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.Null(vpd); + } + + [Fact] + public void GetUrlWithEmptyOptionalValuesReturnsShortUrl() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{p1}/{p2}/{p3}", new RouteValueDictionary(new { p2 = "d2", p3 = "d3", }), null); + + var rd = CreateRouteData(); + rd.Values.Add("p1", "v1"); + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("p2", ""); + valuesDictionary.Add("p3", ""); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("v1", vpd.VirtualPath); + } + + [Fact] + public void GetUrlShouldIgnoreValuesAfterChangedParameter() + { + // DevDiv Bugs 157535 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "orig"); + rd.Values.Add("action", "init"); + rd.Values.Add("id", "123"); + + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }), null); + + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("action", "new"); + + // Act + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("orig/new", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithRouteThatHasExtensionWithSubsequentDefaultValueIncludesExtensionButNotDefaultValue() + { + // DevDiv Bugs 156606 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bank"); + rd.Values.Add("action", "MakeDeposit"); + rd.Values.Add("accountId", "7770"); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "{controller}.mvc/Deposit/{accountId}", + new RouteValueDictionary(new { Action = "DepositView" }))); + + // Note: This route was in the original bug, but it turns out that this behavior is incorrect. With the + // recent fix to Route (in this changelist) this route would have been selected since we have values for + // all three required parameters. + //rc.Add(new Route { + // Url = "{controller}.mvc/{action}/{accountId}", + // RouteHandler = new DummyRouteHandler() + //}); + + // This route should be chosen because the requested action is List. Since the default value of the action + // is List then the Action should not be in the URL. However, the file extension should be included since + // it is considered "safe." + rc.Add(CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { Action = "List" }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "List"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Bank.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithRouteThatHasDifferentControllerCaseShouldStillMatch() + { + // DevDiv Bugs 159099 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bar"); + rd.Values.Add("action", "bbb"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute("PrettyFooUrl", new RouteValueDictionary(new { controller = "Foo", action = "aaa", id = (string)null }))); + + rc.Add(CreateRoute("PrettyBarUrl", new RouteValueDictionary(new { controller = "Bar", action = "bbb", id = (string)null }))); + + rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "aaa"); + values.Add("Controller", "foo"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/PrettyFooUrl", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithNoChangedValuesShouldProduceSameUrl() + { + // DevDiv Bugs 159469 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "Index"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Home.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlAppliesConstraintsRulesToChooseRoute() + { + // DevDiv Bugs 159678: MVC: URL generation chooses the wrong route for generating URLs when route validation is in place + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "foo.mvc/{action}", + new RouteValueDictionary(new { controller = "Home" }), + new RouteValueDictionary(new { controller = "Home", action = "Contact", httpMethod = CreateHttpMethodConstraint("get") }))); + + rc.Add(CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { action = "Index" }), + new RouteValueDictionary(new { controller = "Home", action = "(Index|About)", httpMethod = CreateHttpMethodConstraint("post") }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "Index"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Home.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithNullForMiddleParameterIgnoresRemainingParameters() + { + // DevDiv Bugs 170859: UrlRouting: Passing null or empty string for a parameter in the middle of a route generates the wrong Url + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "UrlRouting"); + rd.Values.Add("action", "Play"); + rd.Values.Add("category", "Photos"); + rd.Values.Add("year", "2008"); + rd.Values.Add("occasion", "Easter"); + rd.Values.Add("SafeParam", "SafeParamValue"); + + TemplateRoute r = CreateRoute( + "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", + new RouteValueDictionary(new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" })); + + // Act + RouteValueDictionary values = CreateRouteValueDictionary(); + values.Add("year", null); + values.Add("occasion", "Hola"); + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("UrlGeneration1/UrlRouting.mvc/Play/Photos/1995/Hola", vpd.VirtualPath); + } + + [Fact] + public void GetUrlShouldValidateOnlyAcceptedParametersAndUserDefaultValuesForInvalidatedParameters() + { + // DevDiv Bugs 172913: UrlRouting: Parameter validation should not run against current request values if a new value has been supplied at a previous position + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("Controller", "UrlRouting"); + rd.Values.Add("Name", "MissmatchedValidateParams"); + rd.Values.Add("action", "MissmatchedValidateParameters2"); + rd.Values.Add("ValidateParam1", "special1"); + rd.Values.Add("ValidateParam2", "special2"); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}", + new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams", ValidateParam2 = "valid" }), + new RouteValueDictionary(new { ValidateParam1 = "valid.*", ValidateParam2 = "valid.*" }))); + + rc.Add(CreateRoute( + "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}", + new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams" }), + new RouteValueDictionary(new { ValidateParam1 = "special.*", ValidateParam2 = "special.*" }))); + + var values = CreateRouteValueDictionary(); + values.Add("Name", "MissmatchedValidateParams"); + values.Add("ValidateParam1", "valid1"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/UrlConstraints/Validation.mvc/Input5/MissmatchedValidateParameters2/valid1", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithEmptyStringForMiddleParameterIgnoresRemainingParameters() + { + // DevDiv Bugs 170859: UrlRouting: Passing null or empty string for a parameter in the middle of a route generates the wrong Url + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "UrlRouting"); + rd.Values.Add("action", "Play"); + rd.Values.Add("category", "Photos"); + rd.Values.Add("year", "2008"); + rd.Values.Add("occasion", "Easter"); + rd.Values.Add("SafeParam", "SafeParamValue"); + + TemplateRoute r = CreateRoute( + "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", + new RouteValueDictionary(new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" })); + + // Act + RouteValueDictionary values = CreateRouteValueDictionary(); + values.Add("year", String.Empty); + values.Add("occasion", "Hola"); + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("UrlGeneration1/UrlRouting.mvc/Play/Photos/1995/Hola", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithEmptyStringForMiddleParameterShouldUseDefaultValue() + { + // DevDiv Bugs 172084: UrlRouting: Route.GetUrl generates the wrong route of new values has a different controller and route has an action parameter with default + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("Controller", "Test"); + rd.Values.Add("Action", "Fallback"); + rd.Values.Add("param1", "fallback1"); + rd.Values.Add("param2", "fallback2"); + rd.Values.Add("param3", "fallback3"); + + TemplateRoute r = CreateRoute( + "{controller}.mvc/{action}/{param1}", + new RouteValueDictionary(new { Controller = "Test", Action = "Default" })); + + // Act + RouteValueDictionary values = CreateRouteValueDictionary(); + values.Add("controller", "subtest"); + values.Add("param1", "b"); + // The original bug for this included this value, but with the new support for + // creating query string values it changes the behavior such that the URL is + // not what was originally expected. To preserve the general behavior of this + // unit test the 'param2' value is no longer being added. + //values.Add("param2", "a"); + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("subtest.mvc/Default/b", vpd.VirtualPath); + } + + [Fact] + public void GetUrlVerifyEncoding() + { + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + TemplateRoute r = CreateRoute( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home" })); + + // Act + RouteValueDictionary values = CreateRouteValueDictionary(); + values.Add("controller", "#;?:@&=+$,"); + values.Add("action", "showcategory"); + values.Add("id", 123); + values.Add("so?rt", "de?sc"); + values.Add("maxPrice", 100); + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("%23%3b%3f%3a%40%26%3d%2b%24%2c.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100", vpd.VirtualPath); + } + + [Fact] + public void GetUrlGeneratesQueryStringForNewValuesAndEscapesQueryString() + { + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + TemplateRoute r = CreateRoute( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home" })); + + // Act + RouteValueDictionary values = CreateRouteValueDictionary(); + values.Add("controller", "products"); + values.Add("action", "showcategory"); + values.Add("id", 123); + values.Add("so?rt", "de?sc"); + values.Add("maxPrice", 100); + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("products.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100", vpd.VirtualPath); + } + + [Fact] + public void GetUrlGeneratesQueryStringForNewValuesButIgnoresNewValuesThatMatchDefaults() + { + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + TemplateRoute r = CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { controller = "Home", Custom = "customValue" })); + + // Act + RouteValueDictionary values = CreateRouteValueDictionary(); + values.Add("controller", "products"); + values.Add("action", "showcategory"); + values.Add("id", 123); + values.Add("sort", "desc"); + values.Add("maxPrice", 100); + values.Add("custom", "customValue"); + var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("products.mvc/showcategory/123?sort=desc&maxPrice=100", vpd.VirtualPath); + } + + [Fact] + public void GetVirtualPathEncodesParametersAndLiterals() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("bl%og/{controller}/he llo/{action}", null); + var rd = CreateRouteData(); + rd.Values.Add("controller", "ho%me"); + rd.Values.Add("action", "li st"); + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("bl%25og/ho%25me/he%20llo/li%20st", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + } + + [Fact] + public void GetVirtualPathUsesCurrentValuesNotInRouteToMatch() + { + // DevDiv Bugs 177401: UrlRouting: Incorrect route picked on urlgeneration if using controller from ambient values and route does not have a url parameter for controller + + // DevDiv Bugs 191162: UrlRouting: Route does not match when an ambient route value doesn't match a required default value in the target route + // Because of this bug the test was split into two separate verifications since the original test was verifying slightly incorrect behavior + + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r1 = CreateRoute( + "ParameterMatching.mvc/{Action}/{product}", + new RouteValueDictionary(new { Controller = "ParameterMatching", product = (string)null }), + null); + + TemplateRoute r2 = CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { Action = "List" }), + new RouteValueDictionary(new { Controller = "Action|Bank|Overridden|DerivedFromAction|OverrideInvokeActionAndExecute|InvalidControllerName|Store|HtmlHelpers|(T|t)est|UrlHelpers|Custom|Parent|Child|TempData|ViewFactory|LocatingViews|AccessingDataInViews|ViewOverrides|ViewMasterPage|InlineCompileError|CustomView" }), + null); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bank"); + rd.Values.Add("Action", "List"); + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("action", "AttemptLogin"); + + // Act for first route + var vpd = r1.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("ParameterMatching.mvc/AttemptLogin", vpd.VirtualPath); + + // Act for second route + vpd = r2.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("Bank.mvc/AttemptLogin", vpd.VirtualPath); + } + +#endif + [Fact] + public void RouteWithCatchAllClauseCapturesManySlashes() + { + // Arrange + HttpContext context = GetHttpContext("~/v1/v2/v3"); + TemplateRoute r = CreateRoute("{p1}/{*p2}", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Equal("v2/v3", rd.Values["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseCapturesTrailingSlash() + { + // Arrange + HttpContext context = GetHttpContext("~/v1/"); + TemplateRoute r = CreateRoute("{p1}/{*p2}", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Null(rd.Values["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseCapturesEmptyContent() + { + // Arrange + HttpContext context = GetHttpContext("~/v1"); + TemplateRoute r = CreateRoute("{p1}/{*p2}", null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Null(rd.Values["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseUsesDefaultValueForEmptyContent() + { + // Arrange + HttpContext context = GetHttpContext("~/v1"); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { p2 = "catchall" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Equal("catchall", rd.Values["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseIgnoresDefaultValueForNonEmptyContent() + { + // Arrange + HttpContext context = GetHttpContext("~/v1/hello/whatever"); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { p2 = "catchall" }), null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Equal("hello/whatever", rd.Values["p2"]); + } + + [Fact] + public void RouteWithCatchAllRejectsConstraints() + { + // Arrange + HttpContext context = GetHttpContext("~/v1/abcd"); + TemplateRoute r = CreateRoute( + "{p1}/{*p2}", + new RouteValueDictionary(new { p2 = "catchall" }), + new RouteValueDictionary(new { p2 = "\\d{4}" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void RouteWithCatchAllAcceptsConstraints() + { + // Arrange + HttpContext context = GetHttpContext("~/v1/1234"); + TemplateRoute r = CreateRoute( + "{p1}/{*p2}", + new RouteValueDictionary(new { p2 = "catchall" }), + new RouteValueDictionary(new { p2 = "\\d{4}" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("v1", rd.Values["p1"]); + Assert.Equal("1234", rd.Values["p2"]); + } + +#if URLGENERATION + + [Fact] + public void GetUrlWithCatchAllWithValue() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { id = "defaultid" }), null); + + var rd = CreateRouteData(); + rd.Values.Add("p1", "v1"); + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("p2", "v2a/v2b"); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("v1/v2a/v2b", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithCatchAllWithEmptyValue() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { id = "defaultid" }), null); + + var rd = CreateRouteData(); + rd.Values.Add("p1", "v1"); + + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("p2", ""); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("v1", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithCatchAllWithNullValue() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { id = "defaultid" }), null); + + var rd = CreateRouteData(); + rd.Values.Add("p1", "v1"); + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("p2", null); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("v1", vpd.VirtualPath); + } + + [Fact] + public void GetVirtualPathWithDataTokensCopiesThemFromRouteToVirtualPathData() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{controller}/{action}", null, null, new RouteValueDictionary(new { foo = "bar", qux = "quux" })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "index"); + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("home/index", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + Assert.Equal(2, vpd.DataTokens.Count); + Assert.Equal("bar", vpd.DataTokens["foo"]); + Assert.Equal("quux", vpd.DataTokens["qux"]); + } + + [Fact] + public void GetVirtualPathWithValidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "index"); + + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("home/index", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("index", r.ConstraintData.ParameterValue); + } + + [Fact] + public void GetVirtualPathWithInvalidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "list"); + + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.Null(vpd); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("list", r.ConstraintData.ParameterValue); + } + +#if DATATOKENS + + [Fact] + public void GetUrlWithCatchAllWithAmbientValue() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { id = "defaultid" }), null, null); + + var rd = CreateRouteData(); + rd.Values.Add("p1", "v1"); + rd.Values.Add("p2", "ambient-catch-all"); + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("v1/ambient-catch-all", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + Assert.Equal(0, vpd.DataTokens.Count); + } +#endif +#endif + +#if DATATOKENS + + [Fact] + public void GetRouteDataWithDataTokensCopiesThemFromRouteToIRouteData() + { + // Arrange + HttpContext context = GetHttpContext(null, "~/category/33", null); + TemplateRoute r = CreateRoute("category/{category}", null, null, new RouteValueDictionary(new { foo = "bar", qux = "quux" })); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(1, rd.Values.Count); + Assert.Equal(2, rd.DataTokens.Count); + Assert.Equal("33", rd.Values["category"]); + Assert.Equal("bar", rd.DataTokens["foo"]); + Assert.Equal("quux", rd.DataTokens["qux"]); + } + +#endif + + [Fact] + public void GetRouteDataWithValidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("~/home/index"); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Values.Count); + Assert.Equal("home", rd.Values["controller"]); + Assert.Equal("index", rd.Values["action"]); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("index", r.ConstraintData.ParameterValue); + } + + [Fact] + public void GetRouteDataWithInvalidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("~/home/list"); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("list", r.ConstraintData.ParameterValue); + } + + [Fact] + public void GetRouteDataWithConstraintIsCultureInsensitive() + { + // Arrange + HttpContext context = GetHttpContext("~/category/\u0130"); // Turkish upper-case dotted I + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { category = @"[a-z]+" }), + null); + + // Act + Thread currentThread = Thread.CurrentThread; + CultureInfo backupCulture = currentThread.CurrentCulture; + RouteMatch rd; + try + { + currentThread.CurrentCulture = new CultureInfo("tr-TR"); // Turkish culture + rd = r.GetRouteData(context); + } + finally + { + currentThread.CurrentCulture = backupCulture; + } + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataWithConstraintThatHasNoValueDoesNotMatch() + { + // Arrange + HttpContext context = GetHttpContext(null, "~/category/33"); + TemplateRoute r = CreateRoute( + "category/{category}", + new RouteValueDictionary(new { controller = "store", action = "showcat" }), + new RouteValueDictionary(new { foo = @"\d\d\d" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void GetRouteDataWithCatchAllConstraintThatHasNoValueDoesNotMatch() + { + // Arrange + HttpContext context = GetHttpContext(null, "~/category"); + TemplateRoute r = CreateRoute( + "category/{*therest}", + null, + new RouteValueDictionary(new { therest = @"hola" }), + null); + + // Act + var rd = r.GetRouteData(context); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void ProcessConstraintShouldGetCalledForCustomConstraintDuringUrlGeneration() + { + // DevDiv Bugs 178588: UrlRouting: ProcessConstraint is not invoked on a custom constraint that is not mapped to a url parameter during urlgeneration + + // Arrange + HttpContext context = GetHttpContext("/app", null); + + DevDivBugs178588CustomRoute r = new DevDivBugs178588CustomRoute( + "CustomPath.mvc/{action}/{param1}/{param2}", + new RouteValueDictionary(new { Controller = "Test" }), + new RouteValueDictionary(new { foo = new DevDivBugs178588CustomConstraint() })); + + var rd = CreateRouteData(); + rd.Values.Add("action", "Test"); + rd.Values.Add("param1", "111"); + rd.Values.Add("param2", "222"); + rd.Values.Add("Controller", "Test"); + + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.Null(vpd); + } + + [Fact] + public void GetRouteDataMatchesEntireLiteralSegmentScenario1a() + { + TemplateRoute r = CreateRoute( + "CatchAllParamsWithDefaults/{Controller}.mvc/{Action}/{*therest}", + new RouteValueDictionary(new { therest = "Hello" }), + new RouteValueDictionary(new { Controller = "CatchAllParams" }), + null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + // Scenario 1.a. + GetRouteDataHelper( + r, + "CatchAllParamsWithDefaults/CatchAllParams.mvc/TestCatchAllParamInIRouteData", + new RouteValueDictionary(new { Controller = "CatchAllParams", Action = "TestCatchAllParamInIRouteData", therest = "Hello" })); + } + + [Fact] + public void GetRouteDataMatchesEntireLiteralSegmentScenario1b() + { + TemplateRoute r = CreateRoute( + "CatchAllParams/{Controller}.mvc/{Action}/{*therest}", + null, + new RouteValueDictionary(new { Controller = "CatchAllParams" }), + null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + // Scenario 1.b. + GetRouteDataHelper( + r, + "CatchAllParamsWithDefaults/CatchAllParams.mvc/TestCatchAllParamInIRouteData", + null); + } + + [Fact] + public void GetRouteDataMatchesEntireLiteralSegmentScenario2() + { + TemplateRoute r = CreateRoute( + "{controller}.mvc/Login", + new RouteValueDictionary(new { Action = "LoginView" }), + new RouteValueDictionary(new { Controller = "Bank" }), + null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + // Scenario 2 + GetRouteDataHelper( + r, + "Bank.mvc/AttemptLogin", + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchOnlyLeftLiteralMatch() + { + TemplateRoute r = CreateRoute("foo", null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + GetRouteDataHelper( + r, + "fooBAR", + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchOnlyRightLiteralMatch() + { + TemplateRoute r = CreateRoute("foo", null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + GetRouteDataHelper( + r, + "BARfoo", + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchMiddleLiteralMatch() + { + TemplateRoute r = CreateRoute("foo", null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + GetRouteDataHelper( + r, + "BARfooBAR", + null); + } + + [Fact] + public void GetRouteDataDoesMatchesExactLiteralMatch() + { + TemplateRoute r = CreateRoute("foo", null); + + // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url + GetRouteDataHelper( + r, + "foo", + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithWeirdParameterNames() + { + TemplateRoute r = CreateRoute( + "foo/{ }/{.!$%}/{dynamic.data}/{op.tional}", + new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }, + null); + + GetRouteDataHelper( + r, + "foo/space/weird/orderid", + new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weird" }, { "dynamic.data", "orderid" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }); + } + +#if URLGENERATION + + [Fact] + public void UrlWithEscapedOpenCloseBraces() + { + RouteFormatHelper("foo/{{p1}}", "foo/{p1}"); + } + + private static void RouteFormatHelper(string routeUrl, string requestUrl) + { + RouteValueDictionary defaults = new RouteValueDictionary(new { route = "matched" }); + TemplateRoute r = CreateRoute(routeUrl, defaults, null); + + GetRouteDataHelper(r, requestUrl, defaults); + GetVirtualPathHelper(r, new RouteValueDictionary(), null, Uri.EscapeUriString(requestUrl)); + } + + [Fact] + public void UrlWithEscapedOpenBraceAtTheEnd() + { + RouteFormatHelper("bar{{", "bar{"); + } + + [Fact] + public void UrlWithEscapedOpenBraceAtTheBeginning() + { + RouteFormatHelper("{{bar", "{bar"); + } + + [Fact] + public void UrlWithRepeatedEscapedOpenBrace() + { + RouteFormatHelper("foo{{{{bar", "foo{{bar"); + } + + [Fact] + public void UrlWithEscapedCloseBraceAtTheEnd() + { + RouteFormatHelper("bar}}", "bar}"); + } + + [Fact] + public void UrlWithEscapedCloseBraceAtTheBeginning() + { + RouteFormatHelper("}}bar", "}bar"); + } + + [Fact] + public void UrlWithRepeatedEscapedCloseBrace() + { + RouteFormatHelper("foo}}}}bar", "foo}}bar"); + } + + [Fact] + public void GetVirtualPathWithUnusedNullValueShouldGenerateUrlAndIgnoreNullValue() + { + // DevDiv Bugs 194371: UrlRouting: Exception thrown when generating URL that has some null values + GetVirtualPathHelper( + CreateRoute( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { action = "Index", id = "" }), + null), + new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }), + new RouteValueDictionary(new { controller = "Home", action = "TestAction", id = "1", format = (string)null }), + "Home.mvc/TestAction/1"); + } + + [Fact] + public void GetVirtualPathCanFillInSeparatedParametersWithDefaultValues() + { + GetVirtualPathHelper( + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + new RouteValueDictionary(), + new RouteValueDictionary(new { controller = "Orders" }), + "Orders/en-US"); + } +#endif + + [Fact] + public void GetRouteDataDoesNotMatchRouteWithLiteralSeparatorDefaultsButNoValue() + { + GetRouteDataHelper( + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + "foo", + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchesRouteWithLiteralSeparatorDefaultsAndLeftValue() + { + GetRouteDataHelper( + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + "foo/xx-", + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchesRouteWithLiteralSeparatorDefaultsAndRightValue() + { + GetRouteDataHelper( + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + "foo/-yy", + null); + } + + [Fact] + public void GetRouteDataMatchesRouteWithLiteralSeparatorDefaultsAndValue() + { + GetRouteDataHelper( + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + "foo/xx-yy", + new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } }); + } + +#if URLGENERATION + + [Fact] + public void GetVirtualPathWithNonParameterConstraintReturnsUrlWithoutQueryString() + { + // DevDiv Bugs 199612: UrlRouting: UrlGeneration should not append parameter to query string if it is a Constraint parameter and not a Url parameter + GetVirtualPathHelper( + CreateRoute("{Controller}.mvc/{action}/{end}", null, new RouteValueDictionary(new { foo = CreateHttpMethodConstraint("GET") }), null), + new RouteValueDictionary(), + new RouteValueDictionary(new { controller = "Orders", action = "Index", end = "end", foo = "GET" }), + "Orders.mvc/Index/end"); + } + + [Fact] + public void DefaultRoutingValuesTestWithStringEmpty() + { + var data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "42", val2 = "", val3 = "" }), new RouteValueDictionary()); + Assert.Equal("Test/42", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}/{val4}", new RouteValueDictionary(new { val1 = "21", val2 = "", val3 = "", val4 = "" }), new RouteValueDictionary(new { val1 = "42", val2 = "11", val3 = "", val4 = "" })); + Assert.Equal("Test/42/11", data.VirtualPath); + + } + + [Fact] + public void MixedDefaultAndExplicitRoutingValuesTestWithStringEmpty() + { + var data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "21", val2 = "", val3 = "" }), new RouteValueDictionary(new { val1 = "42" })); + Assert.Equal("Test/42", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}/{val4}", new RouteValueDictionary(new { val1 = "21", val2 = "", val3 = "", val4 = "" }), new RouteValueDictionary(new { val1 = "42", val2 = "11" })); + Assert.Equal("Test/42/11", data.VirtualPath); + } + + [Fact] + public void DefaultRoutingValuesTestWithNull() + { + var data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "42", val2 = (string)null, val3 = (string)null }), new RouteValueDictionary()); + Assert.Equal("Test/42", data.VirtualPath); + } + + [Fact] + public void MixedDefaultAndExplicitRoutingValuesTestWithNull() + { + var data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "21", val2 = (string)null, val3 = (string)null }), new RouteValueDictionary(new { val1 = "42" })); + Assert.Equal("Test/42", data.VirtualPath); + + data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}/{val4}", new RouteValueDictionary(new { val1 = "21", val2 = (string)null, val3 = (string)null, val4 = (string)null }), new RouteValueDictionary(new { val1 = "42", val2 = "11" })); + Assert.Equal("Test/42/11", data.VirtualPath); + } + +#endif + + private static IRouteValues CreateRouteData() + { + return new RouteValues(new Dictionary(StringComparer.OrdinalIgnoreCase)); + } + + private static RouteValueDictionary CreateRouteValueDictionary() + { + var values = new RouteValueDictionary(); + return values; + } + + private static void GetRouteDataHelper(TemplateRoute route, string requestPath, RouteValueDictionary expectedValues) + { + // Arrange + HttpContext context = GetHttpContext(requestPath); + + // Act + var rd = route.GetRouteData(context); + + // Assert + if (expectedValues == null) + { + Assert.Null(rd); + } + else + { + Assert.NotNull(rd); + Assert.Equal(rd.Values.Count, expectedValues.Count); + foreach (string key in rd.Values.Keys) + { + Assert.Equal(expectedValues[key], rd.Values[key]); + } + } + } + +#if URLGENERATION + private static void GetVirtualPathHelper(TemplateRoute route, RouteValueDictionary currentValues, RouteValueDictionary newValues, string expectedPath) + { + // Arrange + newValues = newValues ?? new RouteValueDictionary(); + + HttpContext context = GetHttpContext("/app", String.Empty, null); + var rd = CreateRouteData(); + foreach (var currentValue in currentValues) + { + rd.Values.Add(currentValue.Key, currentValue.Value); + } + + // Act + var vpd = route.GetVirtualPath(context, newValues); + + // Assert + if (expectedPath == null) + { + Assert.Null(vpd); + } + else + { + Assert.NotNull(vpd); + Assert.Equal(expectedPath, vpd.VirtualPath); + } + } + +#endif + private static ITemplateRouteConstraint CreateHttpMethodConstraint(params string[] methods) + { + return null; + } + + internal static HttpContext GetHttpContext(string requestPath) + { + return GetHttpContext(null, requestPath); + } + + private static HttpContext GetHttpContext(string appPath, string requestPath) + { + if (!String.IsNullOrEmpty(requestPath) && requestPath[0] == '~') + { + requestPath = requestPath.Substring(1); + } + + if (!String.IsNullOrEmpty(requestPath) && requestPath[0] != '/') + { + requestPath = "/" + requestPath; + } + + var context = new MockHttpContext(); + context.Request.Path = new PathString(requestPath); + context.Request.PathBase = new PathString(appPath); + + return context; + } + + private static TemplateRoute CreateRoute(string template) + { + return CreateRoute(template, null, null, null); + } + + private static TemplateRoute CreateRoute(string template, RouteValueDictionary defaults) + { + return CreateRoute(template, defaults, null, null); + } + + private static TemplateRoute CreateRoute(string template, RouteValueDictionary defaults, RouteValueDictionary constraints) + { + return CreateRoute(template, defaults, constraints, null); + } + + private static TemplateRoute CreateRoute(string template, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens) + { + return new TemplateRoute(template, defaults, constraints, dataTokens); + } + + private class DevDivBugs178588CustomConstraint + { + public string AllowedHeader + { + get; + set; + } + } + + private class DevDivBugs178588CustomRoute : TemplateRoute + { + public DevDivBugs178588CustomRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints) + : base(url, defaults, constraints, null) + { + } + + protected override bool ProcessConstraint(HttpContext httpContext, object constraint, string parameterName, IDictionary values, RouteDirection routeDirection) + { + if (constraint is DevDivBugs178588CustomConstraint) + { + return false; + } + else + { + return base.ProcessConstraint(httpContext, constraint, parameterName, values, routeDirection); + } + } + } + + private sealed class ConstraintData + { + public object Constraint + { + get; + set; + } + public string ParameterName + { + get; + set; + } + public object ParameterValue + { + get; + set; + } + } + + private class CustomConstraintTemplateRoute : TemplateRoute + { + public CustomConstraintTemplateRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints) + : base(url, defaults, constraints, null) + { + } + + public ConstraintData ConstraintData + { + get; + set; + } + + protected override bool ProcessConstraint(HttpContext request, object constraint, string parameterName, IDictionary values, RouteDirection routeDirection) + { + object parameterValue; + values.TryGetValue(parameterName, out parameterValue); + + // Save the parameter values to validate them in the unit tests + ConstraintData = new ConstraintData + { + Constraint = constraint, + ParameterName = parameterName, + ParameterValue = parameterValue, + }; + + if (constraint is int) + { + int lengthRequirement = (int)constraint; + string paramString = parameterValue as string; + if (paramString == null) + { + throw new InvalidOperationException("This constraint only works with string values."); + } + return (paramString.Length == lengthRequirement); + } + else + { + return base.ProcessConstraint(request, constraint, parameterName, values, routeDirection); + } + } + } + + // This is a placeholder + private class RouteValueDictionary : Dictionary + { + public RouteValueDictionary() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + public RouteValueDictionary(object obj) + : base(StringComparer.OrdinalIgnoreCase) + { + foreach (var property in obj.GetType().GetProperties()) + { + Add(property.Name, property.GetValue(obj)); + } + } + } + + private class MockHttpContext : HttpContext + { + private readonly Dictionary _features = new Dictionary(); + private readonly MockHttpRequest _request; + + public MockHttpContext() + { + _request = new MockHttpRequest(this); + } + + public override void Dispose() + { + } + + public override object GetFeature(Type type) + { + return _features[type]; + } + + public override IDictionary Items + { + get { throw new NotImplementedException(); } + } + + public override HttpRequest Request + { + get { return _request; } + } + + public override HttpResponse Response + { + get { throw new NotImplementedException(); } + } + + public override void SetFeature(Type type, object instance) + { + _features[type] = instance; + } + } + + private class MockHttpRequest : HttpRequest + { + private readonly HttpContext _context; + public MockHttpRequest(HttpContext context) + { + _context = context; + } + + public override Stream Body + { + get; + set; + } + + public override CancellationToken CallCanceled + { + get; + set; + } + + public override IReadableStringCollection Cookies + { + get { throw new NotImplementedException(); } + } + + public override IHeaderDictionary Headers + { + get { throw new NotImplementedException(); } + } + + public override HostString Host + { + get; + set; + } + + public override HttpContext HttpContext + { + get { return _context; } + } + + public override bool IsSecure + { + get { throw new NotImplementedException(); } + } + + public override string Method + { + get; + set; + } + + public override PathString Path + { + get; + set; + } + + public override PathString PathBase + { + get; + set; + } + + public override string Protocol + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public override IReadableStringCollection Query + { + get { throw new NotImplementedException(); } + } + + public override QueryString QueryString + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + + public override string Scheme + { + get + { + throw new NotImplementedException(); + } + set + { + throw new NotImplementedException(); + } + } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/project.json b/test/Microsoft.AspNet.Routing.Tests/project.json index 5168efba28..6261e25ace 100644 --- a/test/Microsoft.AspNet.Routing.Tests/project.json +++ b/test/Microsoft.AspNet.Routing.Tests/project.json @@ -1,12 +1,15 @@ { "version": "0.1-alpha-*", "dependencies": { + "Microsoft.AspNet.Abstractions" : "0.1-alpha-*", "Microsoft.AspNet.Routing" : "" }, "configurations": { "net45": { "dependencies": { - "Owin": "1.0" + "Owin": "1.0", + "xunit": "1.9.2", + "xunit.extensions": "1.9.2" } } } From 85225055b99fe7a1337e5563004a7466d5fca460 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 6 Feb 2014 11:56:48 -0800 Subject: [PATCH 019/616] removing datatokens/constraints/url-generation --- .../Template/BoundRouteTemplate.cs | 16 - .../Template/ITemplateRouteConstraint.cs | 12 - .../Template/IVirtualPathData.cs | 11 - .../Template/RouteDirection.cs | 10 - .../Template/TemplateParsedRoute.cs | 463 +---- .../Template/TemplateRoute.cs | 198 +- .../Template/VirtualPathData.cs | 42 - .../Template/TemplateRouteTests.cs | 1742 +---------------- 8 files changed, 73 insertions(+), 2421 deletions(-) delete mode 100644 src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/ITemplateRouteConstraint.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/IVirtualPathData.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/RouteDirection.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/VirtualPathData.cs diff --git a/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs b/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs deleted file mode 100644 index 8d73a43dec..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System.Collections.Generic; - -namespace Microsoft.AspNet.Routing.Template -{ - /// - /// Represents a URI generated from a . - /// - public class BoundRouteTemplate - { - public string BoundTemplate { get; set; } - - public IDictionary Values { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/ITemplateRouteConstraint.cs b/src/Microsoft.AspNet.Routing/Template/ITemplateRouteConstraint.cs deleted file mode 100644 index 4f70d7306b..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/ITemplateRouteConstraint.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.AspNet.Abstractions; - -namespace Microsoft.AspNet.Routing.Template -{ - public interface ITemplateRouteConstraint - { - bool Match(HttpContext context, IRoute route, string parameterName, IDictionary values, RouteDirection routeDirection); - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/IVirtualPathData.cs b/src/Microsoft.AspNet.Routing/Template/IVirtualPathData.cs deleted file mode 100644 index f7f2cafd19..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/IVirtualPathData.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Routing.Template -{ - public interface IVirtualPathData - { - IRoute Route { get; } - - string VirtualPath { get; set; } - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/RouteDirection.cs b/src/Microsoft.AspNet.Routing/Template/RouteDirection.cs deleted file mode 100644 index 7f9bcc6c33..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/RouteDirection.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Routing.Template -{ - public enum RouteDirection - { - UriResolution = 0, - UriGeneration - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs index 9b65710400..49db3415a7 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs @@ -2,16 +2,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; -using System.Globalization; using System.Linq; -using System.Text; -using System.Text.RegularExpressions; namespace Microsoft.AspNet.Routing.Template { - public sealed class TemplateParsedRoute + public class TemplateParsedRoute { public TemplateParsedRoute(IList pathSegments) { @@ -21,392 +17,6 @@ namespace Microsoft.AspNet.Routing.Template internal IList PathSegments { get; private set; } - [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Not changing original algorithm")] - [SuppressMessage("Microsoft.Maintainability", "CA1505:AvoidUnmaintainableCode", Justification = "Not changing original algorithm")] - public BoundRouteTemplate Bind(IDictionary currentValues, IDictionary values, IDictionary defaultValues, IDictionary constraints) - { - if (currentValues == null) - { - currentValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - if (values == null) - { - values = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - if (defaultValues == null) - { - defaultValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - // The set of values we should be using when generating the URI in this route - IDictionary acceptedValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // Keep track of which new values have been used - HashSet unusedNewValues = new HashSet(values.Keys, StringComparer.OrdinalIgnoreCase); - - // Step 1: Get the list of values we're going to try to use to match and generate this URI - - // Find out which entries in the URI are valid for the URI we want to generate. - // If the URI had ordered parameters a="1", b="2", c="3" and the new values - // specified that b="9", then we need to invalidate everything after it. The new - // values should then be a="1", b="9", c=. - ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) - { - // If it's a parameter subsegment, examine the current value to see if it matches the new value - string parameterName = parameterSubsegment.ParameterName; - - object newParameterValue; - bool hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue); - if (hasNewParameterValue) - { - unusedNewValues.Remove(parameterName); - } - - object currentParameterValue; - bool hasCurrentParameterValue = currentValues.TryGetValue(parameterName, out currentParameterValue); - - if (hasNewParameterValue && hasCurrentParameterValue) - { - if (!RoutePartsEqual(currentParameterValue, newParameterValue)) - { - // Stop copying current values when we find one that doesn't match - return false; - } - } - - // If the parameter is a match, add it to the list of values we will use for URI generation - if (hasNewParameterValue) - { - if (IsRoutePartNonEmpty(newParameterValue)) - { - acceptedValues.Add(parameterName, newParameterValue); - } - } - else - { - if (hasCurrentParameterValue) - { - acceptedValues.Add(parameterName, currentParameterValue); - } - } - return true; - }); - - // Add all remaining new values to the list of values we will use for URI generation - foreach (var newValue in values) - { - if (IsRoutePartNonEmpty(newValue.Value)) - { - if (!acceptedValues.ContainsKey(newValue.Key)) - { - acceptedValues.Add(newValue.Key, newValue.Value); - } - } - } - - // Add all current values that aren't in the URI at all - foreach (var currentValue in currentValues) - { - string parameterName = currentValue.Key; - if (!acceptedValues.ContainsKey(parameterName)) - { - PathParameterSubsegment parameterSubsegment = GetParameterSubsegment(PathSegments, parameterName); - if (parameterSubsegment == null) - { - acceptedValues.Add(parameterName, currentValue.Value); - } - } - } - - // Add all remaining default values from the route to the list of values we will use for URI generation - ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) - { - if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName)) - { - object defaultValue; - if (!IsParameterRequired(parameterSubsegment, defaultValues, out defaultValue)) - { - // Add the default value only if there isn't already a new value for it and - // only if it actually has a default value, which we determine based on whether - // the parameter value is required. - acceptedValues.Add(parameterSubsegment.ParameterName, defaultValue); - } - } - return true; - }); - - // All required parameters in this URI must have values from somewhere (i.e. the accepted values) - bool hasAllRequiredValues = ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) - { - object defaultValue; - if (IsParameterRequired(parameterSubsegment, defaultValues, out defaultValue)) - { - if (!acceptedValues.ContainsKey(parameterSubsegment.ParameterName)) - { - // If the route parameter value is required that means there's - // no default value, so if there wasn't a new value for it - // either, this route won't match. - return false; - } - } - return true; - }); - if (!hasAllRequiredValues) - { - return null; - } - - // All other default values must match if they are explicitly defined in the new values - IDictionary otherDefaultValues = new Dictionary(defaultValues, StringComparer.OrdinalIgnoreCase); - ForEachParameter(PathSegments, delegate(PathParameterSubsegment parameterSubsegment) - { - otherDefaultValues.Remove(parameterSubsegment.ParameterName); - return true; - }); - - foreach (var defaultValue in otherDefaultValues) - { - object value; - if (values.TryGetValue(defaultValue.Key, out value)) - { - unusedNewValues.Remove(defaultValue.Key); - if (!RoutePartsEqual(value, defaultValue.Value)) - { - // If there is a non-parameterized value in the route and there is a - // new value for it and it doesn't match, this route won't match. - return null; - } - } - } - - // Step 2: If the route is a match generate the appropriate URI - - StringBuilder uri = new StringBuilder(); - StringBuilder pendingParts = new StringBuilder(); - - bool pendingPartsAreAllSafe = false; - bool blockAllUriAppends = false; - - for (int i = 0; i < PathSegments.Count; i++) - { - PathSegment pathSegment = PathSegments[i]; // parsedRouteUriPart - - if (pathSegment is PathSeparatorSegment) - { - if (pendingPartsAreAllSafe) - { - // Accept - if (pendingParts.Length > 0) - { - if (blockAllUriAppends) - { - return null; - } - - // Append any pending literals to the URI - uri.Append(pendingParts.ToString()); - pendingParts.Length = 0; - } - } - pendingPartsAreAllSafe = false; - - // Guard against appending multiple separators for empty segments - if (pendingParts.Length > 0 && pendingParts[pendingParts.Length - 1] == '/') - { - // Dev10 676725: Route should not be matched if that causes mismatched tokens - // Dev11 86819: We will allow empty matches if all subsequent segments are null - if (blockAllUriAppends) - { - return null; - } - - // Append any pending literals to the URI (without the trailing slash) and prevent any future appends - uri.Append(pendingParts.ToString(0, pendingParts.Length - 1)); - pendingParts.Length = 0; - blockAllUriAppends = true; - } - else - { - pendingParts.Append("/"); - } - } - else - { - PathContentSegment contentPathSegment = pathSegment as PathContentSegment; - if (contentPathSegment != null) - { - // Segments are treated as all-or-none. We should never output a partial segment. - // If we add any subsegment of this segment to the generated URI, we have to add - // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we - // used a value for {p1}, we have to output the entire segment up to the next "/". - // Otherwise we could end up with the partial segment "v1" instead of the entire - // segment "v1-v2.xml". - bool addedAnySubsegments = false; - - foreach (PathSubsegment subsegment in contentPathSegment.Subsegments) - { - PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment; - if (literalSubsegment != null) - { - // If it's a literal we hold on to it until we are sure we need to add it - pendingPartsAreAllSafe = true; - pendingParts.Append(literalSubsegment.Literal); - } - else - { - PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment; - if (parameterSubsegment != null) - { - if (pendingPartsAreAllSafe) - { - // Accept - if (pendingParts.Length > 0) - { - if (blockAllUriAppends) - { - return null; - } - - // Append any pending literals to the URI - uri.Append(pendingParts.ToString()); - pendingParts.Length = 0; - - addedAnySubsegments = true; - } - } - pendingPartsAreAllSafe = false; - - // If it's a parameter, get its value - object acceptedParameterValue; - bool hasAcceptedParameterValue = acceptedValues.TryGetValue(parameterSubsegment.ParameterName, out acceptedParameterValue); - if (hasAcceptedParameterValue) - { - unusedNewValues.Remove(parameterSubsegment.ParameterName); - } - - object defaultParameterValue; - defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultParameterValue); - - if (RoutePartsEqual(acceptedParameterValue, defaultParameterValue)) - { - // If the accepted value is the same as the default value, mark it as pending since - // we won't necessarily add it to the URI we generate. - pendingParts.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture)); - } - else - { - if (blockAllUriAppends) - { - return null; - } - - // Add the new part to the URI as well as any pending parts - if (pendingParts.Length > 0) - { - // Append any pending literals to the URI - uri.Append(pendingParts.ToString()); - pendingParts.Length = 0; - } - uri.Append(Convert.ToString(acceptedParameterValue, CultureInfo.InvariantCulture)); - - addedAnySubsegments = true; - } - } - else - { - Contract.Assert(false, "Invalid path subsegment type"); - } - } - } - - if (addedAnySubsegments) - { - // See comment above about why we add the pending parts - if (pendingParts.Length > 0) - { - if (blockAllUriAppends) - { - return null; - } - - // Append any pending literals to the URI - uri.Append(pendingParts.ToString()); - pendingParts.Length = 0; - } - } - } - else - { - Contract.Assert(false, "Invalid path segment type"); - } - } - } - - if (pendingPartsAreAllSafe) - { - // Accept - if (pendingParts.Length > 0) - { - if (blockAllUriAppends) - { - return null; - } - - // Append any pending literals to the URI - uri.Append(pendingParts.ToString()); - } - } - - // Process constraints keys - if (constraints != null) - { - // If there are any constraints, mark all the keys as being used so that we don't - // generate query string items for custom constraints that don't appear as parameters - // in the URI format. - foreach (var constraintsItem in constraints) - { - unusedNewValues.Remove(constraintsItem.Key); - } - } - - // Encode the URI before we append the query string, otherwise we would double encode the query string - StringBuilder encodedUri = new StringBuilder(); - encodedUri.Append(UriEncode(uri.ToString())); - uri = encodedUri; - - // Add remaining new values as query string parameters to the URI - if (unusedNewValues.Count > 0) - { - // Generate the query string - bool firstParam = true; - foreach (string unusedNewValue in unusedNewValues) - { - object value; - if (acceptedValues.TryGetValue(unusedNewValue, out value)) - { - uri.Append(firstParam ? '?' : '&'); - firstParam = false; - uri.Append(Uri.EscapeDataString(unusedNewValue)); - uri.Append('='); - uri.Append(Uri.EscapeDataString(Convert.ToString(value, CultureInfo.InvariantCulture))); - } - } - } - - return new BoundRouteTemplate - { - BoundTemplate = uri.ToString(), - Values = acceptedValues - }; - } - - private static string EscapeReservedCharacters(Match m) - { - return "%" + Convert.ToUInt16(m.Value[0]).ToString("x2", CultureInfo.InvariantCulture); - } - private static bool ForEachParameter(IList pathSegments, Func action) { for (int i = 0; i < pathSegments.Count; i++) @@ -458,47 +68,6 @@ namespace Microsoft.AspNet.Routing.Template return true; } - private static PathParameterSubsegment GetParameterSubsegment(IList pathSegments, string parameterName) - { - PathParameterSubsegment foundParameterSubsegment = null; - - ForEachParameter(pathSegments, delegate(PathParameterSubsegment parameterSubsegment) - { - if (String.Equals(parameterName, parameterSubsegment.ParameterName, StringComparison.OrdinalIgnoreCase)) - { - foundParameterSubsegment = parameterSubsegment; - return false; - } - else - { - return true; - } - }); - - return foundParameterSubsegment; - } - - private static bool IsParameterRequired(PathParameterSubsegment parameterSubsegment, IDictionary defaultValues, out object defaultValue) - { - if (parameterSubsegment.IsCatchAll) - { - defaultValue = null; - return false; - } - - return !defaultValues.TryGetValue(parameterSubsegment.ParameterName, out defaultValue); - } - - private static bool IsRoutePartNonEmpty(object routePart) - { - string routePartString = routePart as string; - if (routePartString != null) - { - return routePartString.Length > 0; - } - return routePart != null; - } - public IDictionary Match(string virtualPath, IDictionary defaultValues) { IList requestPathSegments = TemplateRouteParser.SplitUriToPathSegmentStrings(virtualPath); @@ -808,35 +377,5 @@ namespace Microsoft.AspNet.Routing.Template return true; } } - - private static bool RoutePartsEqual(object a, object b) - { - string sa = a as string; - string sb = b as string; - if (sa != null && sb != null) - { - // For strings do a case-insensitive comparison - return String.Equals(sa, sb, StringComparison.OrdinalIgnoreCase); - } - else - { - if (a != null && b != null) - { - // Explicitly call .Equals() in case it is overridden in the type - return a.Equals(b); - } - else - { - // At least one of them is null. Return true if they both are - return a == b; - } - } - } - - private static string UriEncode(string str) - { - string escape = Uri.EscapeUriString(str); - return Regex.Replace(escape, "([#?])", new MatchEvaluator(EscapeReservedCharacters)); - } } } diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index c0ab8dcb44..76f4a68b66 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -2,68 +2,34 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; -using Microsoft.AspNet.Abstractions; namespace Microsoft.AspNet.Routing.Template { - /// - /// Route class for self-host (i.e. hosted outside of ASP.NET). This class is mostly the - /// same as the System.Web.Routing.Route implementation. - /// This class has the same URL matching functionality as System.Web.Routing.Route. However, - /// in order for this route to match when generating URLs, a special "httproute" key must be - /// specified when generating the URL. - /// public class TemplateRoute : IRoute { - /// - /// Key used to signify that a route URL generation request should include HTTP routes (e.g. Web API). - /// If this key is not specified then no HTTP routes will match. - /// - public static readonly string HttpRouteKey = "httproute"; + private readonly IDictionary _defaults; + private readonly IRouteEndpoint _endpoint; + private readonly TemplateParsedRoute _parsedRoute; + private readonly string _routeTemplate; - private string _routeTemplate; - private IDictionary _defaults; - private IDictionary _constraints; - private IDictionary _dataTokens; - - public TemplateRoute() - : this(routeTemplate: null, defaults: null, constraints: null, dataTokens: null, handler: null) + public TemplateRoute(IRouteEndpoint endpoint, string routeTemplate) + : this(endpoint, routeTemplate, null) { } - public TemplateRoute(string routeTemplate) - : this(routeTemplate, defaults: null, constraints: null, dataTokens: null, handler: null) + public TemplateRoute(IRouteEndpoint endpoint, string routeTemplate, IDictionary defaults) { - } + if (endpoint == null) + { + throw new ArgumentNullException("endpoint"); + } - public TemplateRoute(string routeTemplate, IDictionary defaults) - : this(routeTemplate, defaults, constraints: null, dataTokens: null, handler: null) - { - } - - public TemplateRoute(string routeTemplate, IDictionary defaults, IDictionary constraints) - : this(routeTemplate, defaults, constraints, dataTokens: null, handler: null) - { - } - - public TemplateRoute(string routeTemplate, IDictionary defaults, IDictionary constraints, IDictionary dataTokens) - : this(routeTemplate, defaults, constraints, dataTokens, handler: null) - { - } - - public TemplateRoute(string routeTemplate, IDictionary defaults, IDictionary constraints, IDictionary dataTokens, IRouteEndpoint handler) - { + _endpoint = endpoint; _routeTemplate = routeTemplate == null ? String.Empty : routeTemplate; _defaults = defaults ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - _constraints = constraints ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - _dataTokens = dataTokens ?? new Dictionary(StringComparer.OrdinalIgnoreCase); - Handler = handler; // The parser will throw for invalid routes. - ParsedRoute = TemplateRouteParser.Parse(RouteTemplate); + _parsedRoute = TemplateRouteParser.Parse(RouteTemplate); } public IDictionary Defaults @@ -71,159 +37,39 @@ namespace Microsoft.AspNet.Routing.Template get { return _defaults; } } - public IDictionary Constraints + public IRouteEndpoint Endpoint { - get { return _constraints; } + get { return _endpoint; } } - public IDictionary DataTokens - { - get { return _dataTokens; } - } - - public IRouteEndpoint Handler { get; private set; } - public string RouteTemplate { get { return _routeTemplate; } } - internal TemplateParsedRoute ParsedRoute { get; private set; } - - public virtual RouteMatch GetRouteData(HttpContext request) + public virtual RouteMatch Match(RouteContext context) { - if (request == null) + if (context == null) { - throw new ArgumentNullException("request"); + throw new ArgumentNullException("context"); } - var requestPath = request.Request.Path.Value; + var requestPath = context.RequestPath; if (!String.IsNullOrEmpty(requestPath) && requestPath[0] == '/') { requestPath = requestPath.Substring(1); } - IDictionary values = ParsedRoute.Match(requestPath, _defaults); + IDictionary values = _parsedRoute.Match(requestPath, _defaults); if (values == null) { // If we got back a null value set, that means the URI did not match return null; } - - // Validate the values - if (!ProcessConstraints(request, values, RouteDirection.UriResolution)) + else { - return null; + return new RouteMatch(_endpoint, values); } - - return new RouteMatch(null, values); - } - - /// - /// Attempt to generate a URI that represents the values passed in based on current - /// values from the and new values using the specified . - /// - /// The HTTP request message. - /// The route values. - /// A instance or null if URI cannot be generated. - public virtual IVirtualPathData GetVirtualPath(HttpContext request, IDictionary values) - { - if (request == null) - { - throw new ArgumentNullException("request"); - } - - // Only perform URL generation if the "httproute" key was specified. This allows these - // routes to be ignored when a regular MVC app tries to generate URLs. Without this special - // key an HTTP route used for Web API would normally take over almost all the routes in a - // typical app. - if (values != null && !values.Keys.Contains(HttpRouteKey, StringComparer.OrdinalIgnoreCase)) - { - return null; - } - // Remove the value from the collection so that it doesn't affect the generated URL - var newValues = GetRouteDictionaryWithoutHttpRouteKey(values); - - IRouteValues routeData = request.GetFeature(); - IDictionary requestValues = routeData == null ? null : routeData.Values; - - BoundRouteTemplate result = ParsedRoute.Bind(requestValues, newValues, _defaults, _constraints); - if (result == null) - { - return null; - } - - // Assert that the route matches the validation rules - if (!ProcessConstraints(request, result.Values, RouteDirection.UriGeneration)) - { - return null; - } - - return new VirtualPathData(this, result.BoundTemplate); - } - - private static IDictionary GetRouteDictionaryWithoutHttpRouteKey(IDictionary routeValues) - { - var newRouteValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (routeValues != null) - { - foreach (var routeValue in routeValues) - { - if (!String.Equals(routeValue.Key, HttpRouteKey, StringComparison.OrdinalIgnoreCase)) - { - newRouteValues.Add(routeValue.Key, routeValue.Value); - } - } - } - return newRouteValues; - } - - protected virtual bool ProcessConstraint(HttpContext request, object constraint, string parameterName, IDictionary values, RouteDirection routeDirection) - { - ITemplateRouteConstraint customConstraint = constraint as ITemplateRouteConstraint; - if (customConstraint != null) - { - return customConstraint.Match(request, this, parameterName, values, routeDirection); - } - - // If there was no custom constraint, then treat the constraint as a string which represents a Regex. - string constraintsRule = constraint as string; - if (constraintsRule == null) - { - throw new InvalidOperationException(String.Format( - CultureInfo.CurrentCulture, - Resources.TemplateRoute_ValidationMustBeStringOrCustomConstraint, - parameterName, - RouteTemplate, - typeof(ITemplateRouteConstraint).Name)); - } - - object parameterValue; - values.TryGetValue(parameterName, out parameterValue); - string parameterValueString = Convert.ToString(parameterValue, CultureInfo.InvariantCulture); - string constraintsRegEx = "^(" + constraintsRule + ")$"; - return Regex.IsMatch(parameterValueString, constraintsRegEx, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - } - - private bool ProcessConstraints(HttpContext request, IDictionary values, RouteDirection routeDirection) - { - if (Constraints != null) - { - foreach (KeyValuePair constraintsItem in Constraints) - { - if (!ProcessConstraint(request, constraintsItem.Value, constraintsItem.Key, values, routeDirection)) - { - return false; - } - } - } - - return true; - } - - public RouteMatch Match(RouteContext context) - { - throw new NotImplementedException(); } } } diff --git a/src/Microsoft.AspNet.Routing/Template/VirtualPathData.cs b/src/Microsoft.AspNet.Routing/Template/VirtualPathData.cs deleted file mode 100644 index e6ec047dcd..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/VirtualPathData.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -namespace Microsoft.AspNet.Routing.Template -{ - public class VirtualPathData : IVirtualPathData - { - private string _virtualPath; - - public VirtualPathData(IRoute route, string virtualPath) - { - if (route == null) - { - throw new ArgumentNullException("route"); - } - - if (virtualPath == null) - { - throw new ArgumentNullException("virtualPath"); - } - - Route = route; - VirtualPath = virtualPath; - } - - public IRoute Route { get; private set; } - - public string VirtualPath - { - get { return _virtualPath; } - set - { - if (value == null) - { - throw new ArgumentNullException("value"); - } - - _virtualPath = value; - } - } - } -} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs index c8e6c20211..5c52254d1a 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNet.Abstractions; using Xunit; @@ -12,24 +12,6 @@ namespace Microsoft.AspNet.Routing.Template.Tests { public class TemplateRouteTests { - [Fact] - public void GetRouteDataWithConstraintsThatIsNotStringThrows() - { - // Arrange - HttpContext context = GetHttpContext("~/category/33"); - TemplateRoute r = CreateRoute( - "category/{category}", - new RouteValueDictionary(new { controller = "store", action = "showcat" }), - new RouteValueDictionary(new { category = 5 }), - null); - - // Act - Assert.Throws(() => r.GetRouteData(context), - "The constraint entry 'category' on the route with route template 'category/{category}' must have a string value or " + - "be of a type which implements 'ITemplateRouteConstraint'."); - } - - [Fact] public void MatchSingleRoute() { @@ -38,7 +20,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("{controller}/{action}/{id}", null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.Equal("Bank", rd.Values["controller"]); @@ -54,7 +36,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("{controller}/{action}/{id}", null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.Null(rd); @@ -65,10 +47,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests { // Arrange HttpContext context = GetHttpContext("~/Bank/DoAction"); - TemplateRoute r = CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { id = "default id" }), null); + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { id = "default id" })); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.Equal("Bank", rd.Values["controller"]); @@ -76,52 +58,15 @@ namespace Microsoft.AspNet.Routing.Template.Tests Assert.Equal("default id", rd.Values["id"]); } -#if URLGENERATION - - [Fact] - public void MatchSingleRouteWithEmptyDefaults() - { - IHttpVirtualPathData data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}", new RouteValueDictionary(new { val1 = "", val2 = "" }), new RouteValueDictionary(new { val2 = "SomeVal2" })); - Assert.Null(data); - - data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}", new RouteValueDictionary(new { val1 = "", val2 = "" }), new RouteValueDictionary(new { val1 = "a" })); - Assert.Equal("Test/a", data.VirtualPath); - - data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "", val3 = "" }), new RouteValueDictionary(new { val2 = "a" })); - Assert.Null(data); - - data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}", new RouteValueDictionary(new { val1 = "", val2 = "" }), new RouteValueDictionary(new { val1 = "a", val2 = "b" })); - Assert.Equal("Test/a/b", data.VirtualPath); - - data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "", val2 = "", val3 = "" }), new RouteValueDictionary(new { val1 = "a", val2 = "b", val3 = "c" })); - Assert.Equal("Test/a/b/c", data.VirtualPath); - - data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "", val2 = "", val3 = "" }), new RouteValueDictionary(new { val1 = "a", val2 = "b" })); - Assert.Equal("Test/a/b", data.VirtualPath); - - data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "", val2 = "", val3 = "" }), new RouteValueDictionary(new { val1 = "a" })); - Assert.Equal("Test/a", data.VirtualPath); - - } - - private IHttpVirtualPathData GetVirtualPathFromRoute(string path, string template, RouteValueDictionary defaults, RouteValueDictionary values) - { - TemplateRoute r = CreateRoute(template, defaults, null); - - HttpContext context = GetHttpContext(path); - return r.GetVirtualPath(context, values); - } -#endif - [Fact] public void NoMatchSingleRouteWithDefaults() { // Arrange HttpContext context = GetHttpContext("~/Bank"); - TemplateRoute r = CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { id = "default id" }), null); + TemplateRoute r = CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { id = "default id" })); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.Null(rd); @@ -132,10 +77,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests { // Arrange HttpContext context = GetHttpContext("~/moo/111/bar/222"); - TemplateRoute r = CreateRoute("moo/{p1}/bar/{p2}", new RouteValueDictionary(new { p2 = "default p2" }), null); + TemplateRoute r = CreateRoute("moo/{p1}/bar/{p2}", new RouteValueDictionary(new { p2 = "default p2" })); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.Equal("111", rd.Values["p1"]); @@ -147,10 +92,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests { // Arrange HttpContext context = GetHttpContext("~/moo/111/bar/"); - TemplateRoute r = CreateRoute("moo/{p1}/bar/{p2}", new RouteValueDictionary(new { p2 = "default p2" }), null); + TemplateRoute r = CreateRoute("moo/{p1}/bar/{p2}", new RouteValueDictionary(new { p2 = "default p2" })); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.Equal("111", rd.Values["p1"]); @@ -165,7 +110,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("moo/bar", null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.NotNull(rd); @@ -180,7 +125,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("moo/bars", null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.Null(rd); @@ -194,7 +139,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("moo/bar", null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.NotNull(rd); @@ -209,7 +154,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("moo/bar/", null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.NotNull(rd); @@ -224,7 +169,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("{p1}/{p2}/", null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.NotNull(rd); @@ -240,7 +185,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("{p1}/{p2}/baz", null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.Null(rd); @@ -254,7 +199,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("{p1}", null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.Null(rd); @@ -268,7 +213,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("DEFAULT.ASPX", null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.NotNull(rd); @@ -280,7 +225,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute(route, null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.NotNull(rd); @@ -304,10 +249,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests { // Arrange HttpContext context = GetHttpContext("~/v1"); - TemplateRoute r = CreateRoute("{p1}/{p2}", new RouteValueDictionary(new { p2 = (string)null, foo = "bar" }), null); + TemplateRoute r = CreateRoute("{p1}/{p2}", new RouteValueDictionary(new { p2 = (string)null, foo = "bar" })); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.NotNull(rd); @@ -324,11 +269,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests HttpContext context = GetHttpContext("~/date/2007/08"); TemplateRoute r = CreateRoute( "date/{y}/{m}/{d}", - new RouteValueDictionary(new { controller = "blog", action = "showpost", m = (string)null, d = (string)null }), - null); + new RouteValueDictionary(new { controller = "blog", action = "showpost", m = (string)null, d = (string)null })); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.NotNull(rd); @@ -340,114 +284,6 @@ namespace Microsoft.AspNet.Routing.Template.Tests Assert.Null(rd.Values["d"]); } - [Fact] - public void GetRouteDataWhenConstraintsMatchesExactlyReturnsMatch() - { - // Arrange - HttpContext context = GetHttpContext("~/category/12"); - TemplateRoute r = CreateRoute( - "category/{category}", - new RouteValueDictionary(new { controller = "store", action = "showcat" }), - new RouteValueDictionary(new { category = @"\d\d" }), - null); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.NotNull(rd); - Assert.Equal(3, rd.Values.Count); - Assert.Equal("store", rd.Values["controller"]); - Assert.Equal("showcat", rd.Values["action"]); - Assert.Equal("12", rd.Values["category"]); - } - - [Fact] - public void GetRouteDataShouldApplyRegExModifiersCorrectly1() - { - // DevDiv Bugs 173408: UrlRouting: Route validation doesn't handle ^ and $ correctly - - // Arrange - HttpContext context = GetHttpContext("~/category/FooBar"); - TemplateRoute r = CreateRoute( - "category/{category}", - new RouteValueDictionary(new { controller = "store", action = "showcat" }), - new RouteValueDictionary(new { category = @"Foo|Bar" }), - null); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.Null(rd); - } - - [Fact] - public void GetRouteDataShouldApplyRegExModifiersCorrectly2() - { - // DevDiv Bugs 173408: UrlRouting: Route validation doesn't handle ^ and $ correctly - - // Arrange - HttpContext context = GetHttpContext("~/category/Food"); - TemplateRoute r = CreateRoute( - "category/{category}", - new RouteValueDictionary(new { controller = "store", action = "showcat" }), - new RouteValueDictionary(new { category = @"Foo|Bar" }), - null); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.Null(rd); - } - - [Fact] - public void GetRouteDataShouldApplyRegExModifiersCorrectly3() - { - // DevDiv Bugs 173408: UrlRouting: Route validation doesn't handle ^ and $ correctly - - // Arrange - HttpContext context = GetHttpContext("~/category/Bar"); - TemplateRoute r = CreateRoute( - "category/{category}", - new RouteValueDictionary(new { controller = "store", action = "showcat" }), - new RouteValueDictionary(new { category = @"Foo|Bar" }), - null); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.NotNull(rd); - Assert.Equal(3, rd.Values.Count); - Assert.Equal("store", rd.Values["controller"]); - Assert.Equal("showcat", rd.Values["action"]); - Assert.Equal("Bar", rd.Values["category"]); - } - - [Fact] - public void GetRouteDataWithCaseInsensitiveConstraintsMatches() - { - // Arrange - HttpContext context = GetHttpContext("~/category/aBc"); - TemplateRoute r = CreateRoute( - "category/{category}", - new RouteValueDictionary(new { controller = "store", action = "showcat" }), - new RouteValueDictionary(new { category = @"[a-z]{3}" }), - null); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.NotNull(rd); - Assert.Equal(3, rd.Values.Count); - Assert.Equal("store", rd.Values["controller"]); - Assert.Equal("showcat", rd.Values["action"]); - Assert.Equal("aBc", rd.Values["category"]); - } - [Fact] public void GetRouteDataWithMultiSegmentParamsOnBothEndsMatches() { @@ -569,135 +405,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests public void GetRouteDataWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() { GetRouteDataHelper( - CreateRoute("language/{lang}-{region}", new RouteValueDictionary(new { lang = "xx", region = "yy" }), null), - "language/-", + CreateRoute("language/{lang}-{region}", new RouteValueDictionary(new { lang = "xx", region = "yy" })), + "language/-", null); } -#if URLGENERATION - - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnBothEndsMatches() - { - GetVirtualPathHelper( - CreateRoute("language/{lang}-{region}", null), - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "xx", region = "yy" }), - "language/xx-yy"); - } - - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnLeftEndMatches() - { - GetVirtualPathHelper( - CreateRoute("language/{lang}-{region}a", null), - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "xx", region = "yy" }), - "language/xx-yya"); - } - - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnRightEndMatches() - { - GetVirtualPathHelper( - CreateRoute("language/a{lang}-{region}", null), - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "xx", region = "yy" }), - "language/axx-yy"); - } - - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndMatches() - { - GetVirtualPathHelper( - CreateRoute("language/a{lang}-{region}a", null), - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "xx", region = "yy" }), - "language/axx-yya"); - } - - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch() - { - GetVirtualPathHelper( - CreateRoute("language/a{lang}-{region}a", null), - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "", region = "yy" }), - null); - } - - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch2() - { - GetVirtualPathHelper( - CreateRoute("language/a{lang}-{region}a", null), - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "xx", region = "" }), - null); - } - - [Fact] - public void GetVirtualPathWithSimpleMultiSegmentParamsOnBothEndsMatches() - { - GetVirtualPathHelper( - CreateRoute("language/{lang}", null), - new RouteValueDictionary(new { lang = "en" }), - new RouteValueDictionary(new { lang = "xx" }), - "language/xx"); - } - - [Fact] - public void GetVirtualPathWithSimpleMultiSegmentParamsOnLeftEndMatches() - { - GetVirtualPathHelper( - CreateRoute("language/{lang}-", null), - new RouteValueDictionary(new { lang = "en" }), - new RouteValueDictionary(new { lang = "xx" }), - "language/xx-"); - } - - [Fact] - public void GetVirtualPathWithSimpleMultiSegmentParamsOnRightEndMatches() - { - GetVirtualPathHelper( - CreateRoute("language/a{lang}", null), - new RouteValueDictionary(new { lang = "en" }), - new RouteValueDictionary(new { lang = "xx" }), - "language/axx"); - } - - [Fact] - public void GetVirtualPathWithSimpleMultiSegmentParamsOnNeitherEndMatches() - { - GetVirtualPathHelper( - CreateRoute("language/a{lang}a", null), - new RouteValueDictionary(new { lang = "en" }), - new RouteValueDictionary(new { lang = "xx" }), - "language/axxa"); - } - - [Fact] - public void GetVirtualPathWithMultiSegmentStandardMvcRouteMatches() - { - GetVirtualPathHelper( - CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null })), - new RouteValueDictionary(new { controller = "home", action = "list", id = (string)null }), - new RouteValueDictionary(new { controller = "products" }), - "products.mvc"); - } - - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() - { - GetVirtualPathHelper( - CreateRoute("language/{lang}-{region}", new RouteValueDictionary(new { lang = "xx", region = "yy" }), null), - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "zz" }), - "language/zz-yy"); - } - -#endif - [Fact] public void GetRouteDataWithUrlWithMultiSegmentWithRepeatedDots() { @@ -810,698 +522,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests action = "ShowPilot", missionId = (string)null, name = (string)null - }), - null), + })), "Home/ShowPilot/777/12345./foobar", new RouteValueDictionary(new { controller = "Home", action = "ShowPilot", missionId = "777", name = "12345./foobar" })); } - [Fact] - public void GetRouteDataWhenConstraintsMatchesPartiallyDoesNotMatch() - { - // Arrange - HttpContext context = GetHttpContext("~/category/a12"); - TemplateRoute r = CreateRoute( - "category/{category}", - new RouteValueDictionary(new { controller = "store", action = "showcat" }), - new RouteValueDictionary(new { category = @"\d\d" }), - null); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.Null(rd); - } - - [Fact] - public void GetRouteDataWhenConstraintsDoesNotMatch() - { - // Arrange - HttpContext context = GetHttpContext("~/category/ab"); - TemplateRoute r = CreateRoute( - "category/{category}", - new RouteValueDictionary(new { controller = "store", action = "showcat" }), - new RouteValueDictionary(new { category = @"\d\d" }), - null); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.Null(rd); - } - - [Fact] - public void GetRouteDataWhenOneOfMultipleConstraintsDoesNotMatch() - { - // Arrange - HttpContext context = GetHttpContext("~/category/01/q"); - TemplateRoute r = CreateRoute( - "category/{category}/{sort}", - new RouteValueDictionary(new { controller = "store", action = "showcat" }), - new RouteValueDictionary(new { category = @"\d\d", sort = @"a|d" }), - null); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.Null(rd); - } - - [Fact] - public void GetRouteDataWithNonStringValueReturnsTrueIfMatches() - { - // Arrange - HttpContext context = GetHttpContext("~/category"); - TemplateRoute r = CreateRoute( - "category/{foo}", - new RouteValueDictionary(new { controller = "store", action = "showcat", foo = 123 }), - new RouteValueDictionary(new { foo = @"\d{3}" })); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.NotNull(rd); - } - - [Fact] - public void GetRouteDataWithNonStringValueReturnsFalseIfUnmatched() - { - // Arrange - HttpContext context = GetHttpContext("~/category"); - TemplateRoute r = CreateRoute( - "category/{foo}", - new RouteValueDictionary(new { controller = "store", action = "showcat", foo = 123 }), - new RouteValueDictionary(new { foo = @"\d{2}" })); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.Null(rd); - } - -#if URLGENERATION - [Fact] - public void GetUrlWithDefaultValue() - { - // URL should be found but excluding the 'id' parameter, which has only a default value. - GetVirtualPathHelper( - CreateRoute("{controller}/{action}/{id}", - new RouteValueDictionary(new { id = "defaultid" }), null), - new RouteValueDictionary(new { controller = "home", action = "oldaction" }), - new RouteValueDictionary(new { action = "newaction" }), - "home/newaction"); - } - - [Fact] - public void GetVirtualPathWithEmptyStringRequiredValueReturnsNull() - { - GetVirtualPathHelper( - CreateRoute("foo/{controller}", null), - new RouteValueDictionary(new { }), - new RouteValueDictionary(new { controller = "" }), - null); - } - - [Fact] - public void GetVirtualPathWithNullRequiredValueReturnsNull() - { - GetVirtualPathHelper( - CreateRoute("foo/{controller}", null), - new RouteValueDictionary(new { }), - new RouteValueDictionary(new { controller = (string)null }), - null); - } - - [Fact] - public void GetVirtualPathWithRequiredValueReturnsPath() - { - GetVirtualPathHelper( - CreateRoute("foo/{controller}", null), - new RouteValueDictionary(new { }), - new RouteValueDictionary(new { controller = "home" }), - "foo/home"); - } - - [Fact] - public void GetUrlWithNullDefaultValue() - { - // URL should be found but excluding the 'id' parameter, which has only a default value. - GetVirtualPathHelper( - CreateRoute( - "{controller}/{action}/{id}", - new RouteValueDictionary(new { id = (string)null }), - null), - new RouteValueDictionary(new { controller = "home", action = "oldaction", id = (string)null }), - new RouteValueDictionary(new { action = "newaction" }), - "home/newaction"); - } - - [Fact] - public void GetUrlWithMissingValuesDoesntMatch() - { - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - TemplateRoute r = CreateRoute("{controller}/{action}/{id}", null); - - var rd = CreateRouteData(); - rd.Values.Add("controller", "home"); - rd.Values.Add("action", "oldaction"); - var valuesDictionary = CreateRouteValueDictionary(); - valuesDictionary.Add("action", "newaction"); - - // Act - var vpd = r.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.Null(vpd); - } - - [Fact] - public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrenIRoute() - { - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - IRouteCollection rt = new DefaultRouteCollection(); - rt.Add(CreateRoute("date/{y}/{m}/{d}", null)); - rt.Add(CreateRoute("{controller}/{action}/{id}", null)); - - var rd = CreateRouteData(); - rd.Values.Add("controller", "home"); - rd.Values.Add("action", "dostuff"); - - var values = CreateRouteValueDictionary(); - values.Add("y", "2007"); - values.Add("m", "08"); - values.Add("d", "12"); - - // Act - var vpd = rt.GetVirtualPath(context, values); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("/app/date/2007/08/12", vpd.VirtualPath); - } - - [Fact] - public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrentRouteAsSecondRoute() - { - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - - IRouteCollection rt = new DefaultRouteCollection(); - rt.Add(CreateRoute("{controller}/{action}/{id}")); - rt.Add(CreateRoute("date/{y}/{m}/{d}")); - - var rd = CreateRouteData(); - rd.Values.Add("controller", "home"); - rd.Values.Add("action", "dostuff"); - - var values = CreateRouteValueDictionary(); - values.Add("y", "2007"); - values.Add("m", "08"); - values.Add("d", "12"); - - // Act - var vpd = rt.GetVirtualPath(context, values); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("/app/date/2007/08/12", vpd.VirtualPath); - } - - [Fact] - public void GetUrlWithEmptyRequiredValuesReturnsNull() - { - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - TemplateRoute r = CreateRoute("{p1}/{p2}/{p3}", new RouteValueDictionary(), null); - - var rd = CreateRouteData(); - rd.Values.Add("p1", "v1"); - - var valuesDictionary = CreateRouteValueDictionary(); - valuesDictionary.Add("p2", ""); - valuesDictionary.Add("p3", ""); - - // Act - var vpd = r.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.Null(vpd); - } - - [Fact] - public void GetUrlWithEmptyOptionalValuesReturnsShortUrl() - { - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - TemplateRoute r = CreateRoute("{p1}/{p2}/{p3}", new RouteValueDictionary(new { p2 = "d2", p3 = "d3", }), null); - - var rd = CreateRouteData(); - rd.Values.Add("p1", "v1"); - var valuesDictionary = CreateRouteValueDictionary(); - valuesDictionary.Add("p2", ""); - valuesDictionary.Add("p3", ""); - - // Act - var vpd = r.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("v1", vpd.VirtualPath); - } - - [Fact] - public void GetUrlShouldIgnoreValuesAfterChangedParameter() - { - // DevDiv Bugs 157535 - - // Arrange - var rd = CreateRouteData(); - rd.Values.Add("controller", "orig"); - rd.Values.Add("action", "init"); - rd.Values.Add("id", "123"); - - TemplateRoute r = CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }), null); - - var valuesDictionary = CreateRouteValueDictionary(); - valuesDictionary.Add("action", "new"); - - // Act - var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), valuesDictionary); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("orig/new", vpd.VirtualPath); - } - - [Fact] - public void GetUrlWithRouteThatHasExtensionWithSubsequentDefaultValueIncludesExtensionButNotDefaultValue() - { - // DevDiv Bugs 156606 - - // Arrange - var rd = CreateRouteData(); - rd.Values.Add("controller", "Bank"); - rd.Values.Add("action", "MakeDeposit"); - rd.Values.Add("accountId", "7770"); - - IRouteCollection rc = new DefaultRouteCollection(); - rc.Add(CreateRoute( - "{controller}.mvc/Deposit/{accountId}", - new RouteValueDictionary(new { Action = "DepositView" }))); - - // Note: This route was in the original bug, but it turns out that this behavior is incorrect. With the - // recent fix to Route (in this changelist) this route would have been selected since we have values for - // all three required parameters. - //rc.Add(new Route { - // Url = "{controller}.mvc/{action}/{accountId}", - // RouteHandler = new DummyRouteHandler() - //}); - - // This route should be chosen because the requested action is List. Since the default value of the action - // is List then the Action should not be in the URL. However, the file extension should be included since - // it is considered "safe." - rc.Add(CreateRoute( - "{controller}.mvc/{action}", - new RouteValueDictionary(new { Action = "List" }))); - - var values = CreateRouteValueDictionary(); - values.Add("Action", "List"); - - // Act - var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("/app1/Bank.mvc", vpd.VirtualPath); - } - - [Fact] - public void GetUrlWithRouteThatHasDifferentControllerCaseShouldStillMatch() - { - // DevDiv Bugs 159099 - - // Arrange - var rd = CreateRouteData(); - rd.Values.Add("controller", "Bar"); - rd.Values.Add("action", "bbb"); - rd.Values.Add("id", null); - - IRouteCollection rc = new DefaultRouteCollection(); - rc.Add(CreateRoute("PrettyFooUrl", new RouteValueDictionary(new { controller = "Foo", action = "aaa", id = (string)null }))); - - rc.Add(CreateRoute("PrettyBarUrl", new RouteValueDictionary(new { controller = "Bar", action = "bbb", id = (string)null }))); - - rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); - - var values = CreateRouteValueDictionary(); - values.Add("Action", "aaa"); - values.Add("Controller", "foo"); - - // Act - var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("/app1/PrettyFooUrl", vpd.VirtualPath); - } - - [Fact] - public void GetUrlWithNoChangedValuesShouldProduceSameUrl() - { - // DevDiv Bugs 159469 - - // Arrange - var rd = CreateRouteData(); - rd.Values.Add("controller", "Home"); - rd.Values.Add("action", "Index"); - rd.Values.Add("id", null); - - IRouteCollection rc = new DefaultRouteCollection(); - rc.Add(CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); - - rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); - - var values = CreateRouteValueDictionary(); - values.Add("Action", "Index"); - - // Act - var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("/app1/Home.mvc", vpd.VirtualPath); - } - - [Fact] - public void GetUrlAppliesConstraintsRulesToChooseRoute() - { - // DevDiv Bugs 159678: MVC: URL generation chooses the wrong route for generating URLs when route validation is in place - - // Arrange - var rd = CreateRouteData(); - rd.Values.Add("controller", "Home"); - rd.Values.Add("action", "Index"); - rd.Values.Add("id", null); - - IRouteCollection rc = new DefaultRouteCollection(); - rc.Add(CreateRoute( - "foo.mvc/{action}", - new RouteValueDictionary(new { controller = "Home" }), - new RouteValueDictionary(new { controller = "Home", action = "Contact", httpMethod = CreateHttpMethodConstraint("get") }))); - - rc.Add(CreateRoute( - "{controller}.mvc/{action}", - new RouteValueDictionary(new { action = "Index" }), - new RouteValueDictionary(new { controller = "Home", action = "(Index|About)", httpMethod = CreateHttpMethodConstraint("post") }))); - - var values = CreateRouteValueDictionary(); - values.Add("Action", "Index"); - - // Act - var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("/app1/Home.mvc", vpd.VirtualPath); - } - - [Fact] - public void GetUrlWithNullForMiddleParameterIgnoresRemainingParameters() - { - // DevDiv Bugs 170859: UrlRouting: Passing null or empty string for a parameter in the middle of a route generates the wrong Url - - // Arrange - var rd = CreateRouteData(); - rd.Values.Add("controller", "UrlRouting"); - rd.Values.Add("action", "Play"); - rd.Values.Add("category", "Photos"); - rd.Values.Add("year", "2008"); - rd.Values.Add("occasion", "Easter"); - rd.Values.Add("SafeParam", "SafeParamValue"); - - TemplateRoute r = CreateRoute( - "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", - new RouteValueDictionary(new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" })); - - // Act - RouteValueDictionary values = CreateRouteValueDictionary(); - values.Add("year", null); - values.Add("occasion", "Hola"); - var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("UrlGeneration1/UrlRouting.mvc/Play/Photos/1995/Hola", vpd.VirtualPath); - } - - [Fact] - public void GetUrlShouldValidateOnlyAcceptedParametersAndUserDefaultValuesForInvalidatedParameters() - { - // DevDiv Bugs 172913: UrlRouting: Parameter validation should not run against current request values if a new value has been supplied at a previous position - - // Arrange - var rd = CreateRouteData(); - rd.Values.Add("Controller", "UrlRouting"); - rd.Values.Add("Name", "MissmatchedValidateParams"); - rd.Values.Add("action", "MissmatchedValidateParameters2"); - rd.Values.Add("ValidateParam1", "special1"); - rd.Values.Add("ValidateParam2", "special2"); - - IRouteCollection rc = new DefaultRouteCollection(); - rc.Add(CreateRoute( - "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}", - new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams", ValidateParam2 = "valid" }), - new RouteValueDictionary(new { ValidateParam1 = "valid.*", ValidateParam2 = "valid.*" }))); - - rc.Add(CreateRoute( - "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}", - new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams" }), - new RouteValueDictionary(new { ValidateParam1 = "special.*", ValidateParam2 = "special.*" }))); - - var values = CreateRouteValueDictionary(); - values.Add("Name", "MissmatchedValidateParams"); - values.Add("ValidateParam1", "valid1"); - - // Act - var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("/app1/UrlConstraints/Validation.mvc/Input5/MissmatchedValidateParameters2/valid1", vpd.VirtualPath); - } - - [Fact] - public void GetUrlWithEmptyStringForMiddleParameterIgnoresRemainingParameters() - { - // DevDiv Bugs 170859: UrlRouting: Passing null or empty string for a parameter in the middle of a route generates the wrong Url - - // Arrange - var rd = CreateRouteData(); - rd.Values.Add("controller", "UrlRouting"); - rd.Values.Add("action", "Play"); - rd.Values.Add("category", "Photos"); - rd.Values.Add("year", "2008"); - rd.Values.Add("occasion", "Easter"); - rd.Values.Add("SafeParam", "SafeParamValue"); - - TemplateRoute r = CreateRoute( - "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", - new RouteValueDictionary(new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" })); - - // Act - RouteValueDictionary values = CreateRouteValueDictionary(); - values.Add("year", String.Empty); - values.Add("occasion", "Hola"); - var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("UrlGeneration1/UrlRouting.mvc/Play/Photos/1995/Hola", vpd.VirtualPath); - } - - [Fact] - public void GetUrlWithEmptyStringForMiddleParameterShouldUseDefaultValue() - { - // DevDiv Bugs 172084: UrlRouting: Route.GetUrl generates the wrong route of new values has a different controller and route has an action parameter with default - - // Arrange - var rd = CreateRouteData(); - rd.Values.Add("Controller", "Test"); - rd.Values.Add("Action", "Fallback"); - rd.Values.Add("param1", "fallback1"); - rd.Values.Add("param2", "fallback2"); - rd.Values.Add("param3", "fallback3"); - - TemplateRoute r = CreateRoute( - "{controller}.mvc/{action}/{param1}", - new RouteValueDictionary(new { Controller = "Test", Action = "Default" })); - - // Act - RouteValueDictionary values = CreateRouteValueDictionary(); - values.Add("controller", "subtest"); - values.Add("param1", "b"); - // The original bug for this included this value, but with the new support for - // creating query string values it changes the behavior such that the URL is - // not what was originally expected. To preserve the general behavior of this - // unit test the 'param2' value is no longer being added. - //values.Add("param2", "a"); - var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("subtest.mvc/Default/b", vpd.VirtualPath); - } - - [Fact] - public void GetUrlVerifyEncoding() - { - // Arrange - var rd = CreateRouteData(); - rd.Values.Add("controller", "Home"); - rd.Values.Add("action", "Index"); - rd.Values.Add("id", null); - - TemplateRoute r = CreateRoute( - "{controller}.mvc/{action}/{id}", - new RouteValueDictionary(new { controller = "Home" })); - - // Act - RouteValueDictionary values = CreateRouteValueDictionary(); - values.Add("controller", "#;?:@&=+$,"); - values.Add("action", "showcategory"); - values.Add("id", 123); - values.Add("so?rt", "de?sc"); - values.Add("maxPrice", 100); - var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("%23%3b%3f%3a%40%26%3d%2b%24%2c.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100", vpd.VirtualPath); - } - - [Fact] - public void GetUrlGeneratesQueryStringForNewValuesAndEscapesQueryString() - { - // Arrange - var rd = CreateRouteData(); - rd.Values.Add("controller", "Home"); - rd.Values.Add("action", "Index"); - rd.Values.Add("id", null); - - TemplateRoute r = CreateRoute( - "{controller}.mvc/{action}/{id}", - new RouteValueDictionary(new { controller = "Home" })); - - // Act - RouteValueDictionary values = CreateRouteValueDictionary(); - values.Add("controller", "products"); - values.Add("action", "showcategory"); - values.Add("id", 123); - values.Add("so?rt", "de?sc"); - values.Add("maxPrice", 100); - var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("products.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100", vpd.VirtualPath); - } - - [Fact] - public void GetUrlGeneratesQueryStringForNewValuesButIgnoresNewValuesThatMatchDefaults() - { - // Arrange - var rd = CreateRouteData(); - rd.Values.Add("controller", "Home"); - rd.Values.Add("action", "Index"); - rd.Values.Add("id", null); - - TemplateRoute r = CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { controller = "Home", Custom = "customValue" })); - - // Act - RouteValueDictionary values = CreateRouteValueDictionary(); - values.Add("controller", "products"); - values.Add("action", "showcategory"); - values.Add("id", 123); - values.Add("sort", "desc"); - values.Add("maxPrice", 100); - values.Add("custom", "customValue"); - var vpd = r.GetVirtualPath(GetHttpContext("/app1", "", ""), values); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("products.mvc/showcategory/123?sort=desc&maxPrice=100", vpd.VirtualPath); - } - - [Fact] - public void GetVirtualPathEncodesParametersAndLiterals() - { - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - TemplateRoute r = CreateRoute("bl%og/{controller}/he llo/{action}", null); - var rd = CreateRouteData(); - rd.Values.Add("controller", "ho%me"); - rd.Values.Add("action", "li st"); - var valuesDictionary = CreateRouteValueDictionary(); - - // Act - var vpd = r.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("bl%25og/ho%25me/he%20llo/li%20st", vpd.VirtualPath); - Assert.Equal(r, vpd.Route); - } - - [Fact] - public void GetVirtualPathUsesCurrentValuesNotInRouteToMatch() - { - // DevDiv Bugs 177401: UrlRouting: Incorrect route picked on urlgeneration if using controller from ambient values and route does not have a url parameter for controller - - // DevDiv Bugs 191162: UrlRouting: Route does not match when an ambient route value doesn't match a required default value in the target route - // Because of this bug the test was split into two separate verifications since the original test was verifying slightly incorrect behavior - - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - TemplateRoute r1 = CreateRoute( - "ParameterMatching.mvc/{Action}/{product}", - new RouteValueDictionary(new { Controller = "ParameterMatching", product = (string)null }), - null); - - TemplateRoute r2 = CreateRoute( - "{controller}.mvc/{action}", - new RouteValueDictionary(new { Action = "List" }), - new RouteValueDictionary(new { Controller = "Action|Bank|Overridden|DerivedFromAction|OverrideInvokeActionAndExecute|InvalidControllerName|Store|HtmlHelpers|(T|t)est|UrlHelpers|Custom|Parent|Child|TempData|ViewFactory|LocatingViews|AccessingDataInViews|ViewOverrides|ViewMasterPage|InlineCompileError|CustomView" }), - null); - - var rd = CreateRouteData(); - rd.Values.Add("controller", "Bank"); - rd.Values.Add("Action", "List"); - var valuesDictionary = CreateRouteValueDictionary(); - valuesDictionary.Add("action", "AttemptLogin"); - - // Act for first route - var vpd = r1.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("ParameterMatching.mvc/AttemptLogin", vpd.VirtualPath); - - // Act for second route - vpd = r2.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("Bank.mvc/AttemptLogin", vpd.VirtualPath); - } - -#endif [Fact] public void RouteWithCatchAllClauseCapturesManySlashes() { @@ -1510,7 +535,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("{p1}/{*p2}", null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.NotNull(rd); @@ -1527,7 +552,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("{p1}/{*p2}", null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.NotNull(rd); @@ -1544,7 +569,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("{p1}/{*p2}", null); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.NotNull(rd); @@ -1558,10 +583,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests { // Arrange HttpContext context = GetHttpContext("~/v1"); - TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { p2 = "catchall" }), null); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { p2 = "catchall" })); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.NotNull(rd); @@ -1575,10 +600,10 @@ namespace Microsoft.AspNet.Routing.Template.Tests { // Arrange HttpContext context = GetHttpContext("~/v1/hello/whatever"); - TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { p2 = "catchall" }), null); + TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { p2 = "catchall" })); // Act - var rd = r.GetRouteData(context); + var rd = r.Match(new RouteContext(context)); // Assert Assert.NotNull(rd); @@ -1587,414 +612,6 @@ namespace Microsoft.AspNet.Routing.Template.Tests Assert.Equal("hello/whatever", rd.Values["p2"]); } - [Fact] - public void RouteWithCatchAllRejectsConstraints() - { - // Arrange - HttpContext context = GetHttpContext("~/v1/abcd"); - TemplateRoute r = CreateRoute( - "{p1}/{*p2}", - new RouteValueDictionary(new { p2 = "catchall" }), - new RouteValueDictionary(new { p2 = "\\d{4}" }), - null); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.Null(rd); - } - - [Fact] - public void RouteWithCatchAllAcceptsConstraints() - { - // Arrange - HttpContext context = GetHttpContext("~/v1/1234"); - TemplateRoute r = CreateRoute( - "{p1}/{*p2}", - new RouteValueDictionary(new { p2 = "catchall" }), - new RouteValueDictionary(new { p2 = "\\d{4}" }), - null); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.NotNull(rd); - Assert.Equal(2, rd.Values.Count); - Assert.Equal("v1", rd.Values["p1"]); - Assert.Equal("1234", rd.Values["p2"]); - } - -#if URLGENERATION - - [Fact] - public void GetUrlWithCatchAllWithValue() - { - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { id = "defaultid" }), null); - - var rd = CreateRouteData(); - rd.Values.Add("p1", "v1"); - var valuesDictionary = CreateRouteValueDictionary(); - valuesDictionary.Add("p2", "v2a/v2b"); - - // Act - var vpd = r.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("v1/v2a/v2b", vpd.VirtualPath); - } - - [Fact] - public void GetUrlWithCatchAllWithEmptyValue() - { - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { id = "defaultid" }), null); - - var rd = CreateRouteData(); - rd.Values.Add("p1", "v1"); - - var valuesDictionary = CreateRouteValueDictionary(); - valuesDictionary.Add("p2", ""); - - // Act - var vpd = r.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("v1", vpd.VirtualPath); - } - - [Fact] - public void GetUrlWithCatchAllWithNullValue() - { - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { id = "defaultid" }), null); - - var rd = CreateRouteData(); - rd.Values.Add("p1", "v1"); - var valuesDictionary = CreateRouteValueDictionary(); - valuesDictionary.Add("p2", null); - - // Act - var vpd = r.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("v1", vpd.VirtualPath); - } - - [Fact] - public void GetVirtualPathWithDataTokensCopiesThemFromRouteToVirtualPathData() - { - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - TemplateRoute r = CreateRoute("{controller}/{action}", null, null, new RouteValueDictionary(new { foo = "bar", qux = "quux" })); - - var rd = CreateRouteData(); - rd.Values.Add("controller", "home"); - rd.Values.Add("action", "index"); - var valuesDictionary = CreateRouteValueDictionary(); - - // Act - var vpd = r.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("home/index", vpd.VirtualPath); - Assert.Equal(r, vpd.Route); - Assert.Equal(2, vpd.DataTokens.Count); - Assert.Equal("bar", vpd.DataTokens["foo"]); - Assert.Equal("quux", vpd.DataTokens["qux"]); - } - - [Fact] - public void GetVirtualPathWithValidCustomConstraints() - { - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); - - var rd = CreateRouteData(); - rd.Values.Add("controller", "home"); - rd.Values.Add("action", "index"); - - var valuesDictionary = CreateRouteValueDictionary(); - - // Act - var vpd = r.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("home/index", vpd.VirtualPath); - Assert.Equal(r, vpd.Route); - Assert.NotNull(r.ConstraintData); - Assert.Equal(5, r.ConstraintData.Constraint); - Assert.Equal("action", r.ConstraintData.ParameterName); - Assert.Equal("index", r.ConstraintData.ParameterValue); - } - - [Fact] - public void GetVirtualPathWithInvalidCustomConstraints() - { - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); - - var rd = CreateRouteData(); - rd.Values.Add("controller", "home"); - rd.Values.Add("action", "list"); - - var valuesDictionary = CreateRouteValueDictionary(); - - // Act - var vpd = r.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.Null(vpd); - Assert.NotNull(r.ConstraintData); - Assert.Equal(5, r.ConstraintData.Constraint); - Assert.Equal("action", r.ConstraintData.ParameterName); - Assert.Equal("list", r.ConstraintData.ParameterValue); - } - -#if DATATOKENS - - [Fact] - public void GetUrlWithCatchAllWithAmbientValue() - { - // Arrange - HttpContext context = GetHttpContext("/app", null, null); - TemplateRoute r = CreateRoute("{p1}/{*p2}", new RouteValueDictionary(new { id = "defaultid" }), null, null); - - var rd = CreateRouteData(); - rd.Values.Add("p1", "v1"); - rd.Values.Add("p2", "ambient-catch-all"); - var valuesDictionary = CreateRouteValueDictionary(); - - // Act - var vpd = r.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.NotNull(vpd); - Assert.Equal("v1/ambient-catch-all", vpd.VirtualPath); - Assert.Equal(r, vpd.Route); - Assert.Equal(0, vpd.DataTokens.Count); - } -#endif -#endif - -#if DATATOKENS - - [Fact] - public void GetRouteDataWithDataTokensCopiesThemFromRouteToIRouteData() - { - // Arrange - HttpContext context = GetHttpContext(null, "~/category/33", null); - TemplateRoute r = CreateRoute("category/{category}", null, null, new RouteValueDictionary(new { foo = "bar", qux = "quux" })); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.NotNull(rd); - Assert.Equal(1, rd.Values.Count); - Assert.Equal(2, rd.DataTokens.Count); - Assert.Equal("33", rd.Values["category"]); - Assert.Equal("bar", rd.DataTokens["foo"]); - Assert.Equal("quux", rd.DataTokens["qux"]); - } - -#endif - - [Fact] - public void GetRouteDataWithValidCustomConstraints() - { - // Arrange - HttpContext context = GetHttpContext("~/home/index"); - CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.NotNull(rd); - Assert.Equal(2, rd.Values.Count); - Assert.Equal("home", rd.Values["controller"]); - Assert.Equal("index", rd.Values["action"]); - Assert.NotNull(r.ConstraintData); - Assert.Equal(5, r.ConstraintData.Constraint); - Assert.Equal("action", r.ConstraintData.ParameterName); - Assert.Equal("index", r.ConstraintData.ParameterValue); - } - - [Fact] - public void GetRouteDataWithInvalidCustomConstraints() - { - // Arrange - HttpContext context = GetHttpContext("~/home/list"); - CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.Null(rd); - Assert.NotNull(r.ConstraintData); - Assert.Equal(5, r.ConstraintData.Constraint); - Assert.Equal("action", r.ConstraintData.ParameterName); - Assert.Equal("list", r.ConstraintData.ParameterValue); - } - - [Fact] - public void GetRouteDataWithConstraintIsCultureInsensitive() - { - // Arrange - HttpContext context = GetHttpContext("~/category/\u0130"); // Turkish upper-case dotted I - TemplateRoute r = CreateRoute( - "category/{category}", - new RouteValueDictionary(new { controller = "store", action = "showcat" }), - new RouteValueDictionary(new { category = @"[a-z]+" }), - null); - - // Act - Thread currentThread = Thread.CurrentThread; - CultureInfo backupCulture = currentThread.CurrentCulture; - RouteMatch rd; - try - { - currentThread.CurrentCulture = new CultureInfo("tr-TR"); // Turkish culture - rd = r.GetRouteData(context); - } - finally - { - currentThread.CurrentCulture = backupCulture; - } - - // Assert - Assert.Null(rd); - } - - [Fact] - public void GetRouteDataWithConstraintThatHasNoValueDoesNotMatch() - { - // Arrange - HttpContext context = GetHttpContext(null, "~/category/33"); - TemplateRoute r = CreateRoute( - "category/{category}", - new RouteValueDictionary(new { controller = "store", action = "showcat" }), - new RouteValueDictionary(new { foo = @"\d\d\d" }), - null); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.Null(rd); - } - - [Fact] - public void GetRouteDataWithCatchAllConstraintThatHasNoValueDoesNotMatch() - { - // Arrange - HttpContext context = GetHttpContext(null, "~/category"); - TemplateRoute r = CreateRoute( - "category/{*therest}", - null, - new RouteValueDictionary(new { therest = @"hola" }), - null); - - // Act - var rd = r.GetRouteData(context); - - // Assert - Assert.Null(rd); - } - - [Fact] - public void ProcessConstraintShouldGetCalledForCustomConstraintDuringUrlGeneration() - { - // DevDiv Bugs 178588: UrlRouting: ProcessConstraint is not invoked on a custom constraint that is not mapped to a url parameter during urlgeneration - - // Arrange - HttpContext context = GetHttpContext("/app", null); - - DevDivBugs178588CustomRoute r = new DevDivBugs178588CustomRoute( - "CustomPath.mvc/{action}/{param1}/{param2}", - new RouteValueDictionary(new { Controller = "Test" }), - new RouteValueDictionary(new { foo = new DevDivBugs178588CustomConstraint() })); - - var rd = CreateRouteData(); - rd.Values.Add("action", "Test"); - rd.Values.Add("param1", "111"); - rd.Values.Add("param2", "222"); - rd.Values.Add("Controller", "Test"); - - var valuesDictionary = CreateRouteValueDictionary(); - - // Act - var vpd = r.GetVirtualPath(context, valuesDictionary); - - // Assert - Assert.Null(vpd); - } - - [Fact] - public void GetRouteDataMatchesEntireLiteralSegmentScenario1a() - { - TemplateRoute r = CreateRoute( - "CatchAllParamsWithDefaults/{Controller}.mvc/{Action}/{*therest}", - new RouteValueDictionary(new { therest = "Hello" }), - new RouteValueDictionary(new { Controller = "CatchAllParams" }), - null); - - // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url - // Scenario 1.a. - GetRouteDataHelper( - r, - "CatchAllParamsWithDefaults/CatchAllParams.mvc/TestCatchAllParamInIRouteData", - new RouteValueDictionary(new { Controller = "CatchAllParams", Action = "TestCatchAllParamInIRouteData", therest = "Hello" })); - } - - [Fact] - public void GetRouteDataMatchesEntireLiteralSegmentScenario1b() - { - TemplateRoute r = CreateRoute( - "CatchAllParams/{Controller}.mvc/{Action}/{*therest}", - null, - new RouteValueDictionary(new { Controller = "CatchAllParams" }), - null); - - // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url - // Scenario 1.b. - GetRouteDataHelper( - r, - "CatchAllParamsWithDefaults/CatchAllParams.mvc/TestCatchAllParamInIRouteData", - null); - } - - [Fact] - public void GetRouteDataMatchesEntireLiteralSegmentScenario2() - { - TemplateRoute r = CreateRoute( - "{controller}.mvc/Login", - new RouteValueDictionary(new { Action = "LoginView" }), - new RouteValueDictionary(new { Controller = "Bank" }), - null); - - // DevDiv Bugs 191180: UrlRouting: Wrong route getting matched if a url segment is a substring of the requested url - // Scenario 2 - GetRouteDataHelper( - r, - "Bank.mvc/AttemptLogin", - null); - } - [Fact] public void GetRouteDataDoesNotMatchOnlyLeftLiteralMatch() { @@ -2048,8 +665,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests { TemplateRoute r = CreateRoute( "foo/{ }/{.!$%}/{dynamic.data}/{op.tional}", - new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }, - null); + new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }); GetRouteDataHelper( r, @@ -2057,89 +673,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weird" }, { "dynamic.data", "orderid" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }); } -#if URLGENERATION - - [Fact] - public void UrlWithEscapedOpenCloseBraces() - { - RouteFormatHelper("foo/{{p1}}", "foo/{p1}"); - } - - private static void RouteFormatHelper(string routeUrl, string requestUrl) - { - RouteValueDictionary defaults = new RouteValueDictionary(new { route = "matched" }); - TemplateRoute r = CreateRoute(routeUrl, defaults, null); - - GetRouteDataHelper(r, requestUrl, defaults); - GetVirtualPathHelper(r, new RouteValueDictionary(), null, Uri.EscapeUriString(requestUrl)); - } - - [Fact] - public void UrlWithEscapedOpenBraceAtTheEnd() - { - RouteFormatHelper("bar{{", "bar{"); - } - - [Fact] - public void UrlWithEscapedOpenBraceAtTheBeginning() - { - RouteFormatHelper("{{bar", "{bar"); - } - - [Fact] - public void UrlWithRepeatedEscapedOpenBrace() - { - RouteFormatHelper("foo{{{{bar", "foo{{bar"); - } - - [Fact] - public void UrlWithEscapedCloseBraceAtTheEnd() - { - RouteFormatHelper("bar}}", "bar}"); - } - - [Fact] - public void UrlWithEscapedCloseBraceAtTheBeginning() - { - RouteFormatHelper("}}bar", "}bar"); - } - - [Fact] - public void UrlWithRepeatedEscapedCloseBrace() - { - RouteFormatHelper("foo}}}}bar", "foo}}bar"); - } - - [Fact] - public void GetVirtualPathWithUnusedNullValueShouldGenerateUrlAndIgnoreNullValue() - { - // DevDiv Bugs 194371: UrlRouting: Exception thrown when generating URL that has some null values - GetVirtualPathHelper( - CreateRoute( - "{controller}.mvc/{action}/{id}", - new RouteValueDictionary(new { action = "Index", id = "" }), - null), - new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }), - new RouteValueDictionary(new { controller = "Home", action = "TestAction", id = "1", format = (string)null }), - "Home.mvc/TestAction/1"); - } - - [Fact] - public void GetVirtualPathCanFillInSeparatedParametersWithDefaultValues() - { - GetVirtualPathHelper( - CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), - new RouteValueDictionary(), - new RouteValueDictionary(new { controller = "Orders" }), - "Orders/en-US"); - } -#endif - [Fact] public void GetRouteDataDoesNotMatchRouteWithLiteralSeparatorDefaultsButNoValue() { GetRouteDataHelper( - CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" })), "foo", null); } @@ -2148,7 +686,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests public void GetRouteDataDoesNotMatchesRouteWithLiteralSeparatorDefaultsAndLeftValue() { GetRouteDataHelper( - CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" })), "foo/xx-", null); } @@ -2157,7 +695,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests public void GetRouteDataDoesNotMatchesRouteWithLiteralSeparatorDefaultsAndRightValue() { GetRouteDataHelper( - CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" })), "foo/-yy", null); } @@ -2166,64 +704,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests public void GetRouteDataMatchesRouteWithLiteralSeparatorDefaultsAndValue() { GetRouteDataHelper( - CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" }), null), + CreateRoute("{controller}/{language}-{locale}", new RouteValueDictionary(new { language = "en", locale = "US" })), "foo/xx-yy", new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } }); } -#if URLGENERATION - - [Fact] - public void GetVirtualPathWithNonParameterConstraintReturnsUrlWithoutQueryString() - { - // DevDiv Bugs 199612: UrlRouting: UrlGeneration should not append parameter to query string if it is a Constraint parameter and not a Url parameter - GetVirtualPathHelper( - CreateRoute("{Controller}.mvc/{action}/{end}", null, new RouteValueDictionary(new { foo = CreateHttpMethodConstraint("GET") }), null), - new RouteValueDictionary(), - new RouteValueDictionary(new { controller = "Orders", action = "Index", end = "end", foo = "GET" }), - "Orders.mvc/Index/end"); - } - - [Fact] - public void DefaultRoutingValuesTestWithStringEmpty() - { - var data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "42", val2 = "", val3 = "" }), new RouteValueDictionary()); - Assert.Equal("Test/42", data.VirtualPath); - - data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}/{val4}", new RouteValueDictionary(new { val1 = "21", val2 = "", val3 = "", val4 = "" }), new RouteValueDictionary(new { val1 = "42", val2 = "11", val3 = "", val4 = "" })); - Assert.Equal("Test/42/11", data.VirtualPath); - - } - - [Fact] - public void MixedDefaultAndExplicitRoutingValuesTestWithStringEmpty() - { - var data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "21", val2 = "", val3 = "" }), new RouteValueDictionary(new { val1 = "42" })); - Assert.Equal("Test/42", data.VirtualPath); - - data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}/{val4}", new RouteValueDictionary(new { val1 = "21", val2 = "", val3 = "", val4 = "" }), new RouteValueDictionary(new { val1 = "42", val2 = "11" })); - Assert.Equal("Test/42/11", data.VirtualPath); - } - - [Fact] - public void DefaultRoutingValuesTestWithNull() - { - var data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "42", val2 = (string)null, val3 = (string)null }), new RouteValueDictionary()); - Assert.Equal("Test/42", data.VirtualPath); - } - - [Fact] - public void MixedDefaultAndExplicitRoutingValuesTestWithNull() - { - var data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}", new RouteValueDictionary(new { val1 = "21", val2 = (string)null, val3 = (string)null }), new RouteValueDictionary(new { val1 = "42" })); - Assert.Equal("Test/42", data.VirtualPath); - - data = GetVirtualPathFromRoute("~/Test/", "Test/{val1}/{val2}/{val3}/{val4}", new RouteValueDictionary(new { val1 = "21", val2 = (string)null, val3 = (string)null, val4 = (string)null }), new RouteValueDictionary(new { val1 = "42", val2 = "11" })); - Assert.Equal("Test/42/11", data.VirtualPath); - } - -#endif - private static IRouteValues CreateRouteData() { return new RouteValues(new Dictionary(StringComparer.OrdinalIgnoreCase)); @@ -2241,58 +726,24 @@ namespace Microsoft.AspNet.Routing.Template.Tests HttpContext context = GetHttpContext(requestPath); // Act - var rd = route.GetRouteData(context); + var match = route.Match(new RouteContext(context)); // Assert if (expectedValues == null) { - Assert.Null(rd); + Assert.Null(match); } else { - Assert.NotNull(rd); - Assert.Equal(rd.Values.Count, expectedValues.Count); - foreach (string key in rd.Values.Keys) + Assert.NotNull(match); + Assert.Equal(match.Values.Count, expectedValues.Count); + foreach (string key in match.Values.Keys) { - Assert.Equal(expectedValues[key], rd.Values[key]); + Assert.Equal(expectedValues[key], match.Values[key]); } } } -#if URLGENERATION - private static void GetVirtualPathHelper(TemplateRoute route, RouteValueDictionary currentValues, RouteValueDictionary newValues, string expectedPath) - { - // Arrange - newValues = newValues ?? new RouteValueDictionary(); - - HttpContext context = GetHttpContext("/app", String.Empty, null); - var rd = CreateRouteData(); - foreach (var currentValue in currentValues) - { - rd.Values.Add(currentValue.Key, currentValue.Value); - } - - // Act - var vpd = route.GetVirtualPath(context, newValues); - - // Assert - if (expectedPath == null) - { - Assert.Null(vpd); - } - else - { - Assert.NotNull(vpd); - Assert.Equal(expectedPath, vpd.VirtualPath); - } - } - -#endif - private static ITemplateRouteConstraint CreateHttpMethodConstraint(params string[] methods) - { - return null; - } - internal static HttpContext GetHttpContext(string requestPath) { return GetHttpContext(null, requestPath); @@ -2319,112 +770,19 @@ namespace Microsoft.AspNet.Routing.Template.Tests private static TemplateRoute CreateRoute(string template) { - return CreateRoute(template, null, null, null); + return CreateRoute(template, null); } private static TemplateRoute CreateRoute(string template, RouteValueDictionary defaults) { - return CreateRoute(template, defaults, null, null); + return new TemplateRoute(new MockRouteEndpoint(), template, defaults); } - private static TemplateRoute CreateRoute(string template, RouteValueDictionary defaults, RouteValueDictionary constraints) + private class MockRouteEndpoint : IRouteEndpoint { - return CreateRoute(template, defaults, constraints, null); - } - - private static TemplateRoute CreateRoute(string template, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens) - { - return new TemplateRoute(template, defaults, constraints, dataTokens); - } - - private class DevDivBugs178588CustomConstraint - { - public string AllowedHeader + public Task Send(HttpContext context) { - get; - set; - } - } - - private class DevDivBugs178588CustomRoute : TemplateRoute - { - public DevDivBugs178588CustomRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints) - : base(url, defaults, constraints, null) - { - } - - protected override bool ProcessConstraint(HttpContext httpContext, object constraint, string parameterName, IDictionary values, RouteDirection routeDirection) - { - if (constraint is DevDivBugs178588CustomConstraint) - { - return false; - } - else - { - return base.ProcessConstraint(httpContext, constraint, parameterName, values, routeDirection); - } - } - } - - private sealed class ConstraintData - { - public object Constraint - { - get; - set; - } - public string ParameterName - { - get; - set; - } - public object ParameterValue - { - get; - set; - } - } - - private class CustomConstraintTemplateRoute : TemplateRoute - { - public CustomConstraintTemplateRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints) - : base(url, defaults, constraints, null) - { - } - - public ConstraintData ConstraintData - { - get; - set; - } - - protected override bool ProcessConstraint(HttpContext request, object constraint, string parameterName, IDictionary values, RouteDirection routeDirection) - { - object parameterValue; - values.TryGetValue(parameterName, out parameterValue); - - // Save the parameter values to validate them in the unit tests - ConstraintData = new ConstraintData - { - Constraint = constraint, - ParameterName = parameterName, - ParameterValue = parameterValue, - }; - - if (constraint is int) - { - int lengthRequirement = (int)constraint; - string paramString = parameterValue as string; - if (paramString == null) - { - throw new InvalidOperationException("This constraint only works with string values."); - } - return (paramString.Length == lengthRequirement); - } - else - { - return base.ProcessConstraint(request, constraint, parameterName, values, routeDirection); - } + throw new NotImplementedException(); } } From 179841743e2d3f8fcf5209165d08d5b3c3bf58db Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 7 Feb 2014 15:09:01 -0800 Subject: [PATCH 020/616] updating OM of routing, about 10% better perf --- samples/RoutingSample/Startup.cs | 3 +- .../Resources.Designer.cs | 5 +- src/Microsoft.AspNet.Routing/Resources.resx | 2 +- .../Template/ParsedTemplate.cs | 301 +++++++++++++ .../Template/PathContentSegment.cs | 65 --- .../Template/PathLiteralSubsegment.cs | 30 -- .../Template/PathParameterSubsegment.cs | 42 -- .../Template/PathSegment.cs | 15 - .../Template/PathSeparatorSegment.cs | 23 - .../Template/PathSubsegment.cs | 15 - .../Template/TemplateParsedRoute.cs | 381 ----------------- .../Template/TemplateParser.cs | 404 ++++++++++++++++++ .../Template/TemplatePart.cs | 48 +++ .../Template/TemplateRoute.cs | 4 +- .../Template/TemplateRouteParser.cs | 373 ---------------- .../Template/TemplateSegment.cs | 26 ++ .../Template/TemplateParserTests.cs | 400 +++++++++++++++++ .../Template/TemplateRouteParserTests.cs | 198 --------- .../Template/TemplateRouteTests.cs | 11 +- 19 files changed, 1192 insertions(+), 1154 deletions(-) create mode 100644 src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/PathSegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateParser.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplatePart.cs delete mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateSegment.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs delete mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs diff --git a/samples/RoutingSample/Startup.cs b/samples/RoutingSample/Startup.cs index 6c87a888b9..d169128183 100644 --- a/samples/RoutingSample/Startup.cs +++ b/samples/RoutingSample/Startup.cs @@ -4,6 +4,7 @@ using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.Routing.Owin; +using Microsoft.AspNet.Routing.Template; using Owin; namespace RoutingSample @@ -25,7 +26,7 @@ namespace RoutingSample var endpoint2 = new HttpContextRouteEndpoint(async (context) => await context.Response.WriteAsync("Hello, World!")); routes.Add(new PrefixRoute(endpoint1, "api/store")); - routes.Add(new PrefixRoute(endpoint1, "api/checkout")); + routes.Add(new TemplateRoute(endpoint1, "api/checkout/{*extra}")); routes.Add(new PrefixRoute(endpoint2, "hello/world")); routes.Add(new PrefixRoute(endpoint1, "")); } diff --git a/src/Microsoft.AspNet.Routing/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Resources.Designer.cs index 87554390e5..44df4b6429 100644 --- a/src/Microsoft.AspNet.Routing/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Routing/Resources.Designer.cs @@ -10,7 +10,6 @@ namespace Microsoft.AspNet.Routing { using System; - using System.Reflection; /// @@ -40,7 +39,7 @@ namespace Microsoft.AspNet.Routing { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; @@ -116,7 +115,7 @@ namespace Microsoft.AspNet.Routing { } /// - /// Looks up a localized string similar to There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character.. + /// Looks up a localized string similar to There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.. /// internal static string TemplateRoute_MismatchedParameter { get { diff --git a/src/Microsoft.AspNet.Routing/Resources.resx b/src/Microsoft.AspNet.Routing/Resources.resx index 76c3adf227..5372daea5b 100644 --- a/src/Microsoft.AspNet.Routing/Resources.resx +++ b/src/Microsoft.AspNet.Routing/Resources.resx @@ -136,7 +136,7 @@ The route template cannot start with a '/' or '~' character and it cannot contain a '?' character. - There is an incomplete parameter in this path segment: '{0}'. Check that each '{{' character has a matching '}}' character. + There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. The route parameter name '{0}' appears more than one time in the route template. diff --git a/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs b/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs new file mode 100644 index 0000000000..19a92fedc0 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs @@ -0,0 +1,301 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Linq; + +namespace Microsoft.AspNet.Routing.Template +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class ParsedTemplate + { + private const string SeparatorString = "/"; + private const char SeparatorChar = '/'; + + private static readonly char[] Delimiters = new char[] { SeparatorChar }; + + public ParsedTemplate(List segments) + { + if (segments == null) + { + throw new ArgumentNullException("segments"); + } + + Segments = segments; + } + + public List Segments { get; private set; } + + public IDictionary Match(string requestPath, IDictionary defaults) + { + var requestSegments = requestPath.Split(Delimiters); + + if (defaults == null) + { + defaults = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + var values = new Dictionary(StringComparer.OrdinalIgnoreCase); + + for (int i = 0; i < requestSegments.Length; i++) + { + var routeSegment = Segments.Count > i ? Segments[i] : null; + var requestSegment = requestSegments[i]; + + if (routeSegment == null) + { + // If pathSegment is null, then we're out of route segments. All we can match is the empty + // string. + if (requestSegment.Length > 0) + { + return null; + } + } + else if (routeSegment.Parts.Count == 1) + { + // Optimize for the simple case - the segment is made up for a single part + var part = routeSegment.Parts[0]; + if (part.IsLiteral) + { + if (!part.Text.Equals(requestSegment, StringComparison.OrdinalIgnoreCase)) + { + return null; + } + } + else + { + Contract.Assert(part.IsParameter); + + if (part.IsCatchAll) + { + var captured = string.Join(SeparatorString, requestSegments, i, requestSegments.Length - i); + if (captured.Length > 0) + { + values.Add(part.Name, captured); + } + else + { + // It's ok for a catch-all to produce a null value + object defaultValue; + defaults.TryGetValue(part.Name, out defaultValue); + + values.Add(part.Name, defaultValue); + } + + // A catch-all has to be the last part, so we're done. + break; + } + else + { + if (requestSegment.Length > 0) + { + values.Add(part.Name, requestSegment); + } + else + { + object defaultValue; + if (defaults.TryGetValue(part.Name, out defaultValue)) + { + values.Add(part.Name, defaultValue); + } + else + { + // There's no default for this parameter + return null; + } + } + } + } + } + else + { + if (!MatchComplexSegment(routeSegment, requestSegment, defaults, values)) + { + return null; + } + } + } + + for (int i = requestSegments.Length; i < Segments.Count; i++) + { + // We've matched the request path so far, but still have remaining route segments. These need + // to be all single-part parameter segments with default values or else they won't match. + var routeSegment = Segments[i]; + if (routeSegment.Parts.Count > 1) + { + // If it has more than one part it must contain literals, so it can't match. + return null; + } + + + var part = routeSegment.Parts[0]; + if (part.IsLiteral) + { + return null; + } + + Contract.Assert(part.IsParameter); + + // It's ok for a catch-all to produce a null value + object defaultValue; + if (defaults.TryGetValue(part.Name, out defaultValue) || part.IsCatchAll) + { + values.Add(part.Name, defaultValue); + } + else + { + // There's no default for this (non-catch-all) parameter so it can't match. + return null; + } + } + + // Copy all remaining default values to the route data + if (defaults != null) + { + foreach (var kvp in defaults) + { + if (!values.ContainsKey(kvp.Key)) + { + values.Add(kvp.Key, kvp.Value); + } + } + } + + return values; + } + + private bool MatchComplexSegment(TemplateSegment routeSegment, string requestSegment, IDictionary defaults, Dictionary values) + { + Contract.Assert(routeSegment != null); + Contract.Assert(routeSegment.Parts.Count > 1); + + // Find last literal segment and get its last index in the string + int lastIndex = requestSegment.Length; + int indexOfLastSegmentUsed = routeSegment.Parts.Count - 1; + + TemplatePart parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value + TemplatePart lastLiteral = null; // Keeps track of the left-most literal we've encountered + + while (indexOfLastSegmentUsed >= 0) + { + int newLastIndex = lastIndex; + + var part = routeSegment.Parts[indexOfLastSegmentUsed]; + if (part.IsParameter) + { + // Hold on to the parameter so that we can fill it in when we locate the next literal + parameterNeedsValue = part; + } + else + { + Contract.Assert(part.IsLiteral); + lastLiteral = part; + + int startIndex = lastIndex - 1; + // If we have a pending parameter subsegment, we must leave at least one character for that + if (parameterNeedsValue != null) + { + startIndex--; + } + + if (startIndex < 0) + { + return false; + } + + int indexOfLiteral = requestSegment.LastIndexOf(part.Text, startIndex, StringComparison.OrdinalIgnoreCase); + if (indexOfLiteral == -1) + { + // If we couldn't find this literal index, this segment cannot match + return false; + } + + // If the first subsegment is a literal, it must match at the right-most extent of the request URI. + // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/". + // This check is related to the check we do at the very end of this function. + if (indexOfLastSegmentUsed == (routeSegment.Parts.Count - 1)) + { + if ((indexOfLiteral + part.Text.Length) != requestSegment.Length) + { + return false; + } + } + + newLastIndex = indexOfLiteral; + } + + if ((parameterNeedsValue != null) && (((lastLiteral != null) && (part.IsLiteral)) || (indexOfLastSegmentUsed == 0))) + { + // If we have a pending parameter that needs a value, grab that value + + int parameterStartIndex; + int parameterTextLength; + + if (lastLiteral == null) + { + if (indexOfLastSegmentUsed == 0) + { + parameterStartIndex = 0; + } + else + { + parameterStartIndex = newLastIndex; + Contract.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above"); + } + parameterTextLength = lastIndex; + } + else + { + // If we're getting a value for a parameter that is somewhere in the middle of the segment + if ((indexOfLastSegmentUsed == 0) && (part.IsParameter)) + { + parameterStartIndex = 0; + parameterTextLength = lastIndex; + } + else + { + parameterStartIndex = newLastIndex + lastLiteral.Text.Length; + parameterTextLength = lastIndex - parameterStartIndex; + } + } + + string parameterValueString = requestSegment.Substring(parameterStartIndex, parameterTextLength); + + if (string.IsNullOrEmpty(parameterValueString)) + { + // If we're here that means we have a segment that contains multiple sub-segments. + // For these segments all parameters must have non-empty values. If the parameter + // has an empty value it's not a match. + return false; + } + else + { + // If there's a value in the segment for this parameter, use the subsegment value + values.Add(parameterNeedsValue.Name, parameterValueString); + } + + parameterNeedsValue = null; + lastLiteral = null; + } + + lastIndex = newLastIndex; + indexOfLastSegmentUsed--; + } + + // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of + // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment + // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching + // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire* + // request URI in order for it to be a match. + // This check is related to the check we do earlier in this function for LiteralSubsegments. + return (lastIndex == 0) || routeSegment.Parts[0].IsParameter; + } + + private string DebuggerToString() + { + return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString())); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs b/src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs deleted file mode 100644 index e0382db613..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/PathContentSegment.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.AspNet.Routing.Template -{ - // Represents a segment of a URI that is not a separator. It contains subsegments such as literals and parameters. - internal sealed class PathContentSegment : PathSegment - { - public PathContentSegment(IList subsegments) - { - Subsegments = subsegments; - } - - [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm.")] - public bool IsCatchAll - { - get - { - // TODO: Verify this is correct. Maybe add an assert. - // Performance sensitive - // Caching count is faster for IList - int subsegmentCount = Subsegments.Count; - for (int i = 0; i < subsegmentCount; i++) - { - PathSubsegment seg = Subsegments[i]; - PathParameterSubsegment paramterSubSegment = seg as PathParameterSubsegment; - if (paramterSubSegment != null && paramterSubSegment.IsCatchAll) - { - return true; - } - } - return false; - } - } - - public IList Subsegments { get; private set; } - -#if ROUTE_DEBUGGING - public override string LiteralText - { - get - { - List s = new List(); - foreach (PathSubsegment subsegment in Subsegments) - { - s.Add(subsegment.LiteralText); - } - return String.Join(String.Empty, s.ToArray()); - } - } - - public override string ToString() - { - List s = new List(); - foreach (PathSubsegment subsegment in Subsegments) - { - s.Add(subsegment.ToString()); - } - return "[ " + String.Join(", ", s.ToArray()) + " ]"; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs b/src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs deleted file mode 100644 index 312597a387..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/PathLiteralSubsegment.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Routing.Template -{ - // Represents a literal subsegment of a ContentPathSegment - internal sealed class PathLiteralSubsegment : PathSubsegment - { - public PathLiteralSubsegment(string literal) - { - Literal = literal; - } - - public string Literal { get; private set; } - -#if ROUTE_DEBUGGING - public override string LiteralText - { - get - { - return Literal; - } - } - - public override string ToString() - { - return "\"" + Literal + "\""; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs b/src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs deleted file mode 100644 index 2edf14ccef..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/PathParameterSubsegment.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; - -namespace Microsoft.AspNet.Routing.Template -{ - // Represents a parameter subsegment of a ContentPathSegment - internal sealed class PathParameterSubsegment : PathSubsegment - { - public PathParameterSubsegment(string parameterName) - { - if (parameterName.StartsWith("*", StringComparison.Ordinal)) - { - ParameterName = parameterName.Substring(1); - IsCatchAll = true; - } - else - { - ParameterName = parameterName; - } - } - - public bool IsCatchAll { get; private set; } - - public string ParameterName { get; private set; } - -#if ROUTE_DEBUGGING - public override string LiteralText - { - get - { - return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}"; - } - } - - public override string ToString() - { - return "{" + (IsCatchAll ? "*" : String.Empty) + ParameterName + "}"; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/PathSegment.cs b/src/Microsoft.AspNet.Routing/Template/PathSegment.cs deleted file mode 100644 index f58279ef0f..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/PathSegment.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Routing.Template -{ - // Represents a segment of a URI such as a separator or content - public abstract class PathSegment - { -#if ROUTE_DEBUGGING - public abstract string LiteralText - { - get; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs b/src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs deleted file mode 100644 index 88cb876f70..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/PathSeparatorSegment.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Routing.Template -{ - // Represents a "/" separator in a URI - internal sealed class PathSeparatorSegment : PathSegment - { -#if ROUTE_DEBUGGING - public override string LiteralText - { - get - { - return "/"; - } - } - - public override string ToString() - { - return "\"/\""; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs b/src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs deleted file mode 100644 index 60e0175f50..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/PathSubsegment.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -namespace Microsoft.AspNet.Routing.Template -{ - // Represents a subsegment of a ContentPathSegment such as a parameter or a literal. - internal abstract class PathSubsegment - { -#if ROUTE_DEBUGGING - public abstract string LiteralText - { - get; - } -#endif - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs deleted file mode 100644 index 49db3415a7..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/TemplateParsedRoute.cs +++ /dev/null @@ -1,381 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; - -namespace Microsoft.AspNet.Routing.Template -{ - public class TemplateParsedRoute - { - public TemplateParsedRoute(IList pathSegments) - { - Contract.Assert(pathSegments != null); - PathSegments = pathSegments; - } - - internal IList PathSegments { get; private set; } - - private static bool ForEachParameter(IList pathSegments, Func action) - { - for (int i = 0; i < pathSegments.Count; i++) - { - PathSegment pathSegment = pathSegments[i]; - - if (pathSegment is PathSeparatorSegment) - { - // We only care about parameter subsegments, so skip this - continue; - } - else - { - PathContentSegment contentPathSegment = pathSegment as PathContentSegment; - if (contentPathSegment != null) - { - foreach (PathSubsegment subsegment in contentPathSegment.Subsegments) - { - PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment; - if (literalSubsegment != null) - { - // We only care about parameter subsegments, so skip this - continue; - } - else - { - PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment; - if (parameterSubsegment != null) - { - if (!action(parameterSubsegment)) - { - return false; - } - } - else - { - Contract.Assert(false, "Invalid path subsegment type"); - } - } - } - } - else - { - Contract.Assert(false, "Invalid path segment type"); - } - } - } - - return true; - } - - public IDictionary Match(string virtualPath, IDictionary defaultValues) - { - IList requestPathSegments = TemplateRouteParser.SplitUriToPathSegmentStrings(virtualPath); - - if (defaultValues == null) - { - defaultValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - IDictionary matchedValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // This flag gets set once all the data in the URI has been parsed through, but - // the route we're trying to match against still has more parts. At this point - // we'll only continue matching separator characters and parameters that have - // default values. - bool ranOutOfStuffToParse = false; - - // This value gets set once we start processing a catchall parameter (if there is one - // at all). Once we set this value we consume all remaining parts of the URI into its - // parameter value. - bool usedCatchAllParameter = false; - - for (int i = 0; i < PathSegments.Count; i++) - { - PathSegment pathSegment = PathSegments[i]; - - if (requestPathSegments.Count <= i) - { - ranOutOfStuffToParse = true; - } - - string requestPathSegment = ranOutOfStuffToParse ? null : requestPathSegments[i]; - - if (pathSegment is PathSeparatorSegment) - { - if (ranOutOfStuffToParse) - { - // If we're trying to match a separator in the route but there's no more content, that's OK - } - else - { - if (!String.Equals(requestPathSegment, "/", StringComparison.Ordinal)) - { - return null; - } - } - } - else - { - PathContentSegment contentPathSegment = pathSegment as PathContentSegment; - if (contentPathSegment != null) - { - if (contentPathSegment.IsCatchAll) - { - Contract.Assert(i == (PathSegments.Count - 1), "If we're processing a catch-all, we should be on the last route segment."); - MatchCatchAll(contentPathSegment, requestPathSegments.Skip(i), defaultValues, matchedValues); - usedCatchAllParameter = true; - } - else - { - if (!MatchContentPathSegment(contentPathSegment, requestPathSegment, defaultValues, matchedValues)) - { - return null; - } - } - } - else - { - Contract.Assert(false, "Invalid path segment type"); - } - } - } - - if (!usedCatchAllParameter) - { - if (PathSegments.Count < requestPathSegments.Count) - { - // If we've already gone through all the parts defined in the route but the URI - // still contains more content, check that the remaining content is all separators. - for (int i = PathSegments.Count; i < requestPathSegments.Count; i++) - { - if (!TemplateRouteParser.IsSeparator(requestPathSegments[i])) - { - return null; - } - } - } - } - - // Copy all remaining default values to the route data - if (defaultValues != null) - { - foreach (var defaultValue in defaultValues) - { - if (!matchedValues.ContainsKey(defaultValue.Key)) - { - matchedValues.Add(defaultValue.Key, defaultValue.Value); - } - } - } - - return matchedValues; - } - - private static void MatchCatchAll(PathContentSegment contentPathSegment, IEnumerable remainingRequestSegments, IDictionary defaultValues, IDictionary matchedValues) - { - string remainingRequest = String.Join(String.Empty, remainingRequestSegments.ToArray()); - - PathParameterSubsegment catchAllSegment = contentPathSegment.Subsegments[0] as PathParameterSubsegment; - - object catchAllValue; - - if (remainingRequest.Length > 0) - { - catchAllValue = remainingRequest; - } - else - { - defaultValues.TryGetValue(catchAllSegment.ParameterName, out catchAllValue); - } - - matchedValues.Add(catchAllSegment.ParameterName, catchAllValue); - } - - private static bool MatchContentPathSegment(PathContentSegment routeSegment, string requestPathSegment, IDictionary defaultValues, IDictionary matchedValues) - { - if (String.IsNullOrEmpty(requestPathSegment)) - { - // If there's no data to parse, we must have exactly one parameter segment and no other segments - otherwise no match - - if (routeSegment.Subsegments.Count > 1) - { - return false; - } - - PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[0] as PathParameterSubsegment; - if (parameterSubsegment == null) - { - return false; - } - - // We must have a default value since there's no value in the request URI - object parameterValue; - if (defaultValues.TryGetValue(parameterSubsegment.ParameterName, out parameterValue)) - { - // If there's a default value for this parameter, use that default value - matchedValues.Add(parameterSubsegment.ParameterName, parameterValue); - return true; - } - else - { - // If there's no default value, this segment doesn't match - return false; - } - } - - // Optimize for the common case where there is only one subsegment in the segment - either a parameter or a literal - if (routeSegment.Subsegments.Count == 1) - { - return MatchSingleContentPathSegment(routeSegment.Subsegments[0], requestPathSegment, matchedValues); - } - - // Find last literal segment and get its last index in the string - - int lastIndex = requestPathSegment.Length; - int indexOfLastSegmentUsed = routeSegment.Subsegments.Count - 1; - - PathParameterSubsegment parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value - PathLiteralSubsegment lastLiteral = null; // Keeps track of the left-most literal we've encountered - - while (indexOfLastSegmentUsed >= 0) - { - int newLastIndex = lastIndex; - - PathParameterSubsegment parameterSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathParameterSubsegment; - if (parameterSubsegment != null) - { - // Hold on to the parameter so that we can fill it in when we locate the next literal - parameterNeedsValue = parameterSubsegment; - } - else - { - PathLiteralSubsegment literalSubsegment = routeSegment.Subsegments[indexOfLastSegmentUsed] as PathLiteralSubsegment; - if (literalSubsegment != null) - { - lastLiteral = literalSubsegment; - - int startIndex = lastIndex - 1; - // If we have a pending parameter subsegment, we must leave at least one character for that - if (parameterNeedsValue != null) - { - startIndex--; - } - - if (startIndex < 0) - { - return false; - } - - int indexOfLiteral = requestPathSegment.LastIndexOf(literalSubsegment.Literal, startIndex, StringComparison.OrdinalIgnoreCase); - if (indexOfLiteral == -1) - { - // If we couldn't find this literal index, this segment cannot match - return false; - } - - // If the first subsegment is a literal, it must match at the right-most extent of the request URI. - // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/". - // This check is related to the check we do at the very end of this function. - if (indexOfLastSegmentUsed == (routeSegment.Subsegments.Count - 1)) - { - if ((indexOfLiteral + literalSubsegment.Literal.Length) != requestPathSegment.Length) - { - return false; - } - } - - newLastIndex = indexOfLiteral; - } - else - { - Contract.Assert(false, "Invalid path segment type"); - } - } - - if ((parameterNeedsValue != null) && (((lastLiteral != null) && (parameterSubsegment == null)) || (indexOfLastSegmentUsed == 0))) - { - // If we have a pending parameter that needs a value, grab that value - - int parameterStartIndex; - int parameterTextLength; - - if (lastLiteral == null) - { - if (indexOfLastSegmentUsed == 0) - { - parameterStartIndex = 0; - } - else - { - parameterStartIndex = newLastIndex; - Contract.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above"); - } - parameterTextLength = lastIndex; - } - else - { - // If we're getting a value for a parameter that is somewhere in the middle of the segment - if ((indexOfLastSegmentUsed == 0) && (parameterSubsegment != null)) - { - parameterStartIndex = 0; - parameterTextLength = lastIndex; - } - else - { - parameterStartIndex = newLastIndex + lastLiteral.Literal.Length; - parameterTextLength = lastIndex - parameterStartIndex; - } - } - - string parameterValueString = requestPathSegment.Substring(parameterStartIndex, parameterTextLength); - - if (String.IsNullOrEmpty(parameterValueString)) - { - // If we're here that means we have a segment that contains multiple sub-segments. - // For these segments all parameters must have non-empty values. If the parameter - // has an empty value it's not a match. - return false; - } - else - { - // If there's a value in the segment for this parameter, use the subsegment value - matchedValues.Add(parameterNeedsValue.ParameterName, parameterValueString); - } - - parameterNeedsValue = null; - lastLiteral = null; - } - - lastIndex = newLastIndex; - indexOfLastSegmentUsed--; - } - - // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of - // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment - // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching - // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire* - // request URI in order for it to be a match. - // This check is related to the check we do earlier in this function for LiteralSubsegments. - return (lastIndex == 0) || (routeSegment.Subsegments[0] is PathParameterSubsegment); - } - - private static bool MatchSingleContentPathSegment(PathSubsegment pathSubsegment, string requestPathSegment, IDictionary matchedValues) - { - PathParameterSubsegment parameterSubsegment = pathSubsegment as PathParameterSubsegment; - if (parameterSubsegment == null) - { - // Handle a single literal segment - PathLiteralSubsegment literalSubsegment = pathSubsegment as PathLiteralSubsegment; - Contract.Assert(literalSubsegment != null, "Invalid path segment type"); - return literalSubsegment.Literal.Equals(requestPathSegment, StringComparison.OrdinalIgnoreCase); - } - else - { - // Handle a single parameter segment - matchedValues.Add(parameterSubsegment.ParameterName, requestPathSegment); - return true; - } - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs new file mode 100644 index 0000000000..21c9a87f69 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs @@ -0,0 +1,404 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Linq; + +namespace Microsoft.AspNet.Routing.Template +{ + public static class TemplateParser + { + private const char Separator = '/'; + private const char OpenBrace = '{'; + private const char CloseBrace = '}'; + + public static ParsedTemplate Parse(string routeTemplate) + { + if (routeTemplate == null) + { + routeTemplate = String.Empty; + } + + if (IsInvalidRouteTemplate(routeTemplate)) + { + throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, "routeTemplate"); + } + + var context = new TemplateParserContext(routeTemplate); + var segments = new List(); + + while (context.Next()) + { + if (context.Current == Separator) + { + // If we get here is means that there's a consecutive '/' character. Templates don't start with a '/' and + // parsing a segment consumes the separator. + throw new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, "routeTemplate"); + } + else + { + if (!ParseSegment(context, segments)) + { + throw new ArgumentException(context.Error, "routeTemplate"); + } + } + } + + if (IsAllValid(context, segments)) + { + return new ParsedTemplate(segments); + } + else + { + throw new ArgumentException(context.Error, "routeTemplate"); + } + } + + private static bool ParseSegment(TemplateParserContext context, List segments) + { + Contract.Assert(context != null); + Contract.Assert(segments != null); + + var segment = new TemplateSegment(); + + while (true) + { + if (context.Current == OpenBrace) + { + if (!context.Next()) + { + // This is a dangling open-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + + if (context.Current == OpenBrace) + { + // This is an 'escaped' brace in a literal, like "{{foo" + context.Back(); + if (!ParseLiteral(context, segment)) + { + return false; + } + } + else + { + // This is the inside of a parameter + if (!ParseParameter(context, segment)) + { + return false; + } + } + } + else if (context.Current == Separator) + { + // We've reached the end of the segment + break; + } + else + { + if (!ParseLiteral(context, segment)) + { + return false; + } + } + + if (!context.Next()) + { + // We've reached the end of the string + break; + } + } + + if (IsSegmentValid(context, segment)) + { + segments.Add(segment); + return true; + } + else + { + return false; + } + } + + private static bool ParseParameter(TemplateParserContext context, TemplateSegment segment) + { + context.Mark(); + + while (true) + { + if (context.Current == Separator) + { + // This is a dangling open-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + else if (context.Current == OpenBrace) + { + // If we see a '{' while parsing a parameter name it's invalid. We'll just accept it for now + // and let the validation code for the name find it. + } + else if (context.Current == CloseBrace) + { + if (!context.Next()) + { + // This is the end of the string - and we have a valid parameter + context.Back(); + break; + } + + if (context.Current == CloseBrace) + { + // This is an 'escaped' brace in a parameter name, which is not allowed. We'll just accept it for now + // and let the validation code for the name find it. + } + else + { + // This is the end of the parameter + context.Back(); + break; + } + } + + if (!context.Next()) + { + // This is a dangling open-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + } + + var rawName = context.Capture(); + + var isCatchAll = rawName.StartsWith("*", StringComparison.Ordinal); + var parameterName = isCatchAll ? rawName.Substring(1) : rawName; + if (IsValidParameterName(context, parameterName)) + { + segment.Parts.Add(TemplatePart.CreateParameter(parameterName, isCatchAll)); + return true; + } + else + { + return false; + } + } + + private static bool ParseLiteral(TemplateParserContext context, TemplateSegment segment) + { + context.Mark(); + + string encoded; + while (true) + { + if (context.Current == Separator) + { + encoded = context.Capture(); + context.Back(); + break; + } + else if (context.Current == OpenBrace) + { + if (!context.Next()) + { + // This is a dangling open-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + + if (context.Current == OpenBrace) + { + // This is an 'escaped' brace in a literal, like "{{foo" - keep going. + } + else + { + // We've just seen the start of a parameter, so back up and return + context.Back(); + encoded = context.Capture(); + context.Back(); + break; + } + } + else if (context.Current == CloseBrace) + { + if (!context.Next()) + { + // This is a dangling close-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + + if (context.Current == CloseBrace) + { + // This is an 'escaped' brace in a literal, like "{{foo" - keep going. + } + else + { + // This is an unbalanced close-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + } + + if (!context.Next()) + { + encoded = context.Capture(); + break; + } + } + + var decoded = encoded.Replace("}}", "}").Replace("{{", "}"); + segment.Parts.Add(TemplatePart.CreateLiteral(decoded)); + return true; + } + + private static bool IsAllValid(TemplateParserContext context, List segments) + { + // A catch-all parameter must be the last part of the last segment + for (int i = 0; i < segments.Count; i++) + { + var segment = segments[i]; + for (int j = 0; j < segment.Parts.Count; j++) + { + var part = segment.Parts[j]; + if (part.IsParameter && part.IsCatchAll && (i != segments.Count - 1 || j != segment.Parts.Count - 1)) + { + context.Error = Resources.TemplateRoute_CatchAllMustBeLast; + return false; + } + } + } + + return true; + } + + private static bool IsSegmentValid(TemplateParserContext context, TemplateSegment segment) + { + // If a segment has multiple parts, then it can't contain a catch all. + for (int i = 0; i < segment.Parts.Count; i++) + { + var part = segment.Parts[i]; + if (part.IsParameter && part.IsCatchAll && segment.Parts.Count > 1) + { + context.Error = Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment; + return false; + } + } + + // A segment cannot containt two consecutive parameters + var isLastSegmentParameter = false; + for (int i = 0; i < segment.Parts.Count; i++) + { + var part = segment.Parts[i]; + if (part.IsParameter && isLastSegmentParameter) + { + context.Error = Resources.TemplateRoute_CannotHaveConsecutiveParameters; + return false; + } + + isLastSegmentParameter = part.IsParameter; + } + + return true; + } + + private static bool IsValidParameterName(TemplateParserContext context, string parameterName) + { + if (parameterName.Length == 0) + { + context.Error = String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName); + return false; + } + + for (int i = 0; i < parameterName.Length; i++) + { + var c = parameterName[i]; + if (c == '/' || c == '{' || c == '}') + { + context.Error = String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName); + return false; + } + } + + if (!context.ParameterNames.Add(parameterName)) + { + context.Error = String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_RepeatedParameter, parameterName); + return false; + } + + return true; + } + + private static bool IsInvalidRouteTemplate(string routeTemplate) + { + return routeTemplate.StartsWith("~", StringComparison.Ordinal) || + routeTemplate.StartsWith("/", StringComparison.Ordinal) || + (routeTemplate.IndexOf('?') != -1); + } + + + private class TemplateParserContext + { + private readonly string _template; + private int _index; + private int? _mark; + + private HashSet _parameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + public TemplateParserContext(string template) + { + Contract.Assert(template != null); + _template = template; + + _index = -1; + } + + public char Current + { + get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; } + } + + public string Error + { + get; + set; + } + + public HashSet ParameterNames + { + get { return _parameterNames; } + } + + public bool Back() + { + return --_index >= 0; + } + + public bool Next() + { + return ++_index < _template.Length; + } + + public void Mark() + { + _mark = _index; + } + + public string Capture() + { + if (_mark.HasValue) + { + var value = _template.Substring(_mark.Value, _index - _mark.Value); + _mark = null; + return value; + } + else + { + return null; + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs new file mode 100644 index 0000000000..e21306d4ad --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Diagnostics; + +namespace Microsoft.AspNet.Routing.Template +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class TemplatePart + { + public static TemplatePart CreateLiteral(string text) + { + return new TemplatePart() + { + IsLiteral = true, + Text = text, + }; + } + + public static TemplatePart CreateParameter(string name, bool isCatchAll) + { + return new TemplatePart() + { + IsParameter = true, + Name = name, + IsCatchAll = isCatchAll, + }; + } + + public bool IsCatchAll { get; private set; } + public bool IsLiteral { get; private set; } + public bool IsParameter { get; private set; } + + public string Name { get; private set; } + public string Text { get; private set; } + + internal string DebuggerToString() + { + if (IsParameter) + { + return "{" + (IsCatchAll ? "*" : string.Empty) + Name + "}"; + } + else + { + return Text; + } + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index 76f4a68b66..4eab348596 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Routing.Template { private readonly IDictionary _defaults; private readonly IRouteEndpoint _endpoint; - private readonly TemplateParsedRoute _parsedRoute; + private readonly ParsedTemplate _parsedRoute; private readonly string _routeTemplate; public TemplateRoute(IRouteEndpoint endpoint, string routeTemplate) @@ -29,7 +29,7 @@ namespace Microsoft.AspNet.Routing.Template _defaults = defaults ?? new Dictionary(StringComparer.OrdinalIgnoreCase); // The parser will throw for invalid routes. - _parsedRoute = TemplateRouteParser.Parse(RouteTemplate); + _parsedRoute = TemplateParser.Parse(RouteTemplate); } public IDictionary Defaults diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs deleted file mode 100644 index d82a7b3ec6..0000000000 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRouteParser.cs +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; -using System.Globalization; -using System.Linq; - -namespace Microsoft.AspNet.Routing.Template -{ - public static class TemplateRouteParser - { - private static string GetLiteral(string segmentLiteral) - { - // Scan for errant single { and } and convert double {{ to { and double }} to } - - // First we eliminate all escaped braces and then check if any other braces are remaining - string newLiteral = segmentLiteral.Replace("{{", String.Empty).Replace("}}", String.Empty); - if (newLiteral.Contains("{") || newLiteral.Contains("}")) - { - return null; - } - - // If it's a valid format, we unescape the braces - return segmentLiteral.Replace("{{", "{").Replace("}}", "}"); - } - - private static int IndexOfFirstOpenParameter(string segment, int startIndex) - { - // Find the first unescaped open brace - while (true) - { - startIndex = segment.IndexOf('{', startIndex); - if (startIndex == -1) - { - // If there are no more open braces, stop - return -1; - } - if ((startIndex + 1 == segment.Length) || - ((startIndex + 1 < segment.Length) && (segment[startIndex + 1] != '{'))) - { - // If we found an open brace that is followed by a non-open brace, it's - // a parameter delimiter. - // It's also a delimiter if the open brace is the last character - though - // it ends up being being called out as invalid later on. - return startIndex; - } - // Increment by two since we want to skip both the open brace that - // we're on as well as the subsequent character since we know for - // sure that it is part of an escape sequence. - startIndex += 2; - } - } - - internal static bool IsSeparator(string s) - { - return String.Equals(s, "/", StringComparison.Ordinal); - } - - private static bool IsValidParameterName(string parameterName) - { - if (parameterName.Length == 0) - { - return false; - } - - for (int i = 0; i < parameterName.Length; i++) - { - char c = parameterName[i]; - if (c == '/' || c == '{' || c == '}') - { - return false; - } - } - - return true; - } - - internal static bool IsInvalidRouteTemplate(string routeTemplate) - { - return routeTemplate.StartsWith("~", StringComparison.Ordinal) || - routeTemplate.StartsWith("/", StringComparison.Ordinal) || - (routeTemplate.IndexOf('?') != -1); - } - - public static TemplateParsedRoute Parse(string routeTemplate) - { - if (routeTemplate == null) - { - routeTemplate = String.Empty; - } - - if (IsInvalidRouteTemplate(routeTemplate)) - { - throw new ArgumentException(Resources.TemplateRoute_InvalidRouteTemplate, "routeTemplate"); - } - - IList uriParts = SplitUriToPathSegmentStrings(routeTemplate); - Exception ex = ValidateUriParts(uriParts); - if (ex != null) - { - throw ex; - } - - IList pathSegments = SplitUriToPathSegments(uriParts); - - Contract.Assert(uriParts.Count == pathSegments.Count, "The number of string segments should be the same as the number of path segments"); - - return new TemplateParsedRoute(pathSegments); - } - - [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", - Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] - private static IList ParseUriSegment(string segment, out Exception exception) - { - int startIndex = 0; - - List pathSubsegments = new List(); - - while (startIndex < segment.Length) - { - int nextParameterStart = IndexOfFirstOpenParameter(segment, startIndex); - if (nextParameterStart == -1) - { - // If there are no more parameters in the segment, capture the remainder as a literal and stop - string lastLiteralPart = GetLiteral(segment.Substring(startIndex)); - if (lastLiteralPart == null) - { - exception = new ArgumentException( - String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment), - "routeTemplate"); - - return null; - } - - if (lastLiteralPart.Length > 0) - { - pathSubsegments.Add(new PathLiteralSubsegment(lastLiteralPart)); - } - break; - } - - int nextParameterEnd = segment.IndexOf('}', nextParameterStart + 1); - if (nextParameterEnd == -1) - { - exception = new ArgumentException( - String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment), - "routeTemplate"); - return null; - } - - string literalPart = GetLiteral(segment.Substring(startIndex, nextParameterStart - startIndex)); - if (literalPart == null) - { - exception = new ArgumentException( - String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_MismatchedParameter, segment), - "routeTemplate"); - return null; - } - - if (literalPart.Length > 0) - { - pathSubsegments.Add(new PathLiteralSubsegment(literalPart)); - } - - string parameterName = segment.Substring(nextParameterStart + 1, nextParameterEnd - nextParameterStart - 1); - pathSubsegments.Add(new PathParameterSubsegment(parameterName)); - - startIndex = nextParameterEnd + 1; - } - - exception = null; - return pathSubsegments; - } - - private static IList SplitUriToPathSegments(IList uriParts) - { - List pathSegments = new List(); - - foreach (string pathSegment in uriParts) - { - bool isCurrentPartSeparator = IsSeparator(pathSegment); - if (isCurrentPartSeparator) - { - pathSegments.Add(new PathSeparatorSegment()); - } - else - { - Exception exception; - IList subsegments = ParseUriSegment(pathSegment, out exception); - Contract.Assert(exception == null, "This only gets called after the path has been validated, so there should never be an exception here"); - pathSegments.Add(new PathContentSegment(subsegments)); - } - } - return pathSegments; - } - - internal static IList SplitUriToPathSegmentStrings(string uri) - { - List parts = new List(); - - if (String.IsNullOrEmpty(uri)) - { - return parts; - } - - int currentIndex = 0; - - // Split the incoming URI into individual parts - while (currentIndex < uri.Length) - { - int indexOfNextSeparator = uri.IndexOf('/', currentIndex); - if (indexOfNextSeparator == -1) - { - // If there are no more separators, the rest of the string is the last part - string finalPart = uri.Substring(currentIndex); - if (finalPart.Length > 0) - { - parts.Add(finalPart); - } - break; - } - - string nextPart = uri.Substring(currentIndex, indexOfNextSeparator - currentIndex); - if (nextPart.Length > 0) - { - parts.Add(nextPart); - } - - Contract.Assert(uri[indexOfNextSeparator] == '/', "The separator char itself should always be a '/'."); - parts.Add("/"); - currentIndex = indexOfNextSeparator + 1; - } - - return parts; - } - - [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Not changing original algorithm")] - [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", - Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] - private static Exception ValidateUriParts(IList pathSegments) - { - Contract.Assert(pathSegments != null, "The value should always come from SplitUri(), and that function should never return null."); - - HashSet usedParameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); - bool? isPreviousPartSeparator = null; - - bool foundCatchAllParameter = false; - - foreach (string pathSegment in pathSegments) - { - if (foundCatchAllParameter) - { - // If we ever start an iteration of the loop and we've already found a - // catchall parameter then we have an invalid URI format. - return new ArgumentException(Resources.TemplateRoute_CatchAllMustBeLast, "routeTemplate"); - } - - bool isCurrentPartSeparator; - if (isPreviousPartSeparator == null) - { - // Prime the loop with the first value - isPreviousPartSeparator = IsSeparator(pathSegment); - isCurrentPartSeparator = isPreviousPartSeparator.Value; - } - else - { - isCurrentPartSeparator = IsSeparator(pathSegment); - - // If both the previous part and the current part are separators, it's invalid - if (isCurrentPartSeparator && isPreviousPartSeparator.Value) - { - return new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveSeparators, "routeTemplate"); - } - - Contract.Assert(isCurrentPartSeparator != isPreviousPartSeparator.Value, "This assert should only happen if both the current and previous parts are non-separators. This should never happen because consecutive non-separators are always parsed as a single part."); - isPreviousPartSeparator = isCurrentPartSeparator; - } - - // If it's not a separator, parse the segment for parameters and validate it - if (!isCurrentPartSeparator) - { - Exception exception; - IList subsegments = ParseUriSegment(pathSegment, out exception); - if (exception != null) - { - return exception; - } - - exception = ValidateUriSegment(subsegments, usedParameterNames); - if (exception != null) - { - return exception; - } - - foundCatchAllParameter = subsegments.Any(seg => (seg is PathParameterSubsegment) && ((PathParameterSubsegment)seg).IsCatchAll); - } - } - return null; - } - - [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", - Justification = "The exceptions are just constructed here, but they are thrown from a method that does have those parameter names.")] - private static Exception ValidateUriSegment(IList pathSubsegments, HashSet usedParameterNames) - { - bool segmentContainsCatchAll = false; - - Type previousSegmentType = null; - - foreach (PathSubsegment subsegment in pathSubsegments) - { - if (previousSegmentType != null) - { - if (previousSegmentType == subsegment.GetType()) - { - return new ArgumentException(Resources.TemplateRoute_CannotHaveConsecutiveParameters, "routeTemplate"); - } - } - previousSegmentType = subsegment.GetType(); - - PathLiteralSubsegment literalSubsegment = subsegment as PathLiteralSubsegment; - if (literalSubsegment != null) - { - // Nothing to validate for literals - everything is valid - } - else - { - PathParameterSubsegment parameterSubsegment = subsegment as PathParameterSubsegment; - if (parameterSubsegment != null) - { - string parameterName = parameterSubsegment.ParameterName; - - if (parameterSubsegment.IsCatchAll) - { - segmentContainsCatchAll = true; - } - - // Check for valid characters in the parameter name - if (!IsValidParameterName(parameterName)) - { - return new ArgumentException( - String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName), - "routeTemplate"); - } - - if (usedParameterNames.Contains(parameterName)) - { - return new ArgumentException( - String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_RepeatedParameter, parameterName), - "routeTemplate"); - } - else - { - usedParameterNames.Add(parameterName); - } - } - else - { - Contract.Assert(false, "Invalid path subsegment type"); - } - } - } - - if (segmentContainsCatchAll && (pathSubsegments.Count != 1)) - { - return new ArgumentException(Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment, "routeTemplate"); - } - - return null; - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateSegment.cs b/src/Microsoft.AspNet.Routing/Template/TemplateSegment.cs new file mode 100644 index 0000000000..12b1ecf136 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateSegment.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNet.Routing.Template +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class TemplateSegment + { + private readonly List _parts = new List(); + + public List Parts + { + get { return _parts; } + } + + internal string DebuggerToString() + { + return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString())); + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs new file mode 100644 index 0000000000..0b08688037 --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs @@ -0,0 +1,400 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Extensions; + +namespace Microsoft.AspNet.Routing.Template.Tests +{ + public class TemplateRouteParserTests + { + [Fact] + public void Parse_SingleLiteral() + { + // Arrange + var template = "cool"; + + var expected = new ParsedTemplate(new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_SingleParameter() + { + // Arrange + var template = "{p}"; + + var expected = new ParsedTemplate(new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false)); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_MultipleLiterals() + { + // Arrange + var template = "cool/awesome/super"; + + var expected = new ParsedTemplate(new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("awesome")); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[2].Parts.Add(TemplatePart.CreateLiteral("super")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_MultipleParameters() + { + // Arrange + var template = "{p1}/{p2}/{*p3}"; + + var expected = new ParsedTemplate(new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false)); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", false)); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3", true)); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_LP() + { + // Arrange + var template = "cool-{p1}"; + + var expected = new ParsedTemplate(new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false)); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_PL() + { + // Arrange + var template = "{p1}-cool"; + + var expected = new ParsedTemplate(new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_PLP() + { + // Arrange + var template = "{p1}-cool-{p2}"; + + var expected = new ParsedTemplate(new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", false)); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_ComplexSegment_LPL() + { + // Arrange + var template = "cool-{p1}-awesome"; + + var expected = new ParsedTemplate(new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("-awesome")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void InvalidTemplate_WithRepeatedParameter() + { + var ex = Assert.Throws( + () => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"), + "The route parameter name 'controller' appears more than one time in the route template." + Environment.NewLine + "Parameter name: routeTemplate"); + } + + [Theory] + [InlineData("123{a}abc{")] + [InlineData("123{a}abc}")] + [InlineData("xyz}123{a}abc}")] + [InlineData("{{p1}")] + [InlineData("{p1}}")] + [InlineData("p1}}p2{")] + public void InvalidTemplate_WithMismatchedBraces(string template) + { + Assert.Throws( + () => TemplateParser.Parse(template), + @"There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveCatchAllInMultiSegment() + { + Assert.Throws( + () => TemplateParser.Parse("123{a}abc{*moo}"), + "A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAll() + { + Assert.Throws( + () => TemplateParser.Parse("{*p1}/{*p2}"), + "A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment() + { + Assert.Throws( + () => TemplateParser.Parse("{*p1}abc{*p2}"), + "A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveCatchAllWithNoName() + { + Assert.Throws( + () => TemplateParser.Parse("foo/{*}"), + @"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveOpenBrace() + { + Assert.Throws( + () => TemplateParser.Parse("foo/{{p1}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveCloseBrace() + { + Assert.Throws( + () => TemplateParser.Parse("foo/{p1}}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_SameParameterTwiceThrows() + { + Assert.Throws( + () => TemplateParser.Parse("{aaa}/{AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows() + { + Assert.Throws( + () => TemplateParser.Parse("{aaa}/{*AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows() + { + Assert.Throws( + () => TemplateParser.Parse("{a}/{aa}a}/{z}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows() + { + Assert.Throws( + () => TemplateParser.Parse("{a}/{a{aa}/{z}"), + @"The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows() + { + Assert.Throws( + () => TemplateParser.Parse("{a}/{}/{z}"), + @"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_InvalidParameterNameWithQuestionThrows() + { + Assert.Throws( + () => TemplateParser.Parse("{Controller}.mvc/{?}"), + "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows() + { + Assert.Throws( + () => TemplateParser.Parse("{a}//{z}"), + "The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_WithCatchAllNotAtTheEndThrows() + { + Assert.Throws( + () => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"), + "A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_RepeatedParametersThrows() + { + Assert.Throws( + () => TemplateParser.Parse("foo/aa{p1}{p2}"), + "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotStartWithSlash() + { + Assert.Throws( + () => TemplateParser.Parse("/foo"), + "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotStartWithTilde() + { + Assert.Throws( + () => TemplateParser.Parse("~foo"), + "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_CannotContainQuestionMark() + { + Assert.Throws( + () => TemplateParser.Parse("foor?bar"), + "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + private class TemplateParsedRouteEqualityComparer : IEqualityComparer + { + public bool Equals(ParsedTemplate x, ParsedTemplate y) + { + if (x == null && y == null) + { + return true; + } + else if (x == null || y == null) + { + return false; + } + else + { + if (x.Segments.Count != y.Segments.Count) + { + return false; + } + + for (int i = 0; i < x.Segments.Count; i++) + { + if (x.Segments[i].Parts.Count != y.Segments[i].Parts.Count) + { + return false; + } + + for (int j = 0; j < x.Segments[i].Parts.Count; j++) + { + var xPart = x.Segments[i].Parts[j]; + var yPart = y.Segments[i].Parts[j]; + + if (xPart.IsLiteral != yPart.IsLiteral || + xPart.IsParameter != yPart.IsParameter || + xPart.IsCatchAll != yPart.IsCatchAll || + !String.Equals(xPart.Name, yPart.Name, StringComparison.Ordinal) || + !String.Equals(xPart.Name, yPart.Name, StringComparison.Ordinal)) + { + return false; + } + } + } + + return true; + } + } + + public int GetHashCode(ParsedTemplate obj) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs deleted file mode 100644 index 5096687d93..0000000000 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteParserTests.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; -using Xunit.Extensions; - -namespace Microsoft.AspNet.Routing.Template.Tests -{ - public class TemplateRouteParserTests - { - [Fact] - public void InvalidTemplate_WithRepeatedParameter() - { - var ex = Assert.Throws( - () => TemplateRouteParser.Parse("{Controller}.mvc/{id}/{controller}"), - "The route parameter name 'controller' appears more than one time in the route template." + Environment.NewLine + "Parameter name: routeTemplate"); - } - - [Theory] - [InlineData("123{a}abc{")] - [InlineData("123{a}abc}")] - [InlineData("xyz}123{a}abc}")] - [InlineData("{{p1}")] - [InlineData("{p1}}")] - [InlineData("p1}}p2{")] - public void InvalidTemplate_WithMismatchedBraces(string template) - { - Assert.Throws( - () => TemplateRouteParser.Parse(template), - @"There is an incomplete parameter in this path segment: '" + template + @"'. Check that each '{' character has a matching '}' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotHaveCatchAllInMultiSegment() - { - Assert.Throws( - () => TemplateRouteParser.Parse("123{a}abc{*moo}"), - "A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotHaveMoreThanOneCatchAll() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{*p1}/{*p2}"), - "A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{*p1}abc{*p2}"), - "A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotHaveCatchAllWithNoName() - { - Assert.Throws( - () => TemplateRouteParser.Parse("foo/{*}"), - @"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotHaveConsecutiveOpenBrace() - { - Assert.Throws( - () => TemplateRouteParser.Parse("foo/{{p1}"), - "There is an incomplete parameter in this path segment: '{{p1}'. Check that each '{' character has a matching '}' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotHaveConsecutiveCloseBrace() - { - Assert.Throws( - () => TemplateRouteParser.Parse("foo/{p1}}"), - "There is an incomplete parameter in this path segment: '{p1}}'. Check that each '{' character has a matching '}' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_SameParameterTwiceThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{aaa}/{AAA}"), - "The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{aaa}/{*AAA}"), - "The route parameter name 'AAA' appears more than one time in the route template." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{a}/{aa}a}/{z}"), - "There is an incomplete parameter in this path segment: '{aa}a}'. Check that each '{' character has a matching '}' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{a}/{a{aa}/{z}"), - @"The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{a}/{}/{z}"), - @"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_InvalidParameterNameWithQuestionThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{Controller}.mvc/{?}"), - "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("{a}//{z}"), - "The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_WithCatchAllNotAtTheEndThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("foo/{p1}/{*p2}/{p3}"), - "A catch-all parameter can only appear as the last segment of the route template." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_RepeatedParametersThrows() - { - Assert.Throws( - () => TemplateRouteParser.Parse("foo/aa{p1}{p2}"), - "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotStartWithSlash() - { - Assert.Throws( - () => TemplateRouteParser.Parse("/foo"), - "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotStartWithTilde() - { - Assert.Throws( - () => TemplateRouteParser.Parse("~foo"), - "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - - [Fact] - public void InvalidTemplate_CannotContainQuestionMark() - { - Assert.Throws( - () => TemplateRouteParser.Parse("foor?bar"), - "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + - "Parameter name: routeTemplate"); - } - } -} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs index 5c52254d1a..4c67210827 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs @@ -20,12 +20,13 @@ namespace Microsoft.AspNet.Routing.Template.Tests TemplateRoute r = CreateRoute("{controller}/{action}/{id}", null); // Act - var rd = r.Match(new RouteContext(context)); + var match = r.Match(new RouteContext(context)); // Assert - Assert.Equal("Bank", rd.Values["controller"]); - Assert.Equal("DoAction", rd.Values["action"]); - Assert.Equal("123", rd.Values["id"]); + Assert.NotNull(match); + Assert.Equal("Bank", match.Values["controller"]); + Assert.Equal("DoAction", match.Values["action"]); + Assert.Equal("123", match.Values["id"]); } [Fact] @@ -736,7 +737,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests else { Assert.NotNull(match); - Assert.Equal(match.Values.Count, expectedValues.Count); + Assert.Equal(expectedValues.Count, match.Values.Count); foreach (string key in match.Values.Keys) { Assert.Equal(expectedValues[key], match.Values[key]); From 5ee991cf61adb5b62cd5250157d915c959000267 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 11 Feb 2014 11:55:02 -0800 Subject: [PATCH 021/616] CR feedback --- src/Microsoft.AspNet.Routing/Resources.Designer.cs | 4 ++++ src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNet.Routing/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Resources.Designer.cs index 44df4b6429..34805f5e59 100644 --- a/src/Microsoft.AspNet.Routing/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Routing/Resources.Designer.cs @@ -39,7 +39,11 @@ namespace Microsoft.AspNet.Routing { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { +#if NET45 global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", typeof(Resources).Assembly); +#else + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); +#endif resourceMan = temp; } return resourceMan; diff --git a/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs b/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs index 19a92fedc0..743c8465e8 100644 --- a/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs +++ b/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNet.Routing.Template var part = routeSegment.Parts[0]; if (part.IsLiteral) { - if (!part.Text.Equals(requestSegment, StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(part.Text, requestSegment, StringComparison.OrdinalIgnoreCase)) { return null; } From 42ce8c6594e5a99bf7d8a194e76c907c7468216c Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 11 Feb 2014 12:21:40 -0800 Subject: [PATCH 022/616] Adding routebuilder --- .../RoutingSample/RouteBuilderExtensions.cs | 14 +++++++++++ samples/RoutingSample/Startup.cs | 12 ++++++---- src/Microsoft.AspNet.Routing/IRouteBuilder.cs | 17 +++++++++++++ src/Microsoft.AspNet.Routing/RouteBuilder.cs | 24 +++++++++++++++++++ .../Template/RouteBuilderExtensions.cs | 19 +++++++++++++++ 5 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 samples/RoutingSample/RouteBuilderExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/IRouteBuilder.cs create mode 100644 src/Microsoft.AspNet.Routing/RouteBuilder.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/RouteBuilderExtensions.cs diff --git a/samples/RoutingSample/RouteBuilderExtensions.cs b/samples/RoutingSample/RouteBuilderExtensions.cs new file mode 100644 index 0000000000..2848864774 --- /dev/null +++ b/samples/RoutingSample/RouteBuilderExtensions.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.Routing; + +namespace RoutingSample +{ + public static class RouteBuilderExtensions + { + public static void AddPrefixRoute(this IRouteBuilder builder, string prefix) + { + builder.Routes.Add(new PrefixRoute(builder.Endpoint, prefix)); + } + } +} diff --git a/samples/RoutingSample/Startup.cs b/samples/RoutingSample/Startup.cs index d169128183..ee7c7a3665 100644 --- a/samples/RoutingSample/Startup.cs +++ b/samples/RoutingSample/Startup.cs @@ -3,6 +3,7 @@ #if NET45 using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing.Owin; using Microsoft.AspNet.Routing.Template; using Owin; @@ -25,10 +26,13 @@ namespace RoutingSample var endpoint1 = new HttpContextRouteEndpoint(async (context) => await context.Response.WriteAsync("match1")); var endpoint2 = new HttpContextRouteEndpoint(async (context) => await context.Response.WriteAsync("Hello, World!")); - routes.Add(new PrefixRoute(endpoint1, "api/store")); - routes.Add(new TemplateRoute(endpoint1, "api/checkout/{*extra}")); - routes.Add(new PrefixRoute(endpoint2, "hello/world")); - routes.Add(new PrefixRoute(endpoint1, "")); + var rb1 = new RouteBuilder(endpoint1, routes); + rb1.AddPrefixRoute("api/store"); + rb1.AddTemplateRoute("api/checkout/{*extra}"); + + var rb2 = new RouteBuilder(endpoint2, routes); + rb2.AddPrefixRoute("hello/world"); + rb2.AddPrefixRoute(""); } } } diff --git a/src/Microsoft.AspNet.Routing/IRouteBuilder.cs b/src/Microsoft.AspNet.Routing/IRouteBuilder.cs new file mode 100644 index 0000000000..87f67506cc --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRouteBuilder.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing +{ + public interface IRouteBuilder + { + IRouteEndpoint Endpoint + { + get; + } + + IRouteCollection Routes + { + get; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RouteBuilder.cs b/src/Microsoft.AspNet.Routing/RouteBuilder.cs new file mode 100644 index 0000000000..638c983276 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteBuilder.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing +{ + public class RouteBuilder : IRouteBuilder + { + public RouteBuilder(IRouteEndpoint endpoint, IRouteCollection routes) + { + Endpoint = endpoint; + Routes = routes; + } + public IRouteEndpoint Endpoint + { + get; + private set; + } + + public IRouteCollection Routes + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/RouteBuilderExtensions.cs b/src/Microsoft.AspNet.Routing/Template/RouteBuilderExtensions.cs new file mode 100644 index 0000000000..f6d706d29b --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/RouteBuilderExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing.Template +{ + public static class RouteBuilderExtensions + { + public static void AddTemplateRoute(this IRouteBuilder builder, string template) + { + AddTemplateRoute(builder, template, null); + } + + public static void AddTemplateRoute(this IRouteBuilder builder, string template, IDictionary defaults) + { + builder.Routes.Add(new TemplateRoute(builder.Endpoint, template, defaults)); + } + } +} From 64c29fe8137b9d56bf12cb02875aaaf5217fb88b Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 18 Feb 2014 15:22:18 -0800 Subject: [PATCH 023/616] Adding optional parameters --- .../Properties/Resources.Designer.cs | 115 ++++++++++++++ .../Resources.Designer.cs | 148 ------------------ src/Microsoft.AspNet.Routing/Resources.resx | 10 +- .../Template/ParsedTemplate.cs | 4 + .../Template/TemplateParser.cs | 52 +++++- .../Template/TemplatePart.cs | 7 +- .../Template/TemplateParserTests.cs | 73 +++++++-- .../Template/TemplateRouteTests.cs | 46 ++++++ 8 files changed, 279 insertions(+), 176 deletions(-) create mode 100644 src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs delete mode 100644 src/Microsoft.AspNet.Routing/Resources.Designer.cs diff --git a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..8cdac84de3 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs @@ -0,0 +1,115 @@ +// +namespace Microsoft.AspNet.Routing +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Routing.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter. + /// + internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment + { + get { return GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment"); } + } + + /// + /// A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string. + /// + internal static string TemplateRoute_CannotHaveConsecutiveParameters + { + get { return GetString("TemplateRoute_CannotHaveConsecutiveParameters"); } + } + + /// + /// The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. + /// + internal static string TemplateRoute_CannotHaveConsecutiveSeparators + { + get { return GetString("TemplateRoute_CannotHaveConsecutiveSeparators"); } + } + + /// + /// A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter. + /// + internal static string TemplateRoute_CannotHaveOptionalParameterInMultiSegment + { + get { return GetString("TemplateRoute_CannotHaveOptionalParameterInMultiSegment"); } + } + + /// + /// A catch-all parameter can only appear as the last segment of the route template. + /// + internal static string TemplateRoute_CatchAllMustBeLast + { + get { return GetString("TemplateRoute_CatchAllMustBeLast"); } + } + + /// + /// The literal section '{0}' is invalid. Literal sections cannot contain the '?' character. + /// + internal static string TemplateRoute_InvalidLiteral + { + get { return GetString("TemplateRoute_InvalidLiteral"); } + } + + /// + /// The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can only occur at the end of the parameter. + /// + internal static string TemplateRoute_InvalidParameterName + { + get { return GetString("TemplateRoute_InvalidParameterName"); } + } + + /// + /// The route template cannot start with a '/' or '~' character. + /// + internal static string TemplateRoute_InvalidRouteTemplate + { + get { return GetString("TemplateRoute_InvalidRouteTemplate"); } + } + + /// + /// There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. + /// + internal static string TemplateRoute_MismatchedParameter + { + get { return GetString("TemplateRoute_MismatchedParameter"); } + } + + /// + /// The route parameter name '{0}' appears more than one time in the route template. + /// + internal static string TemplateRoute_RepeatedParameter + { + get { return GetString("TemplateRoute_RepeatedParameter"); } + } + + /// + /// The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'. + /// + internal static string TemplateRoute_ValidationMustBeStringOrCustomConstraint + { + get { return GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint"); } + } + + private static string GetString(string name, params string[] argumentNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + for (var i = 0; i < argumentNames.Length; i++) + { + value = value.Replace("{" + argumentNames[i] + "}", "{" + i + "}"); + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Resources.Designer.cs deleted file mode 100644 index 34805f5e59..0000000000 --- a/src/Microsoft.AspNet.Routing/Resources.Designer.cs +++ /dev/null @@ -1,148 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.34003 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.AspNet.Routing { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { -#if NET45 - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", typeof(Resources).Assembly); -#else - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Routing.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); -#endif - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to A path segment that contains more than one section, such as a literal section or a parameter, cannot contain a catch-all parameter.. - /// - internal static string TemplateRoute_CannotHaveCatchAllInMultiSegment { - get { - return ResourceManager.GetString("TemplateRoute_CannotHaveCatchAllInMultiSegment", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by a literal string.. - /// - internal static string TemplateRoute_CannotHaveConsecutiveParameters { - get { - return ResourceManager.GetString("TemplateRoute_CannotHaveConsecutiveParameters", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value.. - /// - internal static string TemplateRoute_CannotHaveConsecutiveSeparators { - get { - return ResourceManager.GetString("TemplateRoute_CannotHaveConsecutiveSeparators", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to A catch-all parameter can only appear as the last segment of the route template.. - /// - internal static string TemplateRoute_CatchAllMustBeLast { - get { - return ResourceManager.GetString("TemplateRoute_CatchAllMustBeLast", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?". - /// - internal static string TemplateRoute_InvalidParameterName { - get { - return ResourceManager.GetString("TemplateRoute_InvalidParameterName", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The route template cannot start with a '/' or '~' character and it cannot contain a '?' character.. - /// - internal static string TemplateRoute_InvalidRouteTemplate { - get { - return ResourceManager.GetString("TemplateRoute_InvalidRouteTemplate", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character.. - /// - internal static string TemplateRoute_MismatchedParameter { - get { - return ResourceManager.GetString("TemplateRoute_MismatchedParameter", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The route parameter name '{0}' appears more than one time in the route template.. - /// - internal static string TemplateRoute_RepeatedParameter { - get { - return ResourceManager.GetString("TemplateRoute_RepeatedParameter", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to The constraint entry '{0}' on the route with route template '{1}' must have a string value or be of a type which implements '{2}'.. - /// - internal static string TemplateRoute_ValidationMustBeStringOrCustomConstraint { - get { - return ResourceManager.GetString("TemplateRoute_ValidationMustBeStringOrCustomConstraint", resourceCulture); - } - } - } -} diff --git a/src/Microsoft.AspNet.Routing/Resources.resx b/src/Microsoft.AspNet.Routing/Resources.resx index 5372daea5b..35e303768d 100644 --- a/src/Microsoft.AspNet.Routing/Resources.resx +++ b/src/Microsoft.AspNet.Routing/Resources.resx @@ -126,14 +126,20 @@ The route template separator character '/' cannot appear consecutively. It must be separated by either a parameter or a literal value. + + A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter. + A catch-all parameter can only appear as the last segment of the route template. + + The literal section '{0}' is invalid. Literal sections cannot contain the '?' character. + - The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: "{{", "}}", "/", "?" + The route parameter name '{0}' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{{', '}}', '/'. The '?' character marks a parameter as optional, and can only occur at the end of the parameter. - The route template cannot start with a '/' or '~' character and it cannot contain a '?' character. + The route template cannot start with a '/' or '~' character. There is an incomplete parameter in the route template. Check that each '{' character has a matching '}' character. diff --git a/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs b/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs index 743c8465e8..94a32d38c9 100644 --- a/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs +++ b/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs @@ -144,6 +144,10 @@ namespace Microsoft.AspNet.Routing.Template { values.Add(part.Name, defaultValue); } + else if (part.IsOptional) + { + // This is optional (with no default value) - there's nothing to capture here, so just move on. + } else { // There's no default for this (non-catch-all) parameter so it can't match. diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs index 21c9a87f69..3b87faeecb 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs @@ -14,6 +14,8 @@ namespace Microsoft.AspNet.Routing.Template private const char Separator = '/'; private const char OpenBrace = '{'; private const char CloseBrace = '}'; + private const char EqualsSign = '='; + private const char QuestionMark = '?'; public static ParsedTemplate Parse(string routeTemplate) { @@ -174,10 +176,15 @@ namespace Microsoft.AspNet.Routing.Template var rawName = context.Capture(); var isCatchAll = rawName.StartsWith("*", StringComparison.Ordinal); - var parameterName = isCatchAll ? rawName.Substring(1) : rawName; + var isOptional = rawName.EndsWith("?", StringComparison.Ordinal); + + rawName = isCatchAll ? rawName.Substring(1) : rawName; + rawName = isOptional ? rawName.Substring(0, rawName.Length - 1) : rawName; + + var parameterName = rawName; if (IsValidParameterName(context, parameterName)) { - segment.Parts.Add(TemplatePart.CreateParameter(parameterName, isCatchAll)); + segment.Parts.Add(TemplatePart.CreateParameter(parameterName, isCatchAll, isOptional)); return true; } else @@ -250,8 +257,15 @@ namespace Microsoft.AspNet.Routing.Template } var decoded = encoded.Replace("}}", "}").Replace("{{", "}"); - segment.Parts.Add(TemplatePart.CreateLiteral(decoded)); - return true; + if (IsValidLiteral(context, decoded)) + { + segment.Parts.Add(TemplatePart.CreateLiteral(decoded)); + return true; + } + else + { + return false; + } } private static bool IsAllValid(TemplateParserContext context, List segments) @@ -287,6 +301,17 @@ namespace Microsoft.AspNet.Routing.Template } } + // if a segment has multiple parts, then the parameters can't be optional + for (int i = 0; i < segment.Parts.Count; i++) + { + var part = segment.Parts[i]; + if (part.IsParameter && part.IsOptional && segment.Parts.Count > 1) + { + context.Error = Resources.TemplateRoute_CannotHaveOptionalParameterInMultiSegment; + return false; + } + } + // A segment cannot containt two consecutive parameters var isLastSegmentParameter = false; for (int i = 0; i < segment.Parts.Count; i++) @@ -315,7 +340,7 @@ namespace Microsoft.AspNet.Routing.Template for (int i = 0; i < parameterName.Length; i++) { var c = parameterName[i]; - if (c == '/' || c == '{' || c == '}') + if (c == Separator || c == OpenBrace || c == CloseBrace || c == QuestionMark) { context.Error = String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidParameterName, parameterName); return false; @@ -331,11 +356,24 @@ namespace Microsoft.AspNet.Routing.Template return true; } + private static bool IsValidLiteral(TemplateParserContext context, string literal) + { + Contract.Assert(context != null); + Contract.Assert(literal != null); + + if (literal.IndexOf(QuestionMark) != -1) + { + context.Error = String.Format(CultureInfo.CurrentCulture, Resources.TemplateRoute_InvalidLiteral, literal); + return false; + } + + return true; + } + private static bool IsInvalidRouteTemplate(string routeTemplate) { return routeTemplate.StartsWith("~", StringComparison.Ordinal) || - routeTemplate.StartsWith("/", StringComparison.Ordinal) || - (routeTemplate.IndexOf('?') != -1); + routeTemplate.StartsWith("/", StringComparison.Ordinal); } diff --git a/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs index e21306d4ad..1a361f5399 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplatePart.cs @@ -16,20 +16,21 @@ namespace Microsoft.AspNet.Routing.Template }; } - public static TemplatePart CreateParameter(string name, bool isCatchAll) + public static TemplatePart CreateParameter(string name, bool isCatchAll, bool isOptional) { return new TemplatePart() { IsParameter = true, Name = name, IsCatchAll = isCatchAll, + IsOptional = isOptional, }; } public bool IsCatchAll { get; private set; } public bool IsLiteral { get; private set; } public bool IsParameter { get; private set; } - + public bool IsOptional { get; private set; } public string Name { get; private set; } public string Text { get; private set; } @@ -37,7 +38,7 @@ namespace Microsoft.AspNet.Routing.Template { if (IsParameter) { - return "{" + (IsCatchAll ? "*" : string.Empty) + Name + "}"; + return "{" + (IsCatchAll ? "*" : string.Empty) + Name + (IsOptional ? "?" : string.Empty) + "}"; } else { diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs index 0b08688037..1f3ca9d78b 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs @@ -35,7 +35,24 @@ namespace Microsoft.AspNet.Routing.Template.Tests var expected = new ParsedTemplate(new List()); expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false)); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, false)); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + } + + [Fact] + public void Parse_OptionalParameter() + { + // Arrange + var template = "{p?}"; + + var expected = new ParsedTemplate(new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p", false, true)); // Act var actual = TemplateParser.Parse(template); @@ -73,11 +90,11 @@ namespace Microsoft.AspNet.Routing.Template.Tests var expected = new ParsedTemplate(new List()); expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false)); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false)); expected.Segments.Add(new TemplateSegment()); - expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", false)); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", false, false)); expected.Segments.Add(new TemplateSegment()); - expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3", true)); + expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3", true, false)); // Act var actual = TemplateParser.Parse(template); @@ -95,7 +112,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests var expected = new ParsedTemplate(new List()); expected.Segments.Add(new TemplateSegment()); expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false)); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false)); // Act var actual = TemplateParser.Parse(template); @@ -112,7 +129,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests var expected = new ParsedTemplate(new List()); expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false)); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false)); expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); // Act @@ -130,9 +147,9 @@ namespace Microsoft.AspNet.Routing.Template.Tests var expected = new ParsedTemplate(new List()); expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false)); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false)); expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", false)); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", false, false)); // Act var actual = TemplateParser.Parse(template); @@ -150,7 +167,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests var expected = new ParsedTemplate(new List()); expected.Segments.Add(new TemplateSegment()); expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false)); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", false, false)); expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("-awesome")); // Act @@ -215,7 +232,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests { Assert.Throws( () => TemplateParser.Parse("foo/{*}"), - @"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " + + "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine + "Parameter name: routeTemplate"); } @@ -269,7 +287,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests { Assert.Throws( () => TemplateParser.Parse("{a}/{a{aa}/{z}"), - @"The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + + "The route parameter name 'a{aa' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " + + "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine + "Parameter name: routeTemplate"); } @@ -278,7 +297,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests { Assert.Throws( () => TemplateParser.Parse("{a}/{}/{z}"), - @"The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: ""{"", ""}"", ""/"", ""?""" + Environment.NewLine + + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " + + "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine + "Parameter name: routeTemplate"); } @@ -287,7 +307,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests { Assert.Throws( () => TemplateParser.Parse("{Controller}.mvc/{?}"), - "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " + + "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine + "Parameter name: routeTemplate"); } @@ -323,7 +344,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests { Assert.Throws( () => TemplateParser.Parse("/foo"), - "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "The route template cannot start with a '/' or '~' character." + Environment.NewLine + "Parameter name: routeTemplate"); } @@ -332,7 +353,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests { Assert.Throws( () => TemplateParser.Parse("~foo"), - "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "The route template cannot start with a '/' or '~' character." + Environment.NewLine + "Parameter name: routeTemplate"); } @@ -341,10 +362,29 @@ namespace Microsoft.AspNet.Routing.Template.Tests { Assert.Throws( () => TemplateParser.Parse("foor?bar"), - "The route template cannot start with a '/' or '~' character and it cannot contain a '?' character." + Environment.NewLine + + "The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character." + Environment.NewLine + "Parameter name: routeTemplate"); } + [Fact] + public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd() + { + Assert.Throws( + () => TemplateParser.Parse("{foor?b}"), + "The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot contain these characters: '{', '}', '/'. " + + "The '?' character marks a parameter as optional, and can only occur at the end of the parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + + [Fact] + public void InvalidTemplate_MultiSegmentParameterCannotContainOptionalParameter() + { + Assert.Throws( + () => TemplateParser.Parse("{foorb?}-bar-{z}"), + "A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter." + Environment.NewLine + + "Parameter name: routeTemplate"); + } + private class TemplateParsedRouteEqualityComparer : IEqualityComparer { public bool Equals(ParsedTemplate x, ParsedTemplate y) @@ -379,6 +419,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests if (xPart.IsLiteral != yPart.IsLiteral || xPart.IsParameter != yPart.IsParameter || xPart.IsCatchAll != yPart.IsCatchAll || + xPart.IsOptional != yPart.IsOptional || !String.Equals(xPart.Name, yPart.Name, StringComparison.Ordinal) || !String.Equals(xPart.Name, yPart.Name, StringComparison.Ordinal)) { diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs index 4c67210827..8d80336a3c 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs @@ -710,6 +710,52 @@ namespace Microsoft.AspNet.Routing.Template.Tests new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } }); } + [Fact] + public void MatchSetsOptionalParameter() + { + // Arrange + var route = CreateRoute("{controller}/{action?}"); + var url = "Home/Index"; + + // Act + var match = route.Match(new RouteContext(GetHttpContext(url))); + + // Assert + Assert.NotNull(match); + Assert.Equal("Index", match.Values["action"]); + } + + [Fact] + public void MatchDoesNotSetOptionalParameter() + { + // Arrange + var route = CreateRoute("{controller}/{action?}"); + var url = "Home"; + + // Act + var match = route.Match(new RouteContext(GetHttpContext(url))); + + // Assert + Assert.NotNull(match); + Assert.False(match.Values.ContainsKey("action")); + } + + [Fact] + public void MatchMultipleOptionalParameters() + { + // Arrange + var route = CreateRoute("{controller}/{action?}/{id?}"); + var url = "Home/Index"; + + // Act + var match = route.Match(new RouteContext(GetHttpContext(url))); + + // Assert + Assert.NotNull(match); + Assert.Equal("Index", match.Values["action"]); + Assert.False(match.Values.ContainsKey("id")); + } + private static IRouteValues CreateRouteData() { return new RouteValues(new Dictionary(StringComparer.OrdinalIgnoreCase)); From 4f71137cbdca365e1f2384c6d5c3d71bcf4ea86a Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 24 Feb 2014 13:01:08 -0800 Subject: [PATCH 024/616] CR feedback --- .../Properties/Resources.Designer.cs | 8 ++++++++ src/Microsoft.AspNet.Routing/Resources.resx | 3 +++ .../Template/TemplateParser.cs | 6 ++++++ .../Template/TemplateParserTests.cs | 16 ++++++++++++---- .../Template/TemplateRouteTests.cs | 6 ++++++ 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs index 8cdac84de3..a37cb32044 100644 --- a/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Routing/Properties/Resources.Designer.cs @@ -42,6 +42,14 @@ namespace Microsoft.AspNet.Routing get { return GetString("TemplateRoute_CannotHaveOptionalParameterInMultiSegment"); } } + /// + /// A catch-all parameter cannot be marked optional. + /// + internal static string TemplateRoute_CatchAllCannotBeOptional + { + get { return GetString("TemplateRoute_CatchAllCannotBeOptional"); } + } + /// /// A catch-all parameter can only appear as the last segment of the route template. /// diff --git a/src/Microsoft.AspNet.Routing/Resources.resx b/src/Microsoft.AspNet.Routing/Resources.resx index 35e303768d..5fa396065a 100644 --- a/src/Microsoft.AspNet.Routing/Resources.resx +++ b/src/Microsoft.AspNet.Routing/Resources.resx @@ -129,6 +129,9 @@ A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter. + + A catch-all parameter cannot be marked optional. + A catch-all parameter can only appear as the last segment of the route template. diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs index 3b87faeecb..3a74597f93 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs @@ -178,6 +178,12 @@ namespace Microsoft.AspNet.Routing.Template var isCatchAll = rawName.StartsWith("*", StringComparison.Ordinal); var isOptional = rawName.EndsWith("?", StringComparison.Ordinal); + if (isCatchAll && isOptional) + { + context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional; + return false; + } + rawName = isCatchAll ? rawName.Substring(1) : rawName; rawName = isOptional ? rawName.Substring(0, rawName.Length - 1) : rawName; diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs index 1f3ca9d78b..9140836284 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs @@ -1,8 +1,7 @@ -using System; +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Xunit; using Xunit.Extensions; @@ -384,6 +383,15 @@ namespace Microsoft.AspNet.Routing.Template.Tests "A path segment that contains more than one section, such as a literal section or a parameter, cannot contain an optional parameter." + Environment.NewLine + "Parameter name: routeTemplate"); } + + [Fact] + public void InvalidTemplate_CatchAllMarkedOptional() + { + Assert.Throws( + () => TemplateParser.Parse("{a}/{*b?}"), + "A catch-all parameter cannot be marked optional." + Environment.NewLine + + "Parameter name: routeTemplate"); + } private class TemplateParsedRouteEqualityComparer : IEqualityComparer { diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs index 8d80336a3c..a97f956c0d 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs @@ -722,6 +722,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests // Assert Assert.NotNull(match); + Assert.Equal(2, match.Values.Count); + Assert.Equal("Home", match.Values["controller"]); Assert.Equal("Index", match.Values["action"]); } @@ -737,6 +739,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests // Assert Assert.NotNull(match); + Assert.Equal(1, match.Values.Count); + Assert.Equal("Home", match.Values["controller"]); Assert.False(match.Values.ContainsKey("action")); } @@ -752,6 +756,8 @@ namespace Microsoft.AspNet.Routing.Template.Tests // Assert Assert.NotNull(match); + Assert.Equal(2, match.Values.Count); + Assert.Equal("Home", match.Values["controller"]); Assert.Equal("Index", match.Values["action"]); Assert.False(match.Values.ContainsKey("id")); } From 4022e5a5a4098b9c14f43ff2af6deee23d49619f Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 24 Feb 2014 19:37:09 -0800 Subject: [PATCH 025/616] Code dump of algorthmic code for url generation This doesn't yet expose url generation via public api, that will come in the next change. --- .../Template/BoundRouteTemplate.cs | 9 + .../Template/Template.cs | 59 + .../Template/TemplateBinder.cs | 580 +++++++++ .../{ParsedTemplate.cs => TemplateMatcher.cs} | 27 +- .../Template/TemplateParser.cs | 4 +- .../Template/TemplateRoute.cs | 8 +- .../Template/Assert.cs | 4 - .../Template/RouteValueDictionary.cs | 28 + .../Template/TemplateBinderTests.cs | 1056 +++++++++++++++++ .../Template/TemplateMatcherTests.cs | 783 ++++++++++++ .../Template/TemplateParserTests.cs | 94 +- .../Template/TemplateRouteTests.cs | 1006 ---------------- 12 files changed, 2595 insertions(+), 1063 deletions(-) create mode 100644 src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/Template.cs create mode 100644 src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs rename src/Microsoft.AspNet.Routing/Template/{ParsedTemplate.cs => TemplateMatcher.cs} (93%) create mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/RouteValueDictionary.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs create mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs delete mode 100644 test/Microsoft.AspNet.Routing.Tests/Template/TemplateRouteTests.cs diff --git a/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs b/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs new file mode 100644 index 0000000000..f2a3671f82 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/BoundRouteTemplate.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing.Template +{ + public class BoundRouteTemplate + { + public string Path { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/Template.cs b/src/Microsoft.AspNet.Routing/Template/Template.cs new file mode 100644 index 0000000000..13fb861ded --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/Template.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.AspNet.Routing.Template +{ + [DebuggerDisplay("{DebuggerToString()}")] + public class Template + { + private const string SeparatorString = "/"; + + private readonly TemplateMatcher _matcher; + private readonly TemplateBinder _binder; + + public Template(List segments) + { + if (segments == null) + { + throw new ArgumentNullException("segments"); + } + + Segments = segments; + + Parameters = new List(); + for (var i = 0; i < segments.Count; i++) + { + var segment = Segments[i]; + for (var j = 0; j < segment.Parts.Count; j++) + { + var part = segment.Parts[j]; + if (part.IsParameter) + { + Parameters.Add(part); + } + } + } + + _matcher = new TemplateMatcher(this); + _binder = new TemplateBinder(this); + } + + public List Parameters { get; private set; } + + public List Segments { get; private set; } + + public IDictionary Match(string requestPath, IDictionary defaults) + { + return _matcher.Match(requestPath, defaults); + } + + private string DebuggerToString() + { + return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString())); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs new file mode 100644 index 0000000000..638af9dd8e --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Template/TemplateBinder.cs @@ -0,0 +1,580 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.AspNet.Routing.Template +{ + public class TemplateBinder + { + public TemplateBinder(Template template) + { + if (template == null) + { + throw new ArgumentNullException("template"); + } + + Template = template; + } + + public Template Template { get; private set; } + + public BoundRouteTemplate Bind(IDictionary defaults, IDictionary ambientValues, IDictionary values) + { + if (values == null) + { + throw new ArgumentNullException("values"); + } + + var context = GetAcceptedValues(defaults, ambientValues, values); + if (context == null) + { + // We couldn't get values for all the required parameters + return null; + } + + return BindValues(context); + } + + // Step 1: Get the list of values we're going to try to use to match and generate this URI + private TemplateBindingContext GetAcceptedValues(IDictionary defaults, IDictionary ambientValues, IDictionary values) + { + Contract.Assert(values != null); + + var context = new TemplateBindingContext(defaults, values); + + // Find out which entries in the URI are valid for the URI we want to generate. + // If the URI had ordered parameters a="1", b="2", c="3" and the new values + // specified that b="9", then we need to invalidate everything after it. The new + // values should then be a="1", b="9", c=. + for (var i = 0; i < Template.Parameters.Count; i++) + { + var parameter = Template.Parameters[i]; + + // If it's a parameter subsegment, examine the current value to see if it matches the new value + var parameterName = parameter.Name; + + object newParameterValue; + var hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue); + if (hasNewParameterValue) + { + context.Use(parameterName); + } + + object currentParameterValue = null; + var hasCurrentParameterValue = ambientValues != null && ambientValues.TryGetValue(parameterName, out currentParameterValue); + + if (hasNewParameterValue && hasCurrentParameterValue) + { + if (!RoutePartsEqual(currentParameterValue, newParameterValue)) + { + // Stop copying current values when we find one that doesn't match + break; + } + } + + // If the parameter is a match, add it to the list of values we will use for URI generation + if (hasNewParameterValue) + { + if (IsRoutePartNonEmpty(newParameterValue)) + { + context.Accept(parameterName, newParameterValue); + } + } + else + { + if (hasCurrentParameterValue) + { + context.Accept(parameterName, currentParameterValue); + } + } + }; + + // Add all remaining new values to the list of values we will use for URI generation + foreach (var kvp in values) + { + if (IsRoutePartNonEmpty(kvp.Value)) + { + context.Accept(kvp.Key, kvp.Value); + } + } + + // Add all current values that aren't in the URI at all + if (ambientValues != null) + { + foreach (var kvp in ambientValues) + { + var parameter = GetParameter(kvp.Key); + if (parameter == null) + { + context.Accept(kvp.Key, kvp.Value); + } + } + } + + // Accept all remaining default values if they match a required parameter + for (int i = 0; i < Template.Parameters.Count; i++) + { + var parameter = Template.Parameters[i]; + if (parameter.IsOptional || parameter.IsCatchAll) + { + continue; + } + + if (context.NeedsValue(parameter.Name)) + { + // Add the default value only if there isn't already a new value for it and + // only if it actually has a default value, which we determine based on whether + // the parameter value is required. + context.AcceptDefault(parameter.Name); + } + } + + // Validate that all required parameters have a value. + for (var i = 0; i < Template.Parameters.Count; i++) + { + var parameter = Template.Parameters[i]; + if (parameter.IsOptional || parameter.IsCatchAll) + { + continue; + } + + if (!context.AcceptedValues.ContainsKey(parameter.Name)) + { + // We don't have a value for this parameter, so we can't generate a url. + return null; + } + } + + // Any default values that don't appear as parameters are treated like filters. Any new values + // provided must match these defaults. + if (context.Filters != null) + { + foreach (var filter in context.Filters) + { + var parameter = GetParameter(filter.Key); + if (parameter != null) + { + continue; + } + + object value; + if (values.TryGetValue(filter.Key, out value)) + { + if (RoutePartsEqual(value, filter.Value)) + { + context.Use(filter.Key); + } + else + { + // If there is a non-parameterized value in the route and there is a + // new value for it and it doesn't match, this route won't match. + return null; + } + } + } + } + + return context; + } + + // Step 2: If the route is a match generate the appropriate URI + private BoundRouteTemplate BindValues(TemplateBindingContext bindingContext) + { + var context = new UriBuildingContext(); + + for (var i = 0; i < Template.Segments.Count; i++) + { + Contract.Assert(context.BufferState == SegmentState.Beginning); + Contract.Assert(context.UriState == SegmentState.Beginning); + + var segment = Template.Segments[i]; + + for (var j = 0; j < segment.Parts.Count; j++) + { + var part = segment.Parts[j]; + + if (part.IsLiteral) + { + if (!context.Accept(part.Text)) + { + return null; + } + } + else if (part.IsParameter) + { + // If it's a parameter, get its value + object value; + var hasValue = bindingContext.AcceptedValues.TryGetValue(part.Name, out value); + if (hasValue) + { + bindingContext.Use(part.Name); + } + + var converted = Convert.ToString(value, CultureInfo.InvariantCulture); + if (bindingContext.AcceptedDefaultValues.Contains(part.Name)) + { + // If the accepted value is the same as the default value buffer it since + // we won't necessarily add it to the URI we generate. + if (!context.Buffer(converted)) + { + return null; + } + } + else + { + if (!context.Accept(converted)) + { + return null; + } + } + } + } + + context.EndSegment(); + } + + // Encode the URI before we append the query string, otherwise we would double encode the query string + var encoded = new StringBuilder(); + encoded.Append(UriEncode(context.Build())); + + // Generate the query string + var firstParam = true; + foreach (var kvp in bindingContext.UnusedValues) + { + var converted = Convert.ToString(kvp.Value, CultureInfo.InvariantCulture); + if (String.IsNullOrEmpty(converted)) + { + continue; + } + + encoded.Append(firstParam ? '?' : '&'); + firstParam = false; + + encoded.Append(Uri.EscapeDataString(kvp.Key)); + encoded.Append('='); + encoded.Append(Uri.EscapeDataString(converted)); + } + + return new BoundRouteTemplate() + { + Path = encoded.ToString(), + }; + } + + private static string UriEncode(string str) + { + string escape = Uri.EscapeUriString(str); + return Regex.Replace(escape, "([#;?:@&=+$,])", EscapeReservedCharacters); + } + + private static string EscapeReservedCharacters(Match m) + { + return "%" + Convert.ToUInt16(m.Value[0]).ToString("x2", CultureInfo.InvariantCulture); + } + + private TemplatePart GetParameter(string name) + { + for (int i = 0; i < Template.Parameters.Count; i++) + { + var parameter = Template.Parameters[i]; + if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase)) + { + return parameter; + } + } + + return null; + } + + private static bool RoutePartsEqual(object a, object b) + { + string sa = a as string; + string sb = b as string; + + if (sa != null && sb != null) + { + // For strings do a case-insensitive comparison + return string.Equals(sa, sb, StringComparison.OrdinalIgnoreCase); + } + else + { + if (a != null && b != null) + { + // Explicitly call .Equals() in case it is overridden in the type + return a.Equals(b); + } + else + { + // At least one of them is null. Return true if they both are + return a == b; + } + } + } + + private static bool IsRoutePartNonEmpty(object routePart) + { + var routePartString = routePart as string; + if (routePartString == null) + { + return routePart != null; + } + else + { + return routePartString.Length > 0; + } + } + + [DebuggerDisplay("{DebuggerToString(),nq}")] + private class TemplateBindingContext + { + private readonly IDictionary _defaults; + + private readonly Dictionary _acceptedValues; + private readonly HashSet _acceptedDefaultValues; + private readonly Dictionary _unusedValues; + private readonly Dictionary _filters; + + public TemplateBindingContext(IDictionary defaults, IDictionary values) + { + if (values == null) + { + throw new ArgumentNullException("values"); + } + + _defaults = defaults; + + _acceptedValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + _acceptedDefaultValues = new HashSet(StringComparer.OrdinalIgnoreCase); + _unusedValues = new Dictionary(values, StringComparer.OrdinalIgnoreCase); + + if (_defaults != null) + { + _filters = new Dictionary(defaults, StringComparer.OrdinalIgnoreCase); + } + } + + public Dictionary AcceptedValues + { + get { return _acceptedValues; } + } + + /// + /// These are values that are equivalent to the default. These aren't written to the url unless + /// necessary. + /// > + public HashSet AcceptedDefaultValues + { + get { return _acceptedDefaultValues; } + } + + public Dictionary UnusedValues + { + get { return _unusedValues; } + } + + public Dictionary Filters + { + get { return _filters; } + } + + public void Accept(string key, object value) + { + if (!_acceptedValues.ContainsKey(key)) + { + _acceptedValues.Add(key, value); + + object defaultValue; + if (_defaults != null && _defaults.TryGetValue(key, out defaultValue)) + { + if (RoutePartsEqual(value, defaultValue)) + { + _acceptedDefaultValues.Add(key); + } + } + } + } + + public void AcceptDefault(string key) + { + Contract.Assert(!_acceptedValues.ContainsKey(key)); + + object value; + if (_defaults != null && _defaults.TryGetValue(key, out value)) + { + _filters.Remove(key); + _acceptedValues.Add(key, value); + + _acceptedDefaultValues.Add(key); + } + } + + public bool NeedsValue(string key) + { + return !_acceptedValues.ContainsKey(key); + } + + public void Use(string key) + { + _unusedValues.Remove(key); + } + + private string DebuggerToString() + { + return string.Format( + "{{Accepted: '{0}' Filters: '{1}'}}", + string.Join(", ", _acceptedValues.Keys), + string.Join(", ", _filters.Keys)); + } + } + + [DebuggerDisplay("{DebuggerToString(),nq}")] + private class UriBuildingContext + { + // Holds the 'accepted' parts of the uri. + private readonly StringBuilder _uri; + + // Holds the 'optional' parts of the uri. We need a secondary buffer to handle cases where an optional + // segment is in the middle of the uri. We don't know whether or not we need to write it out - if it's + // followed by other optional segments than we will just throw it away. + private readonly StringBuilder _buffer; + + private bool _hasEmptySegment; + + public UriBuildingContext() + { + _uri = new StringBuilder(); + _buffer = new StringBuilder(); + + BufferState = SegmentState.Beginning; + UriState = SegmentState.Beginning; + + } + + public SegmentState BufferState { get; private set; } + + public SegmentState UriState { get; private set; } + + public bool Accept(string value) + { + if (string.IsNullOrEmpty(value)) + { + if (UriState == SegmentState.Inside || BufferState == SegmentState.Inside) + { + // We can't write an 'empty' part inside a segment + return false; + } + else + { + _hasEmptySegment = true; + return true; + } + } + else if (_hasEmptySegment) + { + // We're trying to write text after an empty segment - this is not allowed. + return false; + } + + _uri.Append(_buffer); + _buffer.Clear(); + + if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) + { + if (_uri.Length != 0) + { + _uri.Append("/"); + } + } + + BufferState = SegmentState.Inside; + UriState = SegmentState.Inside; + + _uri.Append(value); + return true; + } + + public bool Buffer(string value) + { + if (string.IsNullOrEmpty(value)) + { + if (BufferState == SegmentState.Inside) + { + // We can't write an 'empty' part inside a segment + return false; + } + else + { + _hasEmptySegment = true; + return true; + } + } + else if (_hasEmptySegment) + { + // We're trying to write text after an empty segment - this is not allowed. + return false; + } + + if (UriState == SegmentState.Inside) + { + // We've already written part of this segment so there's no point in buffering, we need to + // write out the rest or give up. + var result = Accept(value); + + // We've already checked the conditions that could result in a rejected part, so this should + // always be true. + Contract.Assert(result); + + return result; + } + + if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) + { + if (_uri.Length != 0 || _buffer.Length != 0) + { + _buffer.Append("/"); + } + + BufferState = SegmentState.Inside; + } + + _buffer.Append(value); + return true; + } + + internal void EndSegment() + { + BufferState = SegmentState.Beginning; + UriState = SegmentState.Beginning; + } + + internal string Build() + { + // We can ignore any currently buffered segments - they are are guaranteed to be 'defaults'. + return _uri.ToString(); + } + + private string DebuggerToString() + { + return string.Format("{{Accepted: '{0}' Buffered: '{1}'}}", _uri.ToString(), _buffer.ToString()); + } + } + + // Segments are treated as all-or-none. We should never output a partial segment. + // If we add any subsegment of this segment to the generated URI, we have to add + // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we + // used a value for {p1}, we have to output the entire segment up to the next "/". + // Otherwise we could end up with the partial segment "v1" instead of the entire + // segment "v1-v2.xml". + private enum SegmentState + { + Beginning, + Inside, + } + } +} diff --git a/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs b/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs similarity index 93% rename from src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs rename to src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs index 94a32d38c9..05039dd6bb 100644 --- a/src/Microsoft.AspNet.Routing/Template/ParsedTemplate.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateMatcher.cs @@ -2,31 +2,28 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.Contracts; -using System.Linq; namespace Microsoft.AspNet.Routing.Template { - [DebuggerDisplay("{DebuggerToString()}")] - public class ParsedTemplate + public class TemplateMatcher { private const string SeparatorString = "/"; private const char SeparatorChar = '/'; private static readonly char[] Delimiters = new char[] { SeparatorChar }; - public ParsedTemplate(List segments) + public TemplateMatcher(Template template) { - if (segments == null) + if (template == null) { - throw new ArgumentNullException("segments"); + throw new ArgumentNullException("template"); } - Segments = segments; + Template = template; } - public List Segments { get; private set; } + public Template Template { get; private set; } public IDictionary Match(string requestPath, IDictionary defaults) { @@ -41,7 +38,7 @@ namespace Microsoft.AspNet.Routing.Template for (int i = 0; i < requestSegments.Length; i++) { - var routeSegment = Segments.Count > i ? Segments[i] : null; + var routeSegment = Template.Segments.Count > i ? Template.Segments[i] : null; var requestSegment = requestSegments[i]; if (routeSegment == null) @@ -64,7 +61,7 @@ namespace Microsoft.AspNet.Routing.Template return null; } } - else + else { Contract.Assert(part.IsParameter); @@ -118,11 +115,11 @@ namespace Microsoft.AspNet.Routing.Template } } - for (int i = requestSegments.Length; i < Segments.Count; i++) + for (int i = requestSegments.Length; i < Template.Segments.Count; i++) { // We've matched the request path so far, but still have remaining route segments. These need // to be all single-part parameter segments with default values or else they won't match. - var routeSegment = Segments[i]; + var routeSegment = Template.Segments[i]; if (routeSegment.Parts.Count > 1) { // If it has more than one part it must contain literals, so it can't match. @@ -297,9 +294,5 @@ namespace Microsoft.AspNet.Routing.Template return (lastIndex == 0) || routeSegment.Parts[0].IsParameter; } - private string DebuggerToString() - { - return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString())); - } } } diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs index 3a74597f93..c8920bdefe 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateParser.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNet.Routing.Template private const char EqualsSign = '='; private const char QuestionMark = '?'; - public static ParsedTemplate Parse(string routeTemplate) + public static Template Parse(string routeTemplate) { if (routeTemplate == null) { @@ -51,7 +51,7 @@ namespace Microsoft.AspNet.Routing.Template if (IsAllValid(context, segments)) { - return new ParsedTemplate(segments); + return new Template(segments); } else { diff --git a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs index 4eab348596..e64338d49d 100644 --- a/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs +++ b/src/Microsoft.AspNet.Routing/Template/TemplateRoute.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNet.Routing.Template { private readonly IDictionary _defaults; private readonly IRouteEndpoint _endpoint; - private readonly ParsedTemplate _parsedRoute; + private readonly Template _parsedTemplate; private readonly string _routeTemplate; public TemplateRoute(IRouteEndpoint endpoint, string routeTemplate) @@ -25,11 +25,11 @@ namespace Microsoft.AspNet.Routing.Template } _endpoint = endpoint; - _routeTemplate = routeTemplate == null ? String.Empty : routeTemplate; + _routeTemplate = routeTemplate ?? String.Empty; _defaults = defaults ?? new Dictionary(StringComparer.OrdinalIgnoreCase); // The parser will throw for invalid routes. - _parsedRoute = TemplateParser.Parse(RouteTemplate); + _parsedTemplate = TemplateParser.Parse(RouteTemplate); } public IDictionary Defaults @@ -60,7 +60,7 @@ namespace Microsoft.AspNet.Routing.Template requestPath = requestPath.Substring(1); } - IDictionary values = _parsedRoute.Match(requestPath, _defaults); + IDictionary values = _parsedTemplate.Match(requestPath, _defaults); if (values == null) { // If we got back a null value set, that means the URI did not match diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs b/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs index 927b39a679..c5c02593b2 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/Assert.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Microsoft.AspNet.Routing.Template.Tests { diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/RouteValueDictionary.cs b/test/Microsoft.AspNet.Routing.Tests/Template/RouteValueDictionary.cs new file mode 100644 index 0000000000..6a9a9a1f6b --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/RouteValueDictionary.cs @@ -0,0 +1,28 @@ + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.AspNet.Routing.Template.Tests +{ + // This is just a placeholder + public class RouteValueDictionary : Dictionary + { + public RouteValueDictionary() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + public RouteValueDictionary(object obj) + : base(StringComparer.OrdinalIgnoreCase) + { + if (obj != null) + { + foreach (var property in obj.GetType().GetTypeInfo().GetProperties()) + { + Add(property.Name, property.GetValue(obj)); + } + } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs new file mode 100644 index 0000000000..218478d15e --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateBinderTests.cs @@ -0,0 +1,1056 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using Xunit.Extensions; + +namespace Microsoft.AspNet.Routing.Template.Tests +{ + public class TemplateBinderTests + { + public static IEnumerable EmptyAndNullDefaultValues + { + get + { + return new object[][] + { + new object[] + { + "Test/{val1}/{val2}", + new RouteValueDictionary(new {val1 = "", val2 = ""}), + new RouteValueDictionary(new {val2 = "SomeVal2"}), + null, + }, + new object[] + { + "Test/{val1}/{val2}", + new RouteValueDictionary(new {val1 = "", val2 = ""}), + new RouteValueDictionary(new {val1 = "a"}), + "Test/a" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "", val3 = ""}), + new RouteValueDictionary(new {val2 = "a"}), + null + }, + new object[] + { + "Test/{val1}/{val2}", + new RouteValueDictionary(new {val1 = "", val2 = ""}), + new RouteValueDictionary(new {val1 = "a", val2 = "b"}), + "Test/a/b" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "", val2 = "", val3 = ""}), + new RouteValueDictionary(new {val1 = "a", val2 = "b", val3 = "c"}), + "Test/a/b/c" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "", val2 = "", val3 = ""}), + new RouteValueDictionary(new {val1 = "a", val2 = "b"}), + "Test/a/b" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "", val2 = "", val3 = ""}), + new RouteValueDictionary(new {val1 = "a"}), + "Test/a" + }, + new object[] + { + "Test/{val1}", + new RouteValueDictionary(new {val1 = "42", val2 = "", val3 = ""}), + new RouteValueDictionary(), + "Test" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "42", val2 = (string)null, val3 = (string)null}), + new RouteValueDictionary(), + "Test" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}/{val4}", + new RouteValueDictionary(new {val1 = "21", val2 = "", val3 = "", val4 = ""}), + new RouteValueDictionary(new {val1 = "42", val2 = "11", val3 = "", val4 = ""}), + "Test/42/11" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "21", val2 = "", val3 = ""}), + new RouteValueDictionary(new {val1 = "42"}), + "Test/42" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}/{val4}", + new RouteValueDictionary(new {val1 = "21", val2 = "", val3 = "", val4 = ""}), + new RouteValueDictionary(new {val1 = "42", val2 = "11"}), + "Test/42/11" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}", + new RouteValueDictionary(new {val1 = "21", val2 = (string)null, val3 = (string)null}), + new RouteValueDictionary(new {val1 = "42"}), + "Test/42" + }, + new object[] + { + "Test/{val1}/{val2}/{val3}/{val4}", + new RouteValueDictionary(new {val1 = "21", val2 = (string)null, val3 = (string)null, val4 = (string)null}), + new RouteValueDictionary(new {val1 = "42", val2 = "11"}), + "Test/42/11" + }, + }; + } + } + + [Theory] + [PropertyData("EmptyAndNullDefaultValues")] + public void Binding_WithEmptyAndNull_DefaultValues( + string template, + IDictionary defaults, + IDictionary values, + string expected) + { + // Arrange + var binder = new TemplateBinder(TemplateParser.Parse(template)); + + // Act + var boundTemplate = binder.Bind(defaults, null, values); + + // Assert + if (expected == null) + { + Assert.Null(boundTemplate); + } + else + { + Assert.NotNull(boundTemplate); + Assert.Equal(expected, boundTemplate.Path); + } + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}-{region}", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/xx-yy"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/xx-yya"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}-{region}", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/axx-yy"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "language/axx-yya"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch() + { + RunTest( + "language/a{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "", region = "yy" }), + null); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch2() + { + RunTest( + "language/a{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "" }), + null); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/xx"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/xx-"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/axx"); + } + + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}a", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "language/axxa"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentStandardMvcRouteMatches() + { + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { action = "Index", id = (string)null }), + new RouteValueDictionary(new { controller = "home", action = "list", id = (string)null }), + new RouteValueDictionary(new { controller = "products" }), + "products.mvc"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() + { + RunTest( + "language/{lang}-{region}", + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "zz" }), + "language/zz-yy"); + } + + [Fact] + public void GetUrlWithDefaultValue() + { + // URL should be found but excluding the 'id' parameter, which has only a default value. + RunTest( + "{controller}/{action}/{id}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { controller = "home", action = "oldaction" }), + new RouteValueDictionary(new { action = "newaction" }), + "home/newaction"); + } + + [Fact] + public void GetVirtualPathWithEmptyStringRequiredValueReturnsNull() + { + RunTest( + "foo/{controller}", + null, + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = "" }), + null); + } + + [Fact] + public void GetVirtualPathWithNullRequiredValueReturnsNull() + { + RunTest( + "foo/{controller}", + null, + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = (string)null }), + null); + } + + [Fact] + public void GetVirtualPathWithRequiredValueReturnsPath() + { + RunTest( + "foo/{controller}", + null, + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = "home" }), + "foo/home"); + } + + [Fact] + public void GetUrlWithNullDefaultValue() + { + // URL should be found but excluding the 'id' parameter, which has only a default value. + RunTest( + "{controller}/{action}/{id}", + new RouteValueDictionary(new { id = (string)null }), + new RouteValueDictionary(new { controller = "home", action = "oldaction", id = (string)null }), + new RouteValueDictionary(new { action = "newaction" }), + "home/newaction"); + } + + [Fact] + public void GetVirtualPathCanFillInSeparatedParametersWithDefaultValues() + { + RunTest( + "{controller}/{language}-{locale}", + new RouteValueDictionary(new { language = "en", locale = "US" }), + new RouteValueDictionary(), + new RouteValueDictionary(new { controller = "Orders" }), + "Orders/en-US"); + } + + [Fact] + public void GetVirtualPathWithUnusedNullValueShouldGenerateUrlAndIgnoreNullValue() + { + // DevDiv Bugs 194371: UrlRouting: Exception thrown when generating URL that has some null values + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { action = "Index", id = "" }), + new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }), + new RouteValueDictionary(new { controller = "Home", action = "TestAction", id = "1", format = (string)null }), + "Home.mvc/TestAction/1"); + } + + [Fact] + public void GetUrlWithMissingValuesDoesntMatch() + { + RunTest( + "{controller}/{action}/{id}", + null, + new { controller = "home", action = "oldaction" }, + new { action = "newaction" }, + null); + } + + [Fact] + public void GetUrlWithEmptyRequiredValuesReturnsNull() + { + RunTest( + "{p1}/{p2}/{p3}", + null, + new { p1 = "v1", }, + new { p2 = "", p3 = "" }, + null); + } + + [Fact] + public void GetUrlWithEmptyOptionalValuesReturnsShortUrl() + { + RunTest( + "{p1}/{p2}/{p3}", + new { p2 = "d2", p3 = "d3" }, + new { p1 = "v1", }, + new { p2 = "", p3 = "" }, + "v1"); + } + + [Fact] + public void GetUrlShouldIgnoreValuesAfterChangedParameter() + { + // DevDiv Bugs 157535 + RunTest( + "{controller}/{action}/{id}", + new { action = "Index", id = (string)null }, + new { controller = "orig", action = "init", id = "123" }, + new { action = "new", }, + "orig/new"); + } + + [Fact] + public void GetUrlWithNullForMiddleParameterIgnoresRemainingParameters() + { + // DevDiv Bugs 170859: UrlRouting: Passing null or empty string for a parameter in the middle of a route generates the wrong Url + RunTest( + "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", + new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" }, + new { controller = "UrlRouting", action = "Play", category = "Photos", year = "2008", occasion = "Easter", SafeParam = "SafeParamValue" }, + new { year = (string)null, occasion = "Hola" }, + "UrlGeneration1/UrlRouting.mvc/Play/Photos/1995/Hola"); + } + + [Fact] + public void GetUrlWithEmptyStringForMiddleParameterIgnoresRemainingParameters() + { + // DevDiv Bugs 170859: UrlRouting: Passing null or empty string for a parameter in the middle of a route generates the wrong Url + var ambientValues = new RouteValueDictionary(); + ambientValues.Add("controller", "UrlRouting"); + ambientValues.Add("action", "Play"); + ambientValues.Add("category", "Photos"); + ambientValues.Add("year", "2008"); + ambientValues.Add("occasion", "Easter"); + ambientValues.Add("SafeParam", "SafeParamValue"); + + var values = new RouteValueDictionary(); + values.Add("year", String.Empty); + values.Add("occasion", "Hola"); + + RunTest( + "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", + new RouteValueDictionary(new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" }), + ambientValues, + values, + "UrlGeneration1/UrlRouting.mvc/Play/Photos/1995/Hola"); + } + + [Fact] + public void GetUrlWithEmptyStringForMiddleParameterShouldUseDefaultValue() + { + // DevDiv Bugs 172084: UrlRouting: Route.GetUrl generates the wrong route of new values has a different controller and route has an action parameter with default + var ambientValues = new RouteValueDictionary(); + ambientValues.Add("Controller", "Test"); + ambientValues.Add("Action", "Fallback"); + ambientValues.Add("param1", "fallback1"); + ambientValues.Add("param2", "fallback2"); + ambientValues.Add("param3", "fallback3"); + + var values = new RouteValueDictionary(); + values.Add("controller", "subtest"); + values.Add("param1", "b"); + // The original bug for this included this value, but with the new support for + // creating query string values it changes the behavior such that the URL is + // not what was originally expected. To preserve the general behavior of this + // unit test the 'param2' value is no longer being added. + //values.Add("param2", "a"); + + RunTest( + "{controller}.mvc/{action}/{param1}", + new RouteValueDictionary(new { action = "Default" }), + ambientValues, + values, + "subtest.mvc/Default/b"); + } + + [Fact] + public void GetUrlVerifyEncoding() + { + var values = new RouteValueDictionary(); + values.Add("controller", "#;?:@&=+$,"); + values.Add("action", "showcategory"); + values.Add("id", 123); + values.Add("so?rt", "de?sc"); + values.Add("maxPrice", 100); + + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home" }), + new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }), + values, + "%23%3b%3f%3a%40%26%3d%2b%24%2c.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100"); + } + + [Fact] + public void GetUrlGeneratesQueryStringForNewValuesAndEscapesQueryString() + { + var values = new RouteValueDictionary(new { controller = "products", action = "showcategory", id = 123, maxPrice = 100 }); + values.Add("so?rt", "de?sc"); + + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home" }), + new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }), + values, + "products.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100"); + } + + [Fact] + public void GetUrlGeneratesQueryStringForNewValuesButIgnoresNewValuesThatMatchDefaults() + { + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home", Custom = "customValue" }), + new RouteValueDictionary(new { controller = "Home", action = "Index", id = (string)null }), + new RouteValueDictionary(new { controller = "products", action = "showcategory", id = 123, sort = "desc", maxPrice = 100, custom = "customValue" }), + "products.mvc/showcategory/123?sort=desc&maxPrice=100"); + } + + [Fact] + public void GetVirtualPathEncodesParametersAndLiterals() + { + RunTest( + "bl%og/{controller}/he llo/{action}", + null, + new RouteValueDictionary(new { controller = "ho%me", action = "li st" }), + new RouteValueDictionary(), + "bl%25og/ho%25me/he%20llo/li%20st"); + } + + [Fact] + public void GetUrlWithCatchAllWithValue() + { + RunTest( + "{p1}/{*p2}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { p1 = "v1" }), + new RouteValueDictionary(new { p2 = "v2a/v2b" }), + "v1/v2a/v2b"); + } + + [Fact] + public void GetUrlWithCatchAllWithEmptyValue() + { + RunTest( + "{p1}/{*p2}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { p1 = "v1" }), + new RouteValueDictionary(new { p2 = "" }), + "v1"); + } + + [Fact] + public void GetUrlWithCatchAllWithNullValue() + { + RunTest( + "{p1}/{*p2}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { p1 = "v1" }), + new RouteValueDictionary(new { p2 = (string)null }), + "v1"); + } + +#if ROUTE_COLLECTION + + [Fact] + public void GetUrlShouldValidateOnlyAcceptedParametersAndUserDefaultValuesForInvalidatedParameters() + { + // DevDiv Bugs 172913: UrlRouting: Parameter validation should not run against current request values if a new value has been supplied at a previous position + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("Controller", "UrlRouting"); + rd.Values.Add("Name", "MissmatchedValidateParams"); + rd.Values.Add("action", "MissmatchedValidateParameters2"); + rd.Values.Add("ValidateParam1", "special1"); + rd.Values.Add("ValidateParam2", "special2"); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}", + new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams", ValidateParam2 = "valid" }), + new RouteValueDictionary(new { ValidateParam1 = "valid.*", ValidateParam2 = "valid.*" }))); + + rc.Add(CreateRoute( + "UrlConstraints/Validation.mvc/Input5/{action}/{ValidateParam1}/{ValidateParam2}", + new RouteValueDictionary(new { Controller = "UrlRouting", Name = "MissmatchedValidateParams" }), + new RouteValueDictionary(new { ValidateParam1 = "special.*", ValidateParam2 = "special.*" }))); + + var values = CreateRouteValueDictionary(); + values.Add("Name", "MissmatchedValidateParams"); + values.Add("ValidateParam1", "valid1"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/UrlConstraints/Validation.mvc/Input5/MissmatchedValidateParameters2/valid1", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithRouteThatHasExtensionWithSubsequentDefaultValueIncludesExtensionButNotDefaultValue() + { + // DevDiv Bugs 156606 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bank"); + rd.Values.Add("action", "MakeDeposit"); + rd.Values.Add("accountId", "7770"); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "{controller}.mvc/Deposit/{accountId}", + new RouteValueDictionary(new { Action = "DepositView" }))); + + // Note: This route was in the original bug, but it turns out that this behavior is incorrect. With the + // recent fix to Route (in this changelist) this route would have been selected since we have values for + // all three required parameters. + //rc.Add(new Route { + // Url = "{controller}.mvc/{action}/{accountId}", + // RouteHandler = new DummyRouteHandler() + //}); + + // This route should be chosen because the requested action is List. Since the default value of the action + // is List then the Action should not be in the URL. However, the file extension should be included since + // it is considered "safe." + rc.Add(CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { Action = "List" }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "List"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Bank.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithRouteThatHasDifferentControllerCaseShouldStillMatch() + { + // DevDiv Bugs 159099 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bar"); + rd.Values.Add("action", "bbb"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute("PrettyFooUrl", new RouteValueDictionary(new { controller = "Foo", action = "aaa", id = (string)null }))); + + rc.Add(CreateRoute("PrettyBarUrl", new RouteValueDictionary(new { controller = "Bar", action = "bbb", id = (string)null }))); + + rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "aaa"); + values.Add("Controller", "foo"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/PrettyFooUrl", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithNoChangedValuesShouldProduceSameUrl() + { + // DevDiv Bugs 159469 + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute("{controller}.mvc/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + rc.Add(CreateRoute("{controller}/{action}/{id}", new RouteValueDictionary(new { action = "Index", id = (string)null }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "Index"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Home.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlAppliesConstraintsRulesToChooseRoute() + { + // DevDiv Bugs 159678: MVC: URL generation chooses the wrong route for generating URLs when route validation is in place + + // Arrange + var rd = CreateRouteData(); + rd.Values.Add("controller", "Home"); + rd.Values.Add("action", "Index"); + rd.Values.Add("id", null); + + IRouteCollection rc = new DefaultRouteCollection(); + rc.Add(CreateRoute( + "foo.mvc/{action}", + new RouteValueDictionary(new { controller = "Home" }), + new RouteValueDictionary(new { controller = "Home", action = "Contact", httpMethod = CreateHttpMethodConstraint("get") }))); + + rc.Add(CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { action = "Index" }), + new RouteValueDictionary(new { controller = "Home", action = "(Index|About)", httpMethod = CreateHttpMethodConstraint("post") }))); + + var values = CreateRouteValueDictionary(); + values.Add("Action", "Index"); + + // Act + var vpd = rc.GetVirtualPath(GetHttpContext("/app1", "", ""), values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app1/Home.mvc", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrentRoute() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + IRouteCollection rt = new DefaultRouteCollection(); + rt.Add(CreateRoute("date/{y}/{m}/{d}", null)); + rt.Add(CreateRoute("{controller}/{action}/{id}", null)); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "dostuff"); + + var values = CreateRouteValueDictionary(); + values.Add("y", "2007"); + values.Add("m", "08"); + values.Add("d", "12"); + + // Act + var vpd = rt.GetVirtualPath(context, values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app/date/2007/08/12", vpd.VirtualPath); + } + + [Fact] + public void GetUrlWithValuesThatAreCompletelyDifferentFromTheCurrentRouteAsSecondRoute() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + + IRouteCollection rt = new DefaultRouteCollection(); + rt.Add(CreateRoute("{controller}/{action}/{id}")); + rt.Add(CreateRoute("date/{y}/{m}/{d}")); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "dostuff"); + + var values = CreateRouteValueDictionary(); + values.Add("y", "2007"); + values.Add("m", "08"); + values.Add("d", "12"); + + // Act + var vpd = rt.GetVirtualPath(context, values); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("/app/date/2007/08/12", vpd.VirtualPath); + } + + [Fact] + public void GetVirtualPathUsesCurrentValuesNotInRouteToMatch() + { + // DevDiv Bugs 177401: UrlRouting: Incorrect route picked on urlgeneration if using controller from ambient values and route does not have a url parameter for controller + + // DevDiv Bugs 191162: UrlRouting: Route does not match when an ambient route value doesn't match a required default value in the target route + // Because of this bug the test was split into two separate verifications since the original test was verifying slightly incorrect behavior + + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r1 = CreateRoute( + "ParameterMatching.mvc/{Action}/{product}", + new RouteValueDictionary(new { Controller = "ParameterMatching", product = (string)null }), + null); + + TemplateRoute r2 = CreateRoute( + "{controller}.mvc/{action}", + new RouteValueDictionary(new { Action = "List" }), + new RouteValueDictionary(new { Controller = "Action|Bank|Overridden|DerivedFromAction|OverrideInvokeActionAndExecute|InvalidControllerName|Store|HtmlHelpers|(T|t)est|UrlHelpers|Custom|Parent|Child|TempData|ViewFactory|LocatingViews|AccessingDataInViews|ViewOverrides|ViewMasterPage|InlineCompileError|CustomView" }), + null); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "Bank"); + rd.Values.Add("Action", "List"); + var valuesDictionary = CreateRouteValueDictionary(); + valuesDictionary.Add("action", "AttemptLogin"); + + // Act for first route + var vpd = r1.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("ParameterMatching.mvc/AttemptLogin", vpd.VirtualPath); + + // Act for second route + vpd = r2.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("Bank.mvc/AttemptLogin", vpd.VirtualPath); + } + +#endif + +#if DATA_TOKENS + [Fact] + public void GetVirtualPathWithDataTokensCopiesThemFromRouteToVirtualPathData() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + TemplateRoute r = CreateRoute("{controller}/{action}", null, null, new RouteValueDictionary(new { foo = "bar", qux = "quux" })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "index"); + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("home/index", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + Assert.Equal(2, vpd.DataTokens.Count); + Assert.Equal("bar", vpd.DataTokens["foo"]); + Assert.Equal("quux", vpd.DataTokens["qux"]); + } +#endif + +#if ROUTE_FORMAT_HELPER + + [Fact] + public void UrlWithEscapedOpenCloseBraces() + { + RouteFormatHelper("foo/{{p1}}", "foo/{p1}"); + } + + [Fact] + public void UrlWithEscapedOpenBraceAtTheEnd() + { + RouteFormatHelper("bar{{", "bar{"); + } + + [Fact] + public void UrlWithEscapedOpenBraceAtTheBeginning() + { + RouteFormatHelper("{{bar", "{bar"); + } + + [Fact] + public void UrlWithRepeatedEscapedOpenBrace() + { + RouteFormatHelper("foo{{{{bar", "foo{{bar"); + } + + [Fact] + public void UrlWithEscapedCloseBraceAtTheEnd() + { + RouteFormatHelper("bar}}", "bar}"); + } + + [Fact] + public void UrlWithEscapedCloseBraceAtTheBeginning() + { + RouteFormatHelper("}}bar", "}bar"); + } + + [Fact] + public void UrlWithRepeatedEscapedCloseBrace() + { + RouteFormatHelper("foo}}}}bar", "foo}}bar"); + } + + private static void RouteFormatHelper(string routeUrl, string requestUrl) + { + var defaults = new RouteValueDictionary(new { route = "matched" }); + var r = CreateRoute(routeUrl, defaults, null); + + GetRouteDataHelper(r, requestUrl, defaults); + GetVirtualPathHelper(r, new RouteValueDictionary(), null, Uri.EscapeUriString(requestUrl)); + } + +#endif + +#if CONSTRAINTS + [Fact] + public void GetVirtualPathWithNonParameterConstraintReturnsUrlWithoutQueryString() + { + // DevDiv Bugs 199612: UrlRouting: UrlGeneration should not append parameter to query string if it is a Constraint parameter and not a Url parameter + RunTest( + "{Controller}.mvc/{action}/{end}", + null, + new RouteValueDictionary(new { foo = CreateHttpMethodConstraint("GET") }), + new RouteValueDictionary(), + new RouteValueDictionary(new { controller = "Orders", action = "Index", end = "end", foo = "GET" }), + "Orders.mvc/Index/end"); + } + + [Fact] + public void GetVirtualPathWithValidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "index"); + + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.NotNull(vpd); + Assert.Equal("home/index", vpd.VirtualPath); + Assert.Equal(r, vpd.Route); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("index", r.ConstraintData.ParameterValue); + } + + [Fact] + public void GetVirtualPathWithInvalidCustomConstraints() + { + // Arrange + HttpContext context = GetHttpContext("/app", null, null); + CustomConstraintTemplateRoute r = new CustomConstraintTemplateRoute("{controller}/{action}", null, new RouteValueDictionary(new { action = 5 })); + + var rd = CreateRouteData(); + rd.Values.Add("controller", "home"); + rd.Values.Add("action", "list"); + + var valuesDictionary = CreateRouteValueDictionary(); + + // Act + var vpd = r.GetVirtualPath(context, valuesDictionary); + + // Assert + Assert.Null(vpd); + Assert.NotNull(r.ConstraintData); + Assert.Equal(5, r.ConstraintData.Constraint); + Assert.Equal("action", r.ConstraintData.ParameterName); + Assert.Equal("list", r.ConstraintData.ParameterValue); + } + +#endif + + private static void RunTest( + string template, + IDictionary defaults, + IDictionary ambientValues, + IDictionary values, + string expected) + { + // Arrange + var binder = new TemplateBinder(TemplateParser.Parse(template)); + + // Act + var boundTemplate = binder.Bind(defaults, ambientValues, values); + + // Assert + if (expected == null) + { + Assert.Null(boundTemplate); + } + else + { + Assert.NotNull(boundTemplate); + + // We want to chop off the query string and compare that using an unordered comparison + var expectedParts = new PathAndQuery(expected); + var actualParts = new PathAndQuery(boundTemplate.Path); + + Assert.Equal(expectedParts.Path, actualParts.Path); + + if (expectedParts.Parameters == null) + { + Assert.Null(actualParts.Parameters); + } + else + { + Assert.Equal(expectedParts.Parameters.Count, actualParts.Parameters.Count); + + foreach (var kvp in expectedParts.Parameters) + { + string value; + Assert.True(actualParts.Parameters.TryGetValue(kvp.Key, out value)); + Assert.Equal(kvp.Value, value); + } + } + } + } + + private static void RunTest( + string template, + object defaults, + object ambientValues, + object values, + string expected) + { + RunTest( + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(ambientValues), + new RouteValueDictionary(values), + expected); + } + + private class PathAndQuery + { + public PathAndQuery(string uri) + { + var queryIndex = uri.IndexOf("?", StringComparison.Ordinal); + if (queryIndex == -1) + { + Path = uri; + } + else + { + Path = uri.Substring(0, queryIndex); + + var query = uri.Substring(queryIndex + 1); + Parameters = + query + .Split(new char[] { '&' }, StringSplitOptions.None) + .Select(s => s.Split(new char[] { '=' }, StringSplitOptions.None)) + .ToDictionary(pair => pair[0], pair => pair[1]); + } + } + + public string Path { get; private set; } + + public Dictionary Parameters { get; private set; } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs new file mode 100644 index 0000000000..b445e31e8c --- /dev/null +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateMatcherTests.cs @@ -0,0 +1,783 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Xunit; +using Xunit.Extensions; + +namespace Microsoft.AspNet.Routing.Template.Tests +{ + public class TemplateMatcherTests + { + [Fact] + public void MatchSingleRoute() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); + + // Act + var match = matcher.Match("Bank/DoAction/123", null); + + // Assert + Assert.NotNull(match); + Assert.Equal("Bank", match["controller"]); + Assert.Equal("DoAction", match["action"]); + Assert.Equal("123", match["id"]); + } + + [Fact] + public void NoMatchSingleRoute() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); + + // Act + var match = matcher.Match("Bank/DoAction", null); + + // Assert + Assert.Null(match); + } + + [Fact] + public void MatchSingleRouteWithDefaults() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); + + // Act + var rd = matcher.Match("Bank/DoAction", new RouteValueDictionary(new { id = "default id" })); + + // Assert + Assert.Equal("Bank", rd["controller"]); + Assert.Equal("DoAction", rd["action"]); + Assert.Equal("default id", rd["id"]); + } + + [Fact] + public void NoMatchSingleRouteWithDefaults() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); + + // Act + var rd = matcher.Match("Bank", new RouteValueDictionary(new { id = "default id" })); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchRouteWithLiterals() + { + // Arrange + var matcher = CreateMatcher("moo/{p1}/bar/{p2}"); + + // Act + var rd = matcher.Match("moo/111/bar/222", new RouteValueDictionary(new { p2 = "default p2" })); + + // Assert + Assert.Equal("111", rd["p1"]); + Assert.Equal("222", rd["p2"]); + } + + [Fact] + public void MatchRouteWithLiteralsAndDefaults() + { + // Arrange + var matcher = CreateMatcher("moo/{p1}/bar/{p2}"); + + // Act + var rd = matcher.Match("moo/111/bar/", new RouteValueDictionary(new { p2 = "default p2" })); + + // Assert + Assert.Equal("111", rd["p1"]); + Assert.Equal("default p2", rd["p2"]); + } + + [Fact] + public void MatchRouteWithOnlyLiterals() + { + // Arrange + var matcher = CreateMatcher("moo/bar"); + + // Act + var rd = matcher.Match("moo/bar", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal(0, rd.Count); + } + + [Fact] + public void NoMatchRouteWithOnlyLiterals() + { + // Arrange + var matcher = CreateMatcher("moo/bars"); + + // Act + var rd = matcher.Match("moo/bar", null); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchRouteWithExtraSeparators() + { + // Arrange + var matcher = CreateMatcher("moo/bar"); + + // Act + var rd = matcher.Match("moo/bar/", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal(0, rd.Count); + } + + [Fact] + public void MatchRouteUrlWithExtraSeparators() + { + // Arrange + var matcher = CreateMatcher("moo/bar/"); + + // Act + var rd = matcher.Match("moo/bar", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal(0, rd.Count); + } + + [Fact] + public void MatchRouteUrlWithParametersAndExtraSeparators() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}/"); + + // Act + var rd = matcher.Match("moo/bar", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal("moo", rd["p1"]); + Assert.Equal("bar", rd["p2"]); + } + + [Fact] + public void NoMatchRouteUrlWithDifferentLiterals() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}/baz"); + + // Act + var rd = matcher.Match("moo/bar/boo", null); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void NoMatchLongerUrl() + { + // Arrange + var matcher = CreateMatcher("{p1}"); + + // Act + var rd = matcher.Match("moo/bar", null); + + // Assert + Assert.Null(rd); + } + + [Fact] + public void MatchSimpleFilename() + { + // Arrange + var matcher = CreateMatcher("DEFAULT.ASPX"); + + // Act + var rd = matcher.Match("default.aspx", null); + + // Assert + Assert.NotNull(rd); + } + + [Theory] + [InlineData("{prefix}x{suffix}", "xxxxxxxxxx")] + [InlineData("{prefix}xyz{suffix}", "xxxxyzxyzxxxxxxyz")] + [InlineData("{prefix}xyz{suffix}", "abcxxxxyzxyzxxxxxxyzxx")] + [InlineData("{prefix}xyz{suffix}", "xyzxyzxyzxyzxyz")] + [InlineData("{prefix}xyz{suffix}", "xyzxyzxyzxyzxyz1")] + [InlineData("{prefix}xyz{suffix}", "xyzxyzxyz")] + [InlineData("{prefix}aa{suffix}", "aaaaa")] + [InlineData("{prefix}aaa{suffix}", "aaaaa")] + public void VerifyRouteMatchesWithContext(string template, string path) + { + var matcher = CreateMatcher(template); + + // Act + var rd = matcher.Match(path, null); + + // Assert + Assert.NotNull(rd); + } + + [Fact] + public void MatchRouteWithExtraDefaultValues() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}"); + + // Act + var rd = matcher.Match("v1", new RouteValueDictionary(new { p2 = (string)null, foo = "bar" })); + + // Assert + Assert.NotNull(rd); + Assert.Equal(3, rd.Count); + Assert.Equal("v1", rd["p1"]); + Assert.Null(rd["p2"]); + Assert.Equal("bar", rd["foo"]); + } + + [Fact] + public void MatchPrettyRouteWithExtraDefaultValues() + { + // Arrange + var matcher = CreateMatcher("date/{y}/{m}/{d}"); + + // Act + var rd = matcher.Match("date/2007/08", new RouteValueDictionary(new { controller = "blog", action = "showpost", m = (string)null, d = (string)null })); + + // Assert + Assert.NotNull(rd); + Assert.Equal(5, rd.Count); + Assert.Equal("blog", rd["controller"]); + Assert.Equal("showpost", rd["action"]); + Assert.Equal("2007", rd["y"]); + Assert.Equal("08", rd["m"]); + Assert.Null(rd["d"]); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}-{region}", + "language/en-US", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-{region}a", + "language/en-USa", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}-{region}", + "language/aen-US", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}-{region}a", + "language/aen-USa", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnNeitherEndDoesNotMatch() + { + RunTest( + "language/a{lang}-{region}a", + "language/a-USa", + null, + null); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnNeitherEndDoesNotMatch2() + { + RunTest( + "language/a{lang}-{region}a", + "language/aen-a", + null, + null); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}", + "language/en", + null, + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnBothEndsTrailingSlashDoesNotMatch() + { + RunTest( + "language/{lang}", + "language/", + null, + null); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnBothEndsDoesNotMatch() + { + RunTest( + "language/{lang}", + "language", + null, + null); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-", + "language/en-", + null, + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}", + "language/aen", + null, + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithSimpleMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}a", + "language/aena", + null, + new RouteValueDictionary(new { lang = "en" })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentStandardMvcRouteMatches() + { + RunTest( + "{controller}.mvc/{action}/{id}", + "home.mvc/index", + new RouteValueDictionary(new { action = "Index", id = (string)null }), + new RouteValueDictionary(new { controller = "home", action = "index", id = (string)null })); + } + + [Fact] + public void GetRouteDataWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() + { + RunTest( + "language/{lang}-{region}", + "language/-", + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + null); + } + + [Fact] + public void GetRouteDataWithUrlWithMultiSegmentWithRepeatedDots() + { + RunTest( + "{Controller}..mvc/{id}/{Param1}", + "Home..mvc/123/p1", + null, + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } + + [Fact] + public void GetRouteDataWithUrlWithTwoRepeatedDots() + { + RunTest( + "{Controller}.mvc/../{action}", + "Home.mvc/../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithThreeRepeatedDots() + { + RunTest( + "{Controller}.mvc/.../{action}", + "Home.mvc/.../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithManyRepeatedDots() + { + RunTest( + "{Controller}.mvc/../../../{action}", + "Home.mvc/../../../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithExclamationPoint() + { + RunTest( + "{Controller}.mvc!/{action}", + "Home.mvc!/index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } + + [Fact] + public void GetRouteDataWithUrlWithStartingDotDotSlash() + { + RunTest( + "../{Controller}.mvc", + "../Home.mvc", + null, + new RouteValueDictionary(new { Controller = "Home" })); + } + + [Fact] + public void GetRouteDataWithUrlWithStartingBackslash() + { + RunTest( + @"\{Controller}.mvc", + @"\Home.mvc", + null, + new RouteValueDictionary(new { Controller = "Home" })); + } + + [Fact] + public void GetRouteDataWithUrlWithBackslashSeparators() + { + RunTest( + @"{Controller}.mvc\{id}\{Param1}", + @"Home.mvc\123\p1", + null, + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } + + [Fact] + public void GetRouteDataWithUrlWithParenthesesLiterals() + { + RunTest( + @"(Controller).mvc", + @"(Controller).mvc", + null, + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithUrlWithTrailingSlashSpace() + { + RunTest( + @"Controller.mvc/ ", + @"Controller.mvc/ ", + null, + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithUrlWithTrailingSpace() + { + RunTest( + @"Controller.mvc ", + @"Controller.mvc ", + null, + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithCatchAllCapturesDots() + { + // DevDiv Bugs 189892: UrlRouting: Catch all parameter cannot capture url segments that contain the "." + RunTest( + "Home/ShowPilot/{missionId}/{*name}", + "Home/ShowPilot/777/12345./foobar", + new RouteValueDictionary(new + { + controller = "Home", + action = "ShowPilot", + missionId = (string)null, + name = (string)null + }), + new RouteValueDictionary(new { controller = "Home", action = "ShowPilot", missionId = "777", name = "12345./foobar" })); + } + + [Fact] + public void RouteWithCatchAllClauseCapturesManySlashes() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + // Act + var rd = matcher.Match("v1/v2/v3", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Count); + Assert.Equal("v1", rd["p1"]); + Assert.Equal("v2/v3", rd["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseCapturesTrailingSlash() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + // Act + var rd = matcher.Match("v1/", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Count); + Assert.Equal("v1", rd["p1"]); + Assert.Null(rd["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseCapturesEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + // Act + var rd = matcher.Match("v1", null); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Count); + Assert.Equal("v1", rd["p1"]); + Assert.Null(rd["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseUsesDefaultValueForEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + // Act + var rd = matcher.Match("v1", new RouteValueDictionary(new { p2 = "catchall" })); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Count); + Assert.Equal("v1", rd["p1"]); + Assert.Equal("catchall", rd["p2"]); + } + + [Fact] + public void RouteWithCatchAllClauseIgnoresDefaultValueForNonEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); + + // Act + var rd = matcher.Match("v1/hello/whatever", new RouteValueDictionary(new { p2 = "catchall" })); + + // Assert + Assert.NotNull(rd); + Assert.Equal(2, rd.Count); + Assert.Equal("v1", rd["p1"]); + Assert.Equal("hello/whatever", rd["p2"]); + } + + [Fact] + public void GetRouteDataDoesNotMatchOnlyLeftLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "fooBAR", + null, + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchOnlyRightLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "BARfoo", + null, + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchMiddleLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "BARfooBAR", + null, + null); + } + + [Fact] + public void GetRouteDataDoesMatchesExactLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "foo", + null, + new RouteValueDictionary()); + } + + [Fact] + public void GetRouteDataWithWeirdParameterNames() + { + RunTest( + "foo/{ }/{.!$%}/{dynamic.data}/{op.tional}", + "foo/space/weird/orderid", + new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }, + new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weird" }, { "dynamic.data", "orderid" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }); + } + + [Fact] + public void GetRouteDataDoesNotMatchRouteWithLiteralSeparatorDefaultsButNoValue() + { + RunTest( + "{controller}/{language}-{locale}", + "foo", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchesRouteWithLiteralSeparatorDefaultsAndLeftValue() + { + RunTest( + "{controller}/{language}-{locale}", + "foo/xx-", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } + + [Fact] + public void GetRouteDataDoesNotMatchesRouteWithLiteralSeparatorDefaultsAndRightValue() + { + RunTest( + "{controller}/{language}-{locale}", + "foo/-yy", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } + + [Fact] + public void GetRouteDataMatchesRouteWithLiteralSeparatorDefaultsAndValue() + { + RunTest( + "{controller}/{language}-{locale}", + "foo/xx-yy", + new RouteValueDictionary(new { language = "en", locale = "US" }), + new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } }); + } + + [Fact] + public void MatchSetsOptionalParameter() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}"); + var url = "Home/Index"; + + // Act + var match = route.Match(url, null); + + // Assert + Assert.NotNull(match); + Assert.Equal(2, match.Values.Count); + Assert.Equal("Home", match["controller"]); + Assert.Equal("Index", match["action"]); + } + + [Fact] + public void MatchDoesNotSetOptionalParameter() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}"); + var url = "Home"; + + // Act + var match = route.Match(url, null); + + // Assert + Assert.NotNull(match); + Assert.Equal(1, match.Values.Count); + Assert.Equal("Home", match["controller"]); + Assert.False(match.ContainsKey("action")); + } + + [Fact] + public void MatchMultipleOptionalParameters() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}/{id?}"); + var url = "Home/Index"; + + // Act + var match = route.Match(url, null); + + // Assert + Assert.NotNull(match); + Assert.Equal(2, match.Values.Count); + Assert.Equal("Home", match["controller"]); + Assert.Equal("Index", match["action"]); + Assert.False(match.ContainsKey("id")); + } + + private TemplateMatcher CreateMatcher(string template) + { + return new TemplateMatcher(TemplateParser.Parse(template)); + } + + private static void RunTest(string template, string path, IDictionary defaults, IDictionary expected) + { + // Arrange + var matcher = new TemplateMatcher(TemplateParser.Parse(template)); + + // Act + var match = matcher.Match(path, defaults); + + // Assert + if (expected == null) + { + Assert.Null(match); + } + else + { + Assert.NotNull(match); + Assert.Equal(expected.Count, match.Values.Count); + foreach (string key in match.Keys) + { + Assert.Equal(expected[key], match[key]); + } + } + } + } +} diff --git a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs index 9140836284..24074722bb 100644 --- a/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs +++ b/test/Microsoft.AspNet.Routing.Tests/Template/TemplateParserTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests // Arrange var template = "cool"; - var expected = new ParsedTemplate(new List()); + var expected = new Template(new List()); expected.Segments.Add(new TemplateSegment()); expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); @@ -23,7 +23,7 @@ namespace Microsoft.AspNet.Routing.Template.Tests var actual = TemplateParser.Parse(template); // Assert - Assert.Equal(expected, actual, new TemplateParsedRouteEqualityComparer()); + Assert.Equal