From 84f4a2d047893ff9c141e93c83fb64e3c2fee6a9 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 29 Jan 2014 14:29:10 -0800 Subject: [PATCH] Initial mock up of key interfaces for routing. This is heavily based right now on raw Owin as the request/response model since that will work for building samples/tests, and doesn't require ifdefs in the code for k10. Once it's feasible, I'll port this to use the Microsoft.AspNet.Abstractions types. The sample here includes some basic route/endpoint implementations just to prove that the route engine works in naive scenarios. These might live on in the future in some form, but for now it's just part of the sample. The next step is to integrate this with WebFX and start developing the integration/configuration story for WebFX. --- samples/RoutingSample/OwinRouteEndpoint.cs | 24 +++++++++ samples/RoutingSample/PrefixRoute.cs | 54 +++++++++++++++++++ samples/RoutingSample/Program.cs | 22 ++++++++ samples/RoutingSample/Startup.cs | 35 ++++++++++++ samples/RoutingSample/project.json | 9 ++-- .../DefaultRouteCollection.cs | 26 +++++++++ .../DefaultRouteEngine.cs | 41 ++++++++++++++ src/Microsoft.AspNet.Routing/IRoute.cs | 9 ++++ .../IRouteCollection.cs | 19 +++++++ .../IRouteEndpoint.cs | 12 +++++ src/Microsoft.AspNet.Routing/IRouteEngine.cs | 12 +++++ .../Owin/AppBuilderExtensions.cs | 23 ++++++++ .../Owin/RouterMiddleware.cs | 41 ++++++++++++++ src/Microsoft.AspNet.Routing/RouteContext.cs | 28 ++++++++++ src/Microsoft.AspNet.Routing/RouteMatch.cs | 36 +++++++++++++ src/Microsoft.AspNet.Routing/project.json | 7 +-- 16 files changed, 392 insertions(+), 6 deletions(-) create mode 100644 samples/RoutingSample/OwinRouteEndpoint.cs create mode 100644 samples/RoutingSample/PrefixRoute.cs create mode 100644 samples/RoutingSample/Program.cs create mode 100644 samples/RoutingSample/Startup.cs create mode 100644 src/Microsoft.AspNet.Routing/DefaultRouteCollection.cs create mode 100644 src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs create mode 100644 src/Microsoft.AspNet.Routing/IRoute.cs create mode 100644 src/Microsoft.AspNet.Routing/IRouteCollection.cs create mode 100644 src/Microsoft.AspNet.Routing/IRouteEndpoint.cs create mode 100644 src/Microsoft.AspNet.Routing/IRouteEngine.cs create mode 100644 src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs create mode 100644 src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs create mode 100644 src/Microsoft.AspNet.Routing/RouteContext.cs create mode 100644 src/Microsoft.AspNet.Routing/RouteMatch.cs diff --git a/samples/RoutingSample/OwinRouteEndpoint.cs b/samples/RoutingSample/OwinRouteEndpoint.cs new file mode 100644 index 0000000000..f903bfee2f --- /dev/null +++ b/samples/RoutingSample/OwinRouteEndpoint.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.Routing; + +namespace RoutingSample +{ + internal class OwinRouteEndpoint : IRouteEndpoint + { + private readonly Func, Task> _appFunc; + + public OwinRouteEndpoint(Func, Task> appFunc) + { + _appFunc = appFunc; + } + + public Task Invoke(IDictionary context) + { + return _appFunc(context); + } + } +} diff --git a/samples/RoutingSample/PrefixRoute.cs b/samples/RoutingSample/PrefixRoute.cs new file mode 100644 index 0000000000..fe1f4f092a --- /dev/null +++ b/samples/RoutingSample/PrefixRoute.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Routing; + +namespace RoutingSample +{ + internal class PrefixRoute : IRoute + { + private readonly IRouteEndpoint _endpoint; + private readonly string _prefix; + + public PrefixRoute(IRouteEndpoint endpoint, string prefix) + { + _endpoint = endpoint; + + if (prefix == null) + { + prefix = "/"; + } + else if (prefix.Length > 0 && prefix[0] != '/') + { + // owin.RequestPath starts with a / + prefix = "/" + prefix; + } + + if (prefix.Length > 1 && prefix[prefix.Length - 1] == '/') + { + prefix = prefix.Substring(0, prefix.Length - 1); + } + + _prefix = prefix; + } + + public RouteMatch Match(RouteContext context) + { + if (context.RequestPath.StartsWith(_prefix, StringComparison.OrdinalIgnoreCase)) + { + if (context.RequestPath.Length > _prefix.Length) + { + char next = context.RequestPath[_prefix.Length]; + if (next != '/' && next != '#' && next != '?') + { + return null; + } + } + + return new RouteMatch(_endpoint); + } + + return null; + } + } +} diff --git a/samples/RoutingSample/Program.cs b/samples/RoutingSample/Program.cs new file mode 100644 index 0000000000..a5b500e9e1 --- /dev/null +++ b/samples/RoutingSample/Program.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.Owin.Hosting; + +namespace RoutingSample +{ + internal static class Program + { + public static void Main(string[] args) + { + string url = "http://localhost:30000/"; + using (WebApp.Start(url)) + { + Console.WriteLine("Listening on {0}", url); + Console.WriteLine("Press ENTER to quit"); + + Console.ReadLine(); + } + } + } +} diff --git a/samples/RoutingSample/Startup.cs b/samples/RoutingSample/Startup.cs new file mode 100644 index 0000000000..2f6f4d2d1a --- /dev/null +++ b/samples/RoutingSample/Startup.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Routing.Owin; +using Owin; + +namespace RoutingSample +{ + internal class Startup + { + public void Configuration(IAppBuilder appBuilder) + { + var routes = appBuilder.UseRouter(); + + OwinRouteEndpoint endpoint1 = new OwinRouteEndpoint(async (context) => await WriteToBodyAsync(context, "match1")); + OwinRouteEndpoint endpoint2 = new OwinRouteEndpoint(async (context) => await WriteToBodyAsync(context, "Hello, World!")); + + routes.Add(new PrefixRoute(endpoint1, "api/store")); + routes.Add(new PrefixRoute(endpoint1, "api/checkout")); + routes.Add(new PrefixRoute(endpoint2, "hello/world")); + routes.Add(new PrefixRoute(endpoint1, "")); + } + + private static async Task WriteToBodyAsync(IDictionary context, string text) + { + var stream = (Stream)context["owin.ResponseBody"]; + + byte[] bytes = Encoding.UTF8.GetBytes(text); + await stream.WriteAsync(bytes, 0, bytes.Length); + } + } +} diff --git a/samples/RoutingSample/project.json b/samples/RoutingSample/project.json index 5168efba28..71c3fc2226 100644 --- a/samples/RoutingSample/project.json +++ b/samples/RoutingSample/project.json @@ -5,9 +5,12 @@ }, "configurations": { "net45": { - "dependencies": { - "Owin": "1.0" - } + "dependencies": { + "Owin": "1.0", + "Microsoft.Owin" : "2.1.0", + "Microsoft.Owin.Host.HttpListener" : "2.1.0", + "Microsoft.Owin.Hosting" : "2.1.0" + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/DefaultRouteCollection.cs b/src/Microsoft.AspNet.Routing/DefaultRouteCollection.cs new file mode 100644 index 0000000000..81616ed126 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/DefaultRouteCollection.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public class DefaultRouteCollection : IRouteCollection + { + private readonly List _routes = new List(); + + public IRoute this[int index] + { + get { return _routes[index]; } + } + + public int Count + { + get { return _routes.Count; } + } + + public void Add(IRoute route) + { + _routes.Add(route); + } + } +} diff --git a/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs b/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs new file mode 100644 index 0000000000..c4a56b458b --- /dev/null +++ b/src/Microsoft.AspNet.Routing/DefaultRouteEngine.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing +{ + internal class DefaultRouteEngine : IRouteEngine + { + public DefaultRouteEngine(IRouteCollection routes) + { + Routes = routes; + } + + public IRouteCollection Routes + { + get; + private set; + } + + public async Task Invoke(IDictionary context) + { + RouteContext routeContext = new RouteContext(context); + + for (int i = 0; i < Routes.Count; i++) + { + IRoute route = Routes[i]; + + RouteMatch match = route.Match(routeContext); + if (match != null) + { + await match.Endpoint.Invoke(context); + return true; + } + } + + return false; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/IRoute.cs b/src/Microsoft.AspNet.Routing/IRoute.cs new file mode 100644 index 0000000000..561299ca4e --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRoute.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing +{ + public interface IRoute + { + RouteMatch Match(RouteContext context); + } +} diff --git a/src/Microsoft.AspNet.Routing/IRouteCollection.cs b/src/Microsoft.AspNet.Routing/IRouteCollection.cs new file mode 100644 index 0000000000..85b9faf950 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRouteCollection.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Routing +{ + public interface IRouteCollection + { + IRoute this[int index] + { + get; + } + + int Count + { + get; + } + + void Add(IRoute route); + } +} diff --git a/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs b/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs new file mode 100644 index 0000000000..badb71f73b --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRouteEndpoint.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing +{ + public interface IRouteEndpoint + { + Task Invoke(IDictionary context); + } +} diff --git a/src/Microsoft.AspNet.Routing/IRouteEngine.cs b/src/Microsoft.AspNet.Routing/IRouteEngine.cs new file mode 100644 index 0000000000..e813f79b42 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/IRouteEngine.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing +{ + public interface IRouteEngine + { + Task Invoke(IDictionary context); + } +} diff --git a/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs b/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs new file mode 100644 index 0000000000..06e70a5ca1 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Owin/AppBuilderExtensions.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +#if NET45 + +using Owin; + +namespace Microsoft.AspNet.Routing.Owin +{ + public static class AppBuilderExtensions + { + public static IRouteCollection UseRouter(this IAppBuilder builder) + { + var routes = new DefaultRouteCollection(); + var engine = new DefaultRouteEngine(routes); + + builder.Use(typeof(RouterMiddleware), engine); + + return routes; + } + } +} + +#endif diff --git a/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs b/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs new file mode 100644 index 0000000000..21e4cf6904 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/Owin/RouterMiddleware.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +#if NET45 + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Routing.Owin +{ + public class RouterMiddleware + { + public RouterMiddleware(Func, Task> next, IRouteEngine engine) + { + Next = next; + Engine = engine; + } + + private IRouteEngine Engine + { + get; + set; + } + + private Func, Task> Next + { + get; + set; + } + + public async Task Invoke(IDictionary context) + { + if (!(await Engine.Invoke(context))) + { + await Next.Invoke(context); + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Routing/RouteContext.cs b/src/Microsoft.AspNet.Routing/RouteContext.cs new file mode 100644 index 0000000000..fb45e7dd6d --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteContext.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + public sealed class RouteContext + { + public RouteContext(IDictionary context) + { + Context = context; + + RequestPath = (string)context["owin.RequestPath"]; + } + + public IDictionary Context + { + get; + private set; + } + + public string RequestPath + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/RouteMatch.cs b/src/Microsoft.AspNet.Routing/RouteMatch.cs new file mode 100644 index 0000000000..3bea932dd2 --- /dev/null +++ b/src/Microsoft.AspNet.Routing/RouteMatch.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Routing +{ + /// + /// The result of matching a route. Includes an to invoke and an optional collection of + /// captured values. + /// + public sealed class RouteMatch + { + public RouteMatch(IRouteEndpoint endpoint) + : this(endpoint, null) + { + } + + public RouteMatch(IRouteEndpoint endpoint, IDictionary values) + { + Endpoint = endpoint; + Values = values; + } + + public IRouteEndpoint Endpoint + { + get; + private set; + } + + public IDictionary Values + { + get; + private set; + } + } +} diff --git a/src/Microsoft.AspNet.Routing/project.json b/src/Microsoft.AspNet.Routing/project.json index 9fdf6df436..9d789b9e65 100644 --- a/src/Microsoft.AspNet.Routing/project.json +++ b/src/Microsoft.AspNet.Routing/project.json @@ -1,12 +1,13 @@ { "version": "0.1-alpha-*", - "dependencies": {}, + "dependencies": { + }, "configurations": { "net45": { - "dependencies": { + "dependencies": { "Owin": "1.0" } - }, + }, "k10" : { } } } \ No newline at end of file