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