From 54c1fed254d100d075500147f4b44368f391d99f Mon Sep 17 00:00:00 2001 From: sornaks Date: Sat, 21 Mar 2015 15:57:10 -0700 Subject: [PATCH] Issue #1736 - Razor view searches in case-sensitive filesystems. --- .../DefaultViewLocationCache.cs | 8 +- .../Extensions/DictionaryExtensions.cs | 26 -- .../RazorViewEngine.cs | 63 ++- .../RazorFileSystemCaseSensitivityTest.cs | 67 +++ .../DefaultViewLocationCacheTest.cs | 90 +++- .../RazorViewEngineTest.cs | 430 +++++++++++++++++- 6 files changed, 649 insertions(+), 35 deletions(-) delete mode 100644 src/Microsoft.AspNet.Mvc.Razor/Extensions/DictionaryExtensions.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/RazorFileSystemCaseSensitivityTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Razor/DefaultViewLocationCache.cs b/src/Microsoft.AspNet.Mvc.Razor/DefaultViewLocationCache.cs index 9575a89eb9..dbe1f206ce 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/DefaultViewLocationCache.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/DefaultViewLocationCache.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Linq; using System.Text; +using Microsoft.AspNet.Mvc.Razor.Internal; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Mvc.Razor @@ -49,7 +49,9 @@ namespace Microsoft.AspNet.Mvc.Razor { var keyBuilder = new StringBuilder(); var routeValues = context.ActionContext.RouteData.Values; - var controller = routeValues.GetValueOrDefault(RazorViewEngine.ControllerKey); + var controller = RazorViewEngine.GetNormalizedRouteValue( + context.ActionContext, + RazorViewEngine.ControllerKey); // format is "{viewName}:{isPartial}:{controllerName}:{areaName}:" keyBuilder.Append(context.ViewName) @@ -58,7 +60,7 @@ namespace Microsoft.AspNet.Mvc.Razor .Append(CacheKeySeparator) .Append(controller); - var area = routeValues.GetValueOrDefault(RazorViewEngine.AreaKey); + var area = RazorViewEngine.GetNormalizedRouteValue(context.ActionContext, RazorViewEngine.AreaKey); if (!string.IsNullOrEmpty(area)) { keyBuilder.Append(CacheKeySeparator) diff --git a/src/Microsoft.AspNet.Mvc.Razor/Extensions/DictionaryExtensions.cs b/src/Microsoft.AspNet.Mvc.Razor/Extensions/DictionaryExtensions.cs deleted file mode 100644 index a94ffac891..0000000000 --- a/src/Microsoft.AspNet.Mvc.Razor/Extensions/DictionaryExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// 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; -using Microsoft.Framework.Internal; - -namespace System.Collections.Generic -{ - internal static class DictionaryExtensions - { - public static T GetValueOrDefault([NotNull] this IDictionary dictionary, - [NotNull] string key) - { - object valueAsObject; - if (dictionary.TryGetValue(key, out valueAsObject)) - { - if (valueAsObject is T) - { - return (T)valueAsObject; - } - } - - return default(T); - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs index 9e2693cd50..b6dc4c0381 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorViewEngine.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.Linq; +using Microsoft.AspNet.Mvc.Razor.Internal; using Microsoft.AspNet.Mvc.Razor.OptionDescriptors; using Microsoft.AspNet.Mvc.Rendering; using Microsoft.Framework.Internal; @@ -133,6 +135,62 @@ namespace Microsoft.AspNet.Mvc.Razor return GetRazorPageResult(context, pageName, isPartial: true); } + /// + /// Gets the case-normalized route value for the specified route . + /// + /// The . + /// The route key to lookup. + /// The value corresponding to the key. + /// + /// The casing of a route value in is determined by the client. + /// This making constructing paths for view locations in a case sensitive file system unreliable. Using the + /// for attribute routes and + /// for traditional routes to get route values produces + /// consistently cased results. + /// + internal static string GetNormalizedRouteValue(ActionContext context, string key) + { + object routeValue; + if (!context.RouteData.Values.TryGetValue(key, out routeValue)) + { + return null; + } + + var actionDescriptor = context.ActionDescriptor; + string normalizedValue = null; + if (actionDescriptor.AttributeRouteInfo != null) + { + object match; + if (actionDescriptor.RouteValueDefaults.TryGetValue(key, out match)) + { + normalizedValue = match?.ToString(); + } + } + else + { + // For traditional routes, lookup the key in RouteConstraints if the key is RequireKey. + var match = actionDescriptor.RouteConstraints.FirstOrDefault( + constraint => string.Equals(constraint.RouteKey, key, StringComparison.OrdinalIgnoreCase)); + if (match != null && match.KeyHandling != RouteKeyHandling.CatchAll) + { + if (match.KeyHandling == RouteKeyHandling.DenyKey) + { + return null; + } + + normalizedValue = match.RouteValue; + } + } + + var stringRouteValue = routeValue?.ToString(); + if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase)) + { + return normalizedValue; + } + + return stringRouteValue; + } + private RazorPageResult GetRazorPageResult(ActionContext context, string pageName, bool isPartial) @@ -164,8 +222,7 @@ namespace Microsoft.AspNet.Mvc.Razor bool isPartial) { // Initialize the dictionary for the typical case of having controller and action tokens. - var routeValues = context.RouteData.Values; - var areaName = routeValues.GetValueOrDefault(AreaKey); + var areaName = GetNormalizedRouteValue(context, AreaKey); // Only use the area view location formats if we have an area token. var viewLocations = !string.IsNullOrEmpty(areaName) ? AreaViewLocationFormats : @@ -204,7 +261,7 @@ namespace Microsoft.AspNet.Mvc.Razor } // 3. Use the expanded locations to look up a page. - var controllerName = routeValues.GetValueOrDefault(ControllerKey); + var controllerName = GetNormalizedRouteValue(context, ControllerKey); var searchedLocations = new List(); foreach (var path in viewLocations) { diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorFileSystemCaseSensitivityTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorFileSystemCaseSensitivityTest.cs new file mode 100644 index 0000000000..6e996596e6 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/RazorFileSystemCaseSensitivityTest.cs @@ -0,0 +1,67 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.Framework.DependencyInjection; +using RazorEmbeddedViewsWebSite; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + // The EmbeddedFileSystem used by RazorEmbeddedViewsWebSite performs case sensitive lookups for files. + // These tests verify that we correctly normalize route values when constructing view lookup paths. + public class RazorFileSystemCaseSensitivityTest + { + private const string SiteName = nameof(RazorEmbeddedViewsWebSite); + private readonly Action _app = new Startup().Configure; + private readonly Action _configureServices = new Startup().ConfigureServices; + + [Fact] + public async Task RazorViewEngine_NormalizesActionName_WhenLookingUpViewPaths() + { + // Arrange + var expectedMessage = "Hello test-user, this is /RazorEmbeddedViews_Home"; + var server = TestHelper.CreateServer(_app, SiteName, _configureServices); + var client = server.CreateClient(); + + // Act + var response = await client.GetStringAsync("http://localhost/RazorEmbeddedViews_Home/index?User=test-user"); + + // Assert + Assert.Equal(expectedMessage, response); + } + + [Fact] + public async Task RazorViewEngine_NormalizesControllerRouteValue_WhenLookingUpViewPaths() + { + // Arrange + var expectedMessage = "Hello test-user, this is /razorembeddedviews_home"; + var server = TestHelper.CreateServer(_app, SiteName, _configureServices); + var client = server.CreateClient(); + + // Act + var response = await client.GetStringAsync("http://localhost/razorembeddedviews_home?User=test-user"); + + // Assert + Assert.Equal(expectedMessage, response); + } + + [Fact] + public async Task RazorViewEngine_NormalizesAreaRouteValue_WhenLookupViewPaths() + { + // Arrange + var expectedMessage = "Hello admin-user, this is /restricted/razorembeddedviews_admin/login"; + var server = TestHelper.CreateServer(_app, SiteName, _configureServices); + var client = server.CreateClient(); + var target = "http://localhost/restricted/razorembeddedviews_admin/login?AdminUser=admin-user"; + + // Act + var response = await client.GetStringAsync(target); + + // Assert + Assert.Equal(expectedMessage, response); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultViewLocationCacheTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultViewLocationCacheTest.cs index b3c80e2aa0..3ac30cbf4f 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultViewLocationCacheTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultViewLocationCacheTest.cs @@ -126,6 +126,59 @@ namespace Microsoft.AspNet.Mvc.Razor expanderContext, "test3:1:controller3:area3:culture:fr:theme:sleek" }; + + yield return new object[] + { + new ViewLocationExpanderContext( + GetActionContextWithActionDescriptor( + new Dictionary() + { + {"controller", "MyController" }, + }, + new Dictionary() + { + {"controller", "mycontroller" }, + }, + isAttributeRouted: true), + "test", + isPartial: false), + "test:0:mycontroller" + }; + + yield return new object[] + { + new ViewLocationExpanderContext( + GetActionContextWithActionDescriptor( + new Dictionary() + { + {"controller", "MyController" }, + }, + new Dictionary() + { + {"controller", "mycontroller" }, + }, + isAttributeRouted: true), + "test", + isPartial: false), + "test:0:mycontroller" + }; + + yield return new object[] + { + new ViewLocationExpanderContext( + GetActionContextWithActionDescriptor( + new Dictionary() + { + {"controller", "mycontroller" }, + }, + new Dictionary() + { + }, + isAttributeRouted: true), + "test", + isPartial: false), + "test:0:mycontroller" + }; } } @@ -150,7 +203,42 @@ namespace Microsoft.AspNet.Mvc.Razor routeData.Values["area"] = area; } - return new ActionContext(new DefaultHttpContext(), routeData, new ActionDescriptor()); + var actionDesciptor = new ActionDescriptor(); + actionDesciptor.RouteConstraints = new List(); + return new ActionContext(new DefaultHttpContext(), routeData, actionDesciptor); + } + + private static ActionContext GetActionContextWithActionDescriptor( + IDictionary routeValues, + IDictionary routesInActionDescriptor, + bool isAttributeRouted) + { + var httpContext = new DefaultHttpContext(); + var routeData = new RouteData(); + foreach (var kvp in routeValues) + { + routeData.Values.Add(kvp.Key, kvp.Value); + } + + var actionDescriptor = new ActionDescriptor(); + if (isAttributeRouted) + { + actionDescriptor.AttributeRouteInfo = new Routing.AttributeRouteInfo(); + foreach (var kvp in routesInActionDescriptor) + { + actionDescriptor.RouteValueDefaults.Add(kvp.Key, kvp.Value); + } + } + else + { + actionDescriptor.RouteConstraints = new List(); + foreach (var kvp in routesInActionDescriptor) + { + actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(kvp.Key, kvp.Value)); + } + } + + return new ActionContext(httpContext, routeData, actionDescriptor); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs index 03f99faedf..af4582e92c 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorViewEngineTest.cs @@ -10,6 +10,7 @@ using Microsoft.AspNet.Routing; using Microsoft.AspNet.Testing; using Moq; using Xunit; +using Microsoft.AspNet.Mvc.Routing; namespace Microsoft.AspNet.Mvc.Razor.Test { @@ -594,7 +595,151 @@ namespace Microsoft.AspNet.Mvc.Razor.Test Assert.Equal(expected, result.SearchedLocations); } - [Fact] + [Theory] + // Looks in RouteValueDefaults + [InlineData(true)] + // Looks in RouteConstraints + [InlineData(false)] + public void FindPage_SelectsActionCaseInsensitively(bool isAttributeRouted) + { + // The ActionDescriptor contains "Foo" and the RouteData contains "foo" + // which matches the case of the constructor thus searching in the appropriate location. + // Arrange + var routeValues = new Dictionary + { + { "controller", "foo" } + }; + + var page = new Mock(MockBehavior.Strict).Object; + var pageFactory = new Mock(); + pageFactory.Setup(p => p.CreateInstance("/Views/Foo/details.cshtml")) + .Returns(page) + .Verifiable(); + + var viewEngine = CreateViewEngine(pageFactory.Object); + var routesInActionDescriptor = new Dictionary() + { + { "controller", "Foo" } + }; + + var context = GetActionContextWithActionDescriptor(routeValues, routesInActionDescriptor, isAttributeRouted); + + // Act + var result = viewEngine.FindPage(context, "details"); + + // Assert + Assert.Equal("details", result.Name); + Assert.Same(page, result.Page); + Assert.Null(result.SearchedLocations); + pageFactory.Verify(); + } + + [Theory] + // Looks in RouteValueDefaults + [InlineData(true)] + // Looks in RouteConstraints + [InlineData(false)] + public void FindPage_LooksForPages_UsingActionDescriptor_Controller(bool isAttributeRouted) + { + // Arrange + var expected = new[] + { + "/Views/bar/foo.cshtml", + "/Views/Shared/foo.cshtml", + }; + + var routeValues = new Dictionary + { + { "controller", "Bar" } + }; + var routesInActionDescriptor = new Dictionary() + { + { "controller", "bar" } + }; + var page = Mock.Of(); + + var viewEngine = CreateViewEngine(); + var context = GetActionContextWithActionDescriptor(routeValues, routesInActionDescriptor, isAttributeRouted); + + // Act + var result = viewEngine.FindPage(context, "foo"); + + // Assert + Assert.Equal("foo", result.Name); + Assert.Null(result.Page); + Assert.Equal(expected, result.SearchedLocations); + } + + [Theory] + // Looks in RouteValueDefaults + [InlineData(true)] + // Looks in RouteConstraints + [InlineData(false)] + public void FindPage_LooksForPages_UsingActionDescriptor_Areas(bool isAttributeRouted) + { + // Arrange + var expected = new[] + { + "/Areas/world/Views/bar/foo.cshtml", + "/Areas/world/Views/Shared/foo.cshtml", + "/Views/Shared/foo.cshtml" + }; + + var routeValues = new Dictionary + { + { "controller", "Bar" }, + { "area", "World" } + }; + var routesInActionDescriptor = new Dictionary() + { + { "controller", "bar" }, + { "area", "world" } + }; + var page = Mock.Of(); + + var viewEngine = CreateViewEngine(); + var context = GetActionContextWithActionDescriptor(routeValues, routesInActionDescriptor, isAttributeRouted); + + // Act + var result = viewEngine.FindPage(context, "foo"); + + // Assert + Assert.Equal("foo", result.Name); + Assert.Null(result.Page); + Assert.Equal(expected, result.SearchedLocations); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void FindPage_LooksForPages_UsesRouteValuesAsFallback(bool isAttributeRouted) + { + // Arrange + var expected = new[] + { + "/Views/foo/bar.cshtml", + "/Views/Shared/bar.cshtml", + }; + + var routeValues = new Dictionary() + { + { "controller", "foo" } + }; + var page = Mock.Of(); + + var viewEngine = CreateViewEngine(); + var context = GetActionContextWithActionDescriptor(routeValues, new Dictionary(), isAttributeRouted); + + // Act + var result = viewEngine.FindPage(context, "bar"); + + // Assert + Assert.Equal("bar", result.Name); + Assert.Null(result.Page); + Assert.Equal(expected, result.SearchedLocations); + } + + [Fact] public void AreaViewLocationFormats_ContainsExpectedLocations() { // Arrange @@ -626,6 +771,252 @@ namespace Microsoft.AspNet.Mvc.Razor.Test Assert.Equal(viewLocations, viewEngine.ViewLocationFormats); } + [Fact] + public void GetNormalizedRouteValue_ReturnsValueFromRouteConstraints_IfKeyHandlingIsRequired() + { + // Arrange + var key = "some-key"; + var actionDescriptor = new ActionDescriptor + { + RouteConstraints = new[] + { + new RouteDataActionConstraint(key, "Route-Value") + } + }; + + var actionContext = new ActionContext + { + ActionDescriptor = actionDescriptor, + RouteData = new RouteData() + }; + + actionContext.RouteData.Values[key] = "route-value"; + + // Act + var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key); + + // Assert + Assert.Equal("Route-Value", result); + } + + [Fact] + public void GetNormalizedRouteValue_ReturnsRouteValue_IfValueDoesNotMatchRouteConstraint() + { + // Arrange + var key = "some-key"; + var actionDescriptor = new ActionDescriptor + { + RouteConstraints = new[] + { + new RouteDataActionConstraint(key, "different-value") + } + }; + + var actionContext = new ActionContext + { + ActionDescriptor = actionDescriptor, + RouteData = new RouteData() + }; + + actionContext.RouteData.Values[key] = "route-value"; + + // Act + var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key); + + // Assert + Assert.Equal("route-value", result); + } + + [Fact] + public void GetNormalizedRouteValue_ReturnsNull_IfRouteConstraintKeyHandlingIsDeny() + { + // Arrange + var key = "some-key"; + var actionDescriptor = new ActionDescriptor + { + RouteConstraints = new[] + { + new RouteDataActionConstraint(key, routeValue: string.Empty) + } + }; + + var actionContext = new ActionContext + { + ActionDescriptor = actionDescriptor, + RouteData = new RouteData() + }; + + actionContext.RouteData.Values[key] = "route-value"; + + // Act + var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetNormalizedRouteValue_ReturnsRouteDataValue_IfRouteConstraintKeyHandlingIsCatchAll() + { + // Arrange + var key = "some-key"; + var actionDescriptor = new ActionDescriptor + { + RouteConstraints = new[] + { + RouteDataActionConstraint.CreateCatchAll(key) + } + }; + + var actionContext = new ActionContext + { + ActionDescriptor = actionDescriptor, + RouteData = new RouteData() + }; + + actionContext.RouteData.Values[key] = "route-value"; + + // Act + var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key); + + // Assert + Assert.Equal("route-value", result); + } + + [Fact] + public void GetNormalizedRouteValue_UsesRouteValueDefaults_IfAttributeRouted() + { + // Arrange + var key = "some-key"; + var actionDescriptor = new ActionDescriptor + { + AttributeRouteInfo = new AttributeRouteInfo(), + }; + actionDescriptor.RouteValueDefaults[key] = "Route-Value"; + + var actionContext = new ActionContext + { + ActionDescriptor = actionDescriptor, + RouteData = new RouteData() + }; + + actionContext.RouteData.Values[key] = "route-value"; + + // Act + var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key); + + // Assert + Assert.Equal("Route-Value", result); + } + + [Fact] + public void GetNormalizedRouteValue_UsesRouteValue_IfRouteValueDefaultsDoesNotMatchRouteValue() + { + // Arrange + var key = "some-key"; + var actionDescriptor = new ActionDescriptor + { + AttributeRouteInfo = new AttributeRouteInfo(), + }; + actionDescriptor.RouteValueDefaults[key] = "different-value"; + + var actionContext = new ActionContext + { + ActionDescriptor = actionDescriptor, + RouteData = new RouteData() + }; + + actionContext.RouteData.Values[key] = "route-value"; + + // Act + var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key); + + // Assert + Assert.Equal("route-value", result); + } + + [Fact] + public void GetNormalizedRouteValue_ConvertsRouteDefaultToStringValue_IfAttributeRouted() + { + using (new CultureReplacer()) + { + // Arrange + var key = "some-key"; + var actionDescriptor = new ActionDescriptor + { + AttributeRouteInfo = new AttributeRouteInfo(), + }; + actionDescriptor.RouteValueDefaults[key] = 32; + + var actionContext = new ActionContext + { + ActionDescriptor = actionDescriptor, + RouteData = new RouteData() + }; + + actionContext.RouteData.Values[key] = 32; + + // Act + var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key); + + // Assert + Assert.Equal("32", result); + } + } + + [Fact] + public void GetNormalizedRouteValue_UsesRouteDataValue_IfKeyDoesNotExistInRouteDefaultValues() + { + // Arrange + var key = "some-key"; + var actionDescriptor = new ActionDescriptor + { + AttributeRouteInfo = new AttributeRouteInfo(), + }; + + var actionContext = new ActionContext + { + ActionDescriptor = actionDescriptor, + RouteData = new RouteData() + }; + + actionContext.RouteData.Values[key] = "route-value"; + + // Act + var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key); + + // Assert + Assert.Equal("route-value", result); + } + + [Fact] + public void GetNormalizedRouteValue_ConvertsRouteValueToString() + { + using (new CultureReplacer()) + { + // Arrange + var key = "some-key"; + var actionDescriptor = new ActionDescriptor + { + AttributeRouteInfo = new AttributeRouteInfo(), + }; + + var actionContext = new ActionContext + { + ActionDescriptor = actionDescriptor, + RouteData = new RouteData() + }; + + actionContext.RouteData.Values[key] = 43; + + // Act + var result = RazorViewEngine.GetNormalizedRouteValue(actionContext, key); + + // Assert + Assert.Equal("43", result); + } + } + private RazorViewEngine CreateViewEngine(IRazorPageFactory pageFactory = null, IRazorViewFactory viewFactory = null, IEnumerable expanders = null, @@ -673,7 +1064,42 @@ namespace Microsoft.AspNet.Mvc.Razor.Test routeData.Values.Add(kvp.Key, kvp.Value); } - return new ActionContext(httpContext, routeData, new ActionDescriptor()); + var actionDesciptor = new ActionDescriptor(); + actionDesciptor.RouteConstraints = new List(); + return new ActionContext(httpContext, routeData, actionDesciptor); + } + + private static ActionContext GetActionContextWithActionDescriptor( + IDictionary routeValues, + IDictionary routesInActionDescriptor, + bool isAttributeRouted) + { + var httpContext = new DefaultHttpContext(); + var routeData = new RouteData(); + foreach (var kvp in routeValues) + { + routeData.Values.Add(kvp.Key, kvp.Value); + } + + var actionDescriptor = new ActionDescriptor(); + if (isAttributeRouted) + { + actionDescriptor.AttributeRouteInfo = new AttributeRouteInfo(); + foreach (var kvp in routesInActionDescriptor) + { + actionDescriptor.RouteValueDefaults.Add(kvp.Key, kvp.Value); + } + } + else + { + actionDescriptor.RouteConstraints = new List(); + foreach (var kvp in routesInActionDescriptor) + { + actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint(kvp.Key, kvp.Value)); + } + } + + return new ActionContext(httpContext, routeData, actionDescriptor); } private class OverloadedLocationViewEngine : RazorViewEngine