From 623b733eaa16f7c6645040fd38a905511ccca370 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 14 Jan 2015 14:32:41 -0800 Subject: [PATCH] Adding a sample (test) for using custom IRouter implementation with MVC This is a demonstration of how to inject an IRouter in between traditional routes and MVC's handler. This allows you to accomplish a variety of things that were possible with WebAPIs handlers, but inside the routing system. The example here turns a header representing the user into a locale, which is used to select a controller. You could do other things like reject the route match or change link generation. There is one subtle project change here, to allow the same to be possible for attribute routing, we need to create the attribute route after running the user's routing configuration code. --- Mvc.sln | 17 ++++- src/Microsoft.AspNet.Mvc/BuilderExtensions.cs | 8 ++- .../CustomRouteTest.cs | 65 +++++++++++++++++ .../project.json | 1 + .../Canada/CustomRoute_ProductsController.cs | 16 +++++ .../CustomRoute_OrdersControlller.cs | 16 +++++ .../Spain/CustomRoute_ProductsController.cs | 16 +++++ .../US/CustomRoute_ProductsController.cs | 16 +++++ .../CustomRouteWebSite.kproj | 19 +++++ .../CustomRouteWebSite/LocaleAttribute.cs | 15 ++++ .../CustomRouteWebSite/LocalizedRoute.cs | 70 +++++++++++++++++++ test/WebSites/CustomRouteWebSite/Startup.cs | 28 ++++++++ test/WebSites/CustomRouteWebSite/project.json | 19 +++++ .../CustomRouteWebSite/wwwroot/readme.md | 4 ++ 14 files changed, 306 insertions(+), 4 deletions(-) create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/CustomRouteTest.cs create mode 100644 test/WebSites/CustomRouteWebSite/Controllers/Canada/CustomRoute_ProductsController.cs create mode 100644 test/WebSites/CustomRouteWebSite/Controllers/CustomRoute_OrdersControlller.cs create mode 100644 test/WebSites/CustomRouteWebSite/Controllers/Spain/CustomRoute_ProductsController.cs create mode 100644 test/WebSites/CustomRouteWebSite/Controllers/US/CustomRoute_ProductsController.cs create mode 100644 test/WebSites/CustomRouteWebSite/CustomRouteWebSite.kproj create mode 100644 test/WebSites/CustomRouteWebSite/LocaleAttribute.cs create mode 100644 test/WebSites/CustomRouteWebSite/LocalizedRoute.cs create mode 100644 test/WebSites/CustomRouteWebSite/Startup.cs create mode 100644 test/WebSites/CustomRouteWebSite/project.json create mode 100644 test/WebSites/CustomRouteWebSite/wwwroot/readme.md diff --git a/Mvc.sln b/Mvc.sln index ccb7a68633..914ebd1fbe 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22416.0 +VisualStudioVersion = 14.0.22410.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -116,6 +116,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LoggingWebSite", "test\WebS EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ErrorPageMiddlewareWebSite", "test\WebSites\ErrorPageMiddlewareWebSite\ErrorPageMiddlewareWebSite.kproj", "{AD545A5B-2BA5-4314-88AC-FC2ACF2CC718}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CustomRouteWebSite", "test\WebSites\CustomRouteWebSite\CustomRouteWebSite.kproj", "{364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -640,6 +642,18 @@ Global {AD545A5B-2BA5-4314-88AC-FC2ACF2CC718}.Release|Mixed Platforms.Build.0 = Release|Any CPU {AD545A5B-2BA5-4314-88AC-FC2ACF2CC718}.Release|x86.ActiveCfg = Release|Any CPU {AD545A5B-2BA5-4314-88AC-FC2ACF2CC718}.Release|x86.Build.0 = Release|Any CPU + {364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Debug|x86.ActiveCfg = Debug|Any CPU + {364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Debug|x86.Build.0 = Debug|Any CPU + {364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Release|Any CPU.Build.0 = Release|Any CPU + {364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Release|x86.ActiveCfg = Release|Any CPU + {364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -696,5 +710,6 @@ Global {0A6BB4C0-48D3-4E7F-952B-B8917345E075} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {0AD78AB5-D67C-49BC-81B1-0C51BFA82B5E} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {AD545A5B-2BA5-4314-88AC-FC2ACF2CC718} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {364EC3C6-C9DB-45E0-A0F2-1EE61E4B429B} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Mvc/BuilderExtensions.cs b/src/Microsoft.AspNet.Mvc/BuilderExtensions.cs index 04db8c8970..438d916a08 100644 --- a/src/Microsoft.AspNet.Mvc/BuilderExtensions.cs +++ b/src/Microsoft.AspNet.Mvc/BuilderExtensions.cs @@ -37,12 +37,14 @@ namespace Microsoft.AspNet.Builder ServiceProvider = app.ApplicationServices }; - routes.Routes.Add(AttributeRouting.CreateAttributeMegaRoute( + configureRoutes(routes); + + // Adding the attribute route comes after running the user-code because + // we want to respect any changes to the DefaultHandler. + routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute( routes.DefaultHandler, app.ApplicationServices)); - configureRoutes(routes); - return app.UseRouter(routes.Build()); } } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/CustomRouteTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/CustomRouteTest.cs new file mode 100644 index 0000000000..95a0b35d5c --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/CustomRouteTest.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class CustomRouteTest + { + private readonly IServiceProvider _provider = TestHelper.CreateServices(nameof(CustomRouteWebSite)); + private readonly Action _app = new CustomRouteWebSite.Startup().Configure; + + [Theory] + [InlineData("Javier", "Hola from Spain.")] + [InlineData("Doug", "Hello from Canada.")] + [InlineData("Ryan", "Hello from the USA.")] + public async Task RouteToLocale_ConventionalRoute_BasedOnUser(string user, string expected) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/CustomRoute_Products/Index"); + request.Headers.Add("User", user); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, content); + } + + [Theory] + [InlineData("Javier", "Hello from es-ES.")] + [InlineData("Doug", "Hello from en-CA.")] + [InlineData("Ryan", "Hello from en-US.")] + public async Task RouteWithAttributeRoute_IncludesLocale_BasedOnUser(string user, string expected) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/CustomRoute_Orders/5"); + request.Headers.Add("User", user); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, content); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index 80f87a620b..81eeb63311 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -12,6 +12,7 @@ "BasicWebSite": "1.0.0", "CompositeViewEngineWebSite": "1.0.0", "ConnegWebSite": "1.0.0", + "CustomRouteWebSite": "1.0.0", "ErrorPageMiddlewareWebSite": "1.0.0", "FilesWebSite": "1.0.0", "FiltersWebSite": "1.0.0", diff --git a/test/WebSites/CustomRouteWebSite/Controllers/Canada/CustomRoute_ProductsController.cs b/test/WebSites/CustomRouteWebSite/Controllers/Canada/CustomRoute_ProductsController.cs new file mode 100644 index 0000000000..228ef7ef30 --- /dev/null +++ b/test/WebSites/CustomRouteWebSite/Controllers/Canada/CustomRoute_ProductsController.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace CustomRouteWebSite.Controllers.Canada +{ + [Locale("en-CA")] + public class CustomRoute_ProductsController : Controller + { + public string Index() + { + return "Hello from Canada."; + } + } +} \ No newline at end of file diff --git a/test/WebSites/CustomRouteWebSite/Controllers/CustomRoute_OrdersControlller.cs b/test/WebSites/CustomRouteWebSite/Controllers/CustomRoute_OrdersControlller.cs new file mode 100644 index 0000000000..1f3187acfa --- /dev/null +++ b/test/WebSites/CustomRouteWebSite/Controllers/CustomRoute_OrdersControlller.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace CustomRouteWebSite.Controllers +{ + public class CustomRoute_OrdersControlller : Controller + { + [HttpGet("CustomRoute_Orders/{id}")] + public string Index(int id) + { + return "Hello from " + ActionContext.RouteData.Values["locale"] + "."; + } + } +} diff --git a/test/WebSites/CustomRouteWebSite/Controllers/Spain/CustomRoute_ProductsController.cs b/test/WebSites/CustomRouteWebSite/Controllers/Spain/CustomRoute_ProductsController.cs new file mode 100644 index 0000000000..b5e3960e68 --- /dev/null +++ b/test/WebSites/CustomRouteWebSite/Controllers/Spain/CustomRoute_ProductsController.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace CustomRouteWebSite.Controllers.Spain +{ + [Locale("es-ES")] + public class CustomRoute_ProductsController : Controller + { + public string Index() + { + return "Hola from Spain."; + } + } +} \ No newline at end of file diff --git a/test/WebSites/CustomRouteWebSite/Controllers/US/CustomRoute_ProductsController.cs b/test/WebSites/CustomRouteWebSite/Controllers/US/CustomRoute_ProductsController.cs new file mode 100644 index 0000000000..ae7c36dbf1 --- /dev/null +++ b/test/WebSites/CustomRouteWebSite/Controllers/US/CustomRoute_ProductsController.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace CustomRouteWebSite.Controllers.US +{ + [Locale("en-US")] + public class CustomRoute_ProductsController : Controller + { + public string Index() + { + return "Hello from the USA."; + } + } +} \ No newline at end of file diff --git a/test/WebSites/CustomRouteWebSite/CustomRouteWebSite.kproj b/test/WebSites/CustomRouteWebSite/CustomRouteWebSite.kproj new file mode 100644 index 0000000000..fa333aa175 --- /dev/null +++ b/test/WebSites/CustomRouteWebSite/CustomRouteWebSite.kproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 364ec3c6-c9db-45e0-a0f2-1ee61e4b429b + CustomRouteWebSite + ..\..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 4095 + + + \ No newline at end of file diff --git a/test/WebSites/CustomRouteWebSite/LocaleAttribute.cs b/test/WebSites/CustomRouteWebSite/LocaleAttribute.cs new file mode 100644 index 0000000000..495f456479 --- /dev/null +++ b/test/WebSites/CustomRouteWebSite/LocaleAttribute.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace CustomRouteWebSite +{ + public class LocaleAttribute : RouteConstraintAttribute + { + public LocaleAttribute(string locale) + : base("locale", routeValue: locale, blockNonAttributedActions: true) + { + } + } +} \ No newline at end of file diff --git a/test/WebSites/CustomRouteWebSite/LocalizedRoute.cs b/test/WebSites/CustomRouteWebSite/LocalizedRoute.cs new file mode 100644 index 0000000000..61d2f85db2 --- /dev/null +++ b/test/WebSites/CustomRouteWebSite/LocalizedRoute.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Http; + +namespace CustomRouteWebSite +{ + public class LocalizedRoute : IRouter + { + private readonly IRouter _next; + + private readonly Dictionary _users = new Dictionary(StringComparer.Ordinal) + { + { "Javier", "es-ES" }, + { "Doug", "en-CA" }, + }; + + public LocalizedRoute(IRouter next) + { + _next = next; + } + + public string GetVirtualPath(VirtualPathContext context) + { + // We just want to act as a pass-through for link generation + return _next.GetVirtualPath(context); + } + + public async Task RouteAsync(RouteContext context) + { + // Saving and restoring the original route data ensures that any values we + // add won't 'leak' if action selection doesn't match. + var oldRouteData = context.RouteData; + + // For diagnostics and link-generation purposes, routing should include + // a list of IRoute instances that lead to the ultimate destination. + // It's the responsibility of each IRouter to add the 'next' before + // calling it. + var newRouteData = new RouteData(oldRouteData); + newRouteData.Routers.Add(_next); + + var locale = GetLocale(context.HttpContext) ?? "en-US"; + newRouteData.Values.Add("locale", locale); + + try + { + context.RouteData = newRouteData; + await _next.RouteAsync(context); + } + finally + { + if (!context.IsHandled) + { + context.RouteData = oldRouteData; + } + } + } + + private string GetLocale(HttpContext context) + { + string locale; + _users.TryGetValue(context.Request.Headers.Get("User"), out locale); + return locale; + } + } +} \ No newline at end of file diff --git a/test/WebSites/CustomRouteWebSite/Startup.cs b/test/WebSites/CustomRouteWebSite/Startup.cs new file mode 100644 index 0000000000..2d39cc65b9 --- /dev/null +++ b/test/WebSites/CustomRouteWebSite/Startup.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; + +namespace CustomRouteWebSite +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + var configuration = app.GetTestConfiguration(); + + app.UseServices(services => + { + services.AddMvc(configuration); + }); + + app.UseMvc(routes => + { + routes.DefaultHandler = new LocalizedRoute(routes.DefaultHandler); + routes.MapRoute("default", "{controller}/{action}"); + }); + } + } +} \ No newline at end of file diff --git a/test/WebSites/CustomRouteWebSite/project.json b/test/WebSites/CustomRouteWebSite/project.json new file mode 100644 index 0000000000..01696e38f0 --- /dev/null +++ b/test/WebSites/CustomRouteWebSite/project.json @@ -0,0 +1,19 @@ +{ + "commands": { + "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001", + "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000" + }, + "dependencies": { + "Kestrel": "1.0.0-*", + "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-*" + }, + "frameworks": { + "aspnet50": { }, + "aspnetcore50": { } + }, + "webroot": "wwwroot" +} diff --git a/test/WebSites/CustomRouteWebSite/wwwroot/readme.md b/test/WebSites/CustomRouteWebSite/wwwroot/readme.md new file mode 100644 index 0000000000..04955ec21b --- /dev/null +++ b/test/WebSites/CustomRouteWebSite/wwwroot/readme.md @@ -0,0 +1,4 @@ +CustomRouteWebSite +=== + +This web site illustrates how a custom route injects route data based on the user. \ No newline at end of file