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.
This commit is contained in:
Ryan Nowak 2015-01-14 14:32:41 -08:00
parent 02f4ca9f05
commit 623b733eaa
14 changed files with 306 additions and 4 deletions

17
Mvc.sln
View File

@ -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

View File

@ -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());
}
}

View File

@ -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<IApplicationBuilder> _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);
}
}
}

View File

@ -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",

View File

@ -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.";
}
}
}

View File

@ -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"] + ".";
}
}
}

View File

@ -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.";
}
}
}

View File

@ -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.";
}
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>364ec3c6-c9db-45e0-a0f2-1ee61e4b429b</ProjectGuid>
<RootNamespace>CustomRouteWebSite</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>4095</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -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)
{
}
}
}

View File

@ -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<string, string> _users = new Dictionary<string, string>(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;
}
}
}

View File

@ -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}");
});
}
}
}

View File

@ -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"
}

View File

@ -0,0 +1,4 @@
CustomRouteWebSite
===
This web site illustrates how a custom route injects route data based on the user.