From 10285d7d39f6bc4b76d23abd2283effe29d0421a Mon Sep 17 00:00:00 2001 From: harshgMSFT Date: Fri, 6 Jun 2014 14:37:29 -0700 Subject: [PATCH] This additional constraint enables adding a route to the template (and potentially to the UseMvc method) without actually implementing the actual artifact. For example without adding an area to a controller, a route can still be added to the template. - Also added functional tests. --- Mvc.sln | 17 +- samples/MvcSample.Web/Startup.cs | 3 +- .../DefaultActionSelector.cs | 15 +- .../KnownRouteValueConstraint.cs | 101 +++++++++ .../Microsoft.AspNet.Mvc.Core.kproj | 1 + .../MvcServiceCollectionExtensions.cs | 10 + .../KnownRouteValueConstraintTests.cs | 198 ++++++++++++++++++ .../Microsoft.AspNet.Mvc.Core.Test.kproj | 1 + .../BasicTests.cs | 43 +--- .../InlineConstraintTests.cs | 87 ++++++++ ...Microsoft.AspNet.Mvc.FunctionalTests.kproj | 2 + .../TestHelper.cs | 56 +++++ .../config/InlineConstraintTestsConfig.json | 10 + .../project.json | 4 +- test/WebSites/BasicWebSite/BasicWebSite.kproj | 1 + .../Controllers/UsersController.cs | 12 ++ test/WebSites/BasicWebSite/Startup.cs | 5 + .../App_Data/config.json | 10 + .../Controllers/HomeController.cs | 12 ++ .../Controllers/UsersController.cs | 12 ++ .../InlineConstraintsWebSite.kproj | 47 +++++ .../InlineConstraintsWebSite/Startup.cs | 71 +++++++ .../TestControllerAssemblyProvider.cs | 17 ++ .../Views/Home/Index.cshtml | 8 + .../Views/Shared/Error.cshtml | 7 + .../Views/Shared/_Layout.cshtml | 30 +++ .../InlineConstraintsWebSite/project.json | 11 + 27 files changed, 741 insertions(+), 50 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Core/KnownRouteValueConstraint.cs create mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/KnownRouteValueConstraintTests.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/InlineConstraintTests.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/config/InlineConstraintTestsConfig.json create mode 100644 test/WebSites/BasicWebSite/Controllers/UsersController.cs create mode 100644 test/WebSites/InlineConstraintsWebSite/App_Data/config.json create mode 100644 test/WebSites/InlineConstraintsWebSite/Controllers/HomeController.cs create mode 100644 test/WebSites/InlineConstraintsWebSite/Controllers/UsersController.cs create mode 100644 test/WebSites/InlineConstraintsWebSite/InlineConstraintsWebSite.kproj create mode 100644 test/WebSites/InlineConstraintsWebSite/Startup.cs create mode 100644 test/WebSites/InlineConstraintsWebSite/TestControllerAssemblyProvider.cs create mode 100644 test/WebSites/InlineConstraintsWebSite/Views/Home/Index.cshtml create mode 100644 test/WebSites/InlineConstraintsWebSite/Views/Shared/Error.cshtml create mode 100644 test/WebSites/InlineConstraintsWebSite/Views/Shared/_Layout.cshtml create mode 100644 test/WebSites/InlineConstraintsWebSite/project.json diff --git a/Mvc.sln b/Mvc.sln index 5729755b0d..5506ce9d33 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.21708.0 +VisualStudioVersion = 14.0.21806.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -17,7 +17,7 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Core", EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.ModelBinding", "src\Microsoft.AspNet.Mvc.ModelBinding\Microsoft.AspNet.Mvc.ModelBinding.kproj", "{FA915D3D-22C3-4478-97F2-A81D28B6C503}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Common", "src\Microsoft.AspNet.Mvc.Common\Microsoft.AspNet.Mvc.Common.kproj", "{F3DF6D0B-16FE-4402-B92C-7243A75CF1FD}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Common", "src\Microsoft.AspNet.Mvc.Common\Microsoft.AspNet.Mvc.Common.kproj", "{F3DF6D0B-16FE-4402-B92C-7243A75CF1FD}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.ModelBinding.Test", "test\Microsoft.AspNet.Mvc.ModelBinding.Test\Microsoft.AspNet.Mvc.ModelBinding.Test.kproj", "{3B8DC0C0-6C55-4034-AD96-DE1000928E6B}" EndProject @@ -37,6 +37,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Functi EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "BasicWebSite", "test\WebSites\BasicWebSite\BasicWebSite.kproj", "{34DF1487-12C6-476C-BE0A-F31DF1939AE5}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "InlineConstraintsWebSite", "test\WebSites\InlineConstraintsWebSite\InlineConstraintsWebSite.kproj", "{EA34877F-1AC1-42B7-B4E6-15A093F40CAE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -177,6 +179,16 @@ Global {34DF1487-12C6-476C-BE0A-F31DF1939AE5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {34DF1487-12C6-476C-BE0A-F31DF1939AE5}.Release|Mixed Platforms.Build.0 = Release|Any CPU {34DF1487-12C6-476C-BE0A-F31DF1939AE5}.Release|x86.ActiveCfg = Release|Any CPU + {EA34877F-1AC1-42B7-B4E6-15A093F40CAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA34877F-1AC1-42B7-B4E6-15A093F40CAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA34877F-1AC1-42B7-B4E6-15A093F40CAE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {EA34877F-1AC1-42B7-B4E6-15A093F40CAE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {EA34877F-1AC1-42B7-B4E6-15A093F40CAE}.Debug|x86.ActiveCfg = Debug|Any CPU + {EA34877F-1AC1-42B7-B4E6-15A093F40CAE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA34877F-1AC1-42B7-B4E6-15A093F40CAE}.Release|Any CPU.Build.0 = Release|Any CPU + {EA34877F-1AC1-42B7-B4E6-15A093F40CAE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {EA34877F-1AC1-42B7-B4E6-15A093F40CAE}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {EA34877F-1AC1-42B7-B4E6-15A093F40CAE}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -196,5 +208,6 @@ Global {16703B76-C9F7-4C75-AE6C-53D92E308E3C} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {323D0C04-B518-4A8F-8A8E-3546AD153D34} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {34DF1487-12C6-476C-BE0A-F31DF1939AE5} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {EA34877F-1AC1-42B7-B4E6-15A093F40CAE} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/samples/MvcSample.Web/Startup.cs b/samples/MvcSample.Web/Startup.cs index cdedc534a5..2fea2a856c 100644 --- a/samples/MvcSample.Web/Startup.cs +++ b/samples/MvcSample.Web/Startup.cs @@ -1,6 +1,7 @@ using System; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Routing; +using Microsoft.Framework.ConfigurationModel; using Microsoft.Framework.DependencyInjection; using MvcSample.Web.Filters; using MvcSample.Web.Services; @@ -63,7 +64,7 @@ namespace MvcSample.Web app.UseMvc(routes => { - routes.MapRoute("areaRoute", "{area}/{controller}/{action}"); + routes.MapRoute("areaRoute", "{area:exists}/{controller}/{action}"); routes.MapRoute( "controllerActionRoute", diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultActionSelector.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultActionSelector.cs index 611d7fcc8b..abe86fee91 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultActionSelector.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultActionSelector.cs @@ -345,19 +345,12 @@ namespace Microsoft.AspNet.Mvc if (descriptors == null) { throw new InvalidOperationException( - Resources.FormatPropertyOfTypeCannotBeNull(_actionDescriptorsCollectionProvider.GetType(), - "ActionDescriptors")); + Resources.FormatPropertyOfTypeCannotBeNull("ActionDescriptors", + _actionDescriptorsCollectionProvider.GetType() + )); } - var items = descriptors.Items; - - if (items == null) - { - throw new InvalidOperationException( - Resources.FormatPropertyOfTypeCannotBeNull(descriptors.GetType(), "Items")); - } - - return items; + return descriptors.Items; } private class ActionDescriptorCandidate diff --git a/src/Microsoft.AspNet.Mvc.Core/KnownRouteValueConstraint.cs b/src/Microsoft.AspNet.Mvc.Core/KnownRouteValueConstraint.cs new file mode 100644 index 0000000000..23fad258cc --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/KnownRouteValueConstraint.cs @@ -0,0 +1,101 @@ +// 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.Linq; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc.Core; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Mvc +{ + public class KnownRouteValueConstraint : IRouteConstraint + { + private RouteValuesCollection _cachedValuesCollection; + + public bool Match([NotNull] HttpContext httpContext, + [NotNull] IRouter route, + [NotNull] string routeKey, + [NotNull] IDictionary values, + RouteDirection routeDirection) + { + object value; + if (values.TryGetValue(routeKey, out value)) + { + string valueAsString = value as string; + + if (valueAsString != null) + { + var allValues = GetAndCacheAllMatchingValues(routeKey, httpContext); + var match = allValues.Any(existingRouteValue => + existingRouteValue.Equals( + valueAsString, + StringComparison.OrdinalIgnoreCase)); + + return match; + } + } + + return false; + } + + private string[] GetAndCacheAllMatchingValues(string routeKey, HttpContext httpContext) + { + var actionDescriptors = GetAndValidateActionDescriptorsCollection(httpContext); + var version = actionDescriptors.Version; + var valuesCollection = _cachedValuesCollection; + + if (valuesCollection == null || + version != valuesCollection.Version) + { + var routeValueCollection = actionDescriptors + .Items + .Select(ad => ad.RouteConstraints + .FirstOrDefault( + c => c.RouteKey == routeKey && + c.KeyHandling == RouteKeyHandling.RequireKey)) + .Where(rc => rc != null) + .Select(rc => rc.RouteValue) + .Distinct() + .ToArray(); + + valuesCollection = new RouteValuesCollection(version, routeValueCollection); + _cachedValuesCollection = valuesCollection; + } + + return _cachedValuesCollection.Items; + } + + private static ActionDescriptorsCollection GetAndValidateActionDescriptorsCollection(HttpContext httpContext) + { + var provider = httpContext.ApplicationServices + .GetService(); + var descriptors = provider.ActionDescriptors; + + if (descriptors == null) + { + throw new InvalidOperationException( + Resources.FormatPropertyOfTypeCannotBeNull("ActionDescriptors", + provider.GetType() + )); + } + + return descriptors; + } + + private class RouteValuesCollection + { + public RouteValuesCollection(int version, string[] items) + { + Version = version; + Items = items; + } + + public int Version { get; private set; } + + public string[] Items { get; private set; } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj index 506d365981..5cdfa12834 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj +++ b/src/Microsoft.AspNet.Mvc.Core/Microsoft.AspNet.Mvc.Core.kproj @@ -202,6 +202,7 @@ + diff --git a/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs index 2acc3224ec..c0bbd64713 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs @@ -13,13 +13,23 @@ namespace Microsoft.Framework.DependencyInjection public static IServiceCollection AddMvc(this IServiceCollection services) { services.Add(RoutingServices.GetDefaultServices()); + AddMvcRouteOptions(services); return services.Add(MvcServices.GetDefaultServices()); } public static IServiceCollection AddMvc(this IServiceCollection services, IConfiguration configuration) { services.Add(RoutingServices.GetDefaultServices()); + AddMvcRouteOptions(services); return services.Add(MvcServices.GetDefaultServices(configuration)); } + + private static void AddMvcRouteOptions(IServiceCollection services) + { + services.SetupOptions(routeOptions => + routeOptions.ConstraintMap + .Add("exists", + typeof(KnownRouteValueConstraint))); + } } } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/KnownRouteValueConstraintTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/KnownRouteValueConstraintTests.cs new file mode 100644 index 0000000000..b5e2e4a9b1 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/KnownRouteValueConstraintTests.cs @@ -0,0 +1,198 @@ +// 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. + +#if NET45 +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc; +using Microsoft.Framework.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Routing.Tests +{ + public class KnownRouteValueConstraintTests + { + private readonly IRouteConstraint _constraint = new KnownRouteValueConstraint(); + + [Theory] + [InlineData("area", RouteDirection.IncomingRequest)] + [InlineData("controller", RouteDirection.IncomingRequest)] + [InlineData("action", RouteDirection.IncomingRequest)] + [InlineData("randomKey", RouteDirection.IncomingRequest)] + [InlineData("area", RouteDirection.UrlGeneration)] + [InlineData("controller", RouteDirection.UrlGeneration)] + [InlineData("action", RouteDirection.UrlGeneration)] + [InlineData("randomKey", RouteDirection.UrlGeneration)] + public void RouteKey_DoesNotExist_MatchFails(string keyName, RouteDirection direction) + { + // Arrange + var values = new Dictionary(); + var httpContext = GetHttpContext(new ActionDescriptor()); + var route = (new Mock()).Object; + + // Act + var match = _constraint.Match(httpContext, route, keyName, values, direction); + + // Assert + Assert.False(match); + } + + [Theory] + [InlineData("area", RouteDirection.IncomingRequest)] + [InlineData("controller", RouteDirection.IncomingRequest)] + [InlineData("action", RouteDirection.IncomingRequest)] + [InlineData("randomKey", RouteDirection.IncomingRequest)] + [InlineData("area", RouteDirection.UrlGeneration)] + [InlineData("controller", RouteDirection.UrlGeneration)] + [InlineData("action", RouteDirection.UrlGeneration)] + [InlineData("randomKey", RouteDirection.UrlGeneration)] + public void RouteKey_Exists_MatchSucceeds(string keyName, RouteDirection direction) + { + // Arrange + var actionDescriptor = CreateActionDescriptor("testArea", + "testController", + "testAction"); + actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint("randomKey", "testRandom")); + var httpContext = GetHttpContext(actionDescriptor); + var route = (new Mock()).Object; + var values = new Dictionary() + { + { "area", "testArea" }, + { "controller", "testController" }, + { "action", "testAction" }, + { "randomKey", "testRandom" } + }; + + // Act + var match = _constraint.Match(httpContext, route, keyName, values, direction); + + // Assert + Assert.True(match); + } + + [Theory] + [InlineData("area", RouteDirection.IncomingRequest)] + [InlineData("controller", RouteDirection.IncomingRequest)] + [InlineData("action", RouteDirection.IncomingRequest)] + [InlineData("randomKey", RouteDirection.IncomingRequest)] + [InlineData("area", RouteDirection.UrlGeneration)] + [InlineData("controller", RouteDirection.UrlGeneration)] + [InlineData("action", RouteDirection.UrlGeneration)] + [InlineData("randomKey", RouteDirection.UrlGeneration)] + public void RouteValue_DoesNotExists_MatchFails(string keyName, RouteDirection direction) + { + // Arrange + var actionDescriptor = CreateActionDescriptor("testArea", + "testController", + "testAction"); + actionDescriptor.RouteConstraints.Add(new RouteDataActionConstraint("randomKey", "testRandom")); + var httpContext = GetHttpContext(actionDescriptor); + var route = (new Mock()).Object; + var values = new Dictionary() + { + { "area", "invalidTestArea" }, + { "controller", "invalidTestController" }, + { "action", "invalidTestAction" }, + { "randomKey", "invalidTestRandom" } + }; + + // Act + var match = _constraint.Match(httpContext, route, keyName, values, direction); + + // Assert + Assert.False(match); + } + + [Theory] + [InlineData(RouteDirection.IncomingRequest)] + [InlineData(RouteDirection.UrlGeneration)] + public void RouteValue_IsNotAString_MatchFails(RouteDirection direction) + { + var actionDescriptor = CreateActionDescriptor("testArea", + controller: null, + action: null); + var httpContext = GetHttpContext(actionDescriptor); + var route = (new Mock()).Object; + var values = new Dictionary() + { + { "area", 12 }, + }; + + // Act + var match = _constraint.Match(httpContext, route, "area", values, direction); + + // Assert + Assert.False(match); + } + + [Theory] + [InlineData(RouteDirection.IncomingRequest)] + [InlineData(RouteDirection.UrlGeneration)] + public void ActionDescriptorsCollection_SettingNullValue_Throws(RouteDirection direction) + { + // Arrange + var httpContext = new Mock(); + httpContext.Setup(o => o.ApplicationServices + .GetService(typeof(IActionDescriptorsCollectionProvider))) + .Returns(new Mock().Object); + // Act & Assert + var ex = Assert.Throws( + () => _constraint.Match(httpContext.Object, + null, + "area", + new Dictionary{ { "area", "area" } }, + direction)); + Assert.Equal("The 'ActionDescriptors' property of "+ + "'Castle.Proxies.IActionDescriptorsCollectionProviderProxy' must not be null.", + ex.Message); + } + + private static HttpContext GetHttpContext(ActionDescriptor actionDescriptor) + { + var actionProvider = new Mock>( + MockBehavior.Strict); + + actionProvider + .Setup(p => p.Invoke(It.IsAny())) + .Callback(c => c.Results.Add(actionDescriptor)); + + var context = new Mock(); + context.Setup(o => o.ApplicationServices + .GetService(typeof(INestedProviderManager))) + .Returns(actionProvider.Object); + context.Setup(o => o.ApplicationServices + .GetService(typeof(IActionDescriptorsCollectionProvider))) + .Returns(new DefaultActionDescriptorsCollectionProvider(context.Object.ApplicationServices)); + return context.Object; + } + + private static ActionDescriptor CreateActionDescriptor(string area, string controller, string action) + { + var actionDescriptor = new ActionDescriptor() + { + Name = string.Format("Area: {0}, Controller: {1}, Action: {2}", area, controller, action), + RouteConstraints = new List(), + }; + + actionDescriptor.RouteConstraints.Add( + area == null ? + new RouteDataActionConstraint("area", RouteKeyHandling.DenyKey) : + new RouteDataActionConstraint("area", area)); + + actionDescriptor.RouteConstraints.Add( + controller == null ? + new RouteDataActionConstraint("controller", RouteKeyHandling.DenyKey) : + new RouteDataActionConstraint("controller", controller)); + + actionDescriptor.RouteConstraints.Add( + action == null ? + new RouteDataActionConstraint("action", RouteKeyHandling.DenyKey) : + new RouteDataActionConstraint("action", action)); + + return actionDescriptor; + } + } +} +#endif \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj index f5f4fc2985..b6ab6319fb 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj +++ b/test/Microsoft.AspNet.Mvc.Core.Test/Microsoft.AspNet.Mvc.Core.Test.kproj @@ -55,6 +55,7 @@ + diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/BasicTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/BasicTests.cs index c5af73cfe3..768a1152d8 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/BasicTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/BasicTests.cs @@ -2,16 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.IO; using System.Reflection; using System.Threading.Tasks; using BasicWebSite; using Microsoft.AspNet.Builder; using Microsoft.AspNet.TestHost; -using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.DependencyInjection.Fallback; -using Microsoft.Framework.Runtime; -using Microsoft.Framework.Runtime.Infrastructure; using Xunit; namespace Microsoft.AspNet.Mvc.FunctionalTests @@ -29,24 +24,15 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests public BasicTests() { - var originalProvider = CallContextServiceLocator.Locator.ServiceProvider; - IApplicationEnvironment appEnvironment = originalProvider.GetService(); - - // When an application executes in a regular context, the application base path points to the root - // directory where the application is located, for example MvcSample.Web. However, when executing - // an aplication as part of a test, the ApplicationBasePath of the IApplicationEnvironment points - // to the root folder of the test project. - // To compensate for this, we need to calculate the original path and override the application - // environment value so that components like the view engine work properly in the context of the - // test. - string appBasePath = CalculateApplicationBasePath(appEnvironment); - _provider = new ServiceCollection() - .AddInstance(typeof(IApplicationEnvironment), new TestApplicationEnvironment(appEnvironment, appBasePath)) - .BuildServiceProvider(originalProvider); + _provider = TestHelper.CreateServices("BasicWebSite"); } - [Fact] - public async Task CanRender_ViewsWithLayout() + [InlineData("http://localhost/")] + [InlineData("http://localhost/Home")] + [InlineData("http://localhost/Home/Index")] + [InlineData("http://localhost/Users")] + [InlineData("http://localhost/Monitor/CountActionDescriptorInvocations")] + public async Task CanRender_ViewsWithLayout(string url) { // Arrange var server = TestServer.Create(_provider, _app); @@ -59,7 +45,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests // Act // The host is not important as everything runs in memory and tests are isolated from each other. - var result = await client.GetAsync("http://localhost/"); + var result = await client.GetAsync(url); var responseContent = await result.ReadBodyAsStringAsync(); // Assert @@ -129,18 +115,5 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal(expectedContent, results[1]); Assert.Equal(expectedContent, results[2]); } - - // Calculate the path relative to the current application base path. - private static string CalculateApplicationBasePath(IApplicationEnvironment appEnvironment) - { - // Mvc/test/Microsoft.AspNet.Mvc.FunctionalTests - var appBase = appEnvironment.ApplicationBasePath; - - // Mvc/test - var test = Path.GetDirectoryName(appBase); - - // Mvc/test/WebSites/BasicWebSite - return Path.GetFullPath(Path.Combine(appBase, "..", "WebSites", "BasicWebSite")); - } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/InlineConstraintTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/InlineConstraintTests.cs new file mode 100644 index 0000000000..8ea1f2ed1f --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/InlineConstraintTests.cs @@ -0,0 +1,87 @@ +// 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.IO; +using System.Reflection; +using System.Threading.Tasks; +using InlineConstraints; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Routing; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.ConfigurationModel; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; +using Microsoft.Framework.Runtime; +using Microsoft.Framework.Runtime.Infrastructure; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class InlineConstraintTests + { + private readonly IServiceProvider _provider; + private readonly Action _app = new Startup().Configure; + private readonly string _jsonConfigFilePath; + private readonly Configuration _config = new Configuration(); + public InlineConstraintTests() + { + _provider = TestHelper.CreateServices("InlineConstraintsWebSite"); + + // TODO: Hardcoding the config file path for now. Update it to read it from args. + _jsonConfigFilePath = @"config\InlineConstraintTestsConfig.json"; + _config.AddJsonFile(_jsonConfigFilePath); + + Environment.SetEnvironmentVariable("AppConfigPath", _jsonConfigFilePath); + } + + [Fact] + public async Task RoutingToANonExistantArea_WithExistConstraint_RoutesToCorrectAction() + { + // Arrange + var source = new JsonConfigurationSource(_jsonConfigFilePath); + + // Add the exists inline constraint. + _config.Set("TemplateCollection:areaRoute:TemplateValue", + @"{area:exists}/{controller=Home}/{action=Index}"); + _config.Set("TemplateCollection:actionAsMethod:TemplateValue", + @"{controller=Home}/{action=Index}"); + _config.Commit(); + + var server = TestServer.Create(_provider, _app); + var client = server.Handler; + + // Act + var result = await client.GetAsync("http://localhost/Users"); + Assert.Equal(200, result.StatusCode); + + // Assert + var returnValue = await result.ReadBodyAsStringAsync(); + Assert.Equal("Users.Index", returnValue); + } + + [Fact] + public async Task RoutingToANonExistantArea_WithoutExistConstraint_RoutesToIncorrectAction() + { + // Arrange + _config.Set("TemplateCollection:areaRoute:TemplateValue", + @"{area}/{controller=Home}/{action=Index}"); + _config.Set("TemplateCollection:actionAsMethod:TemplateValue", + @"{controller=Home}/{action=Index}"); + + _config.Commit(); + + var server = TestServer.Create(_provider, _app); + var client = server.Handler; + + // Act & Assert + var ex = await Assert.ThrowsAsync + (async () => await client.GetAsync("http://localhost/Users")); + + Assert.Equal("The view 'Index' was not found." + + " The following locations were searched:\r\n/Areas/Users/Views/Home/Index.cshtml\r\n" + + "/Areas/Users/Views/Shared/Index.cshtml\r\n/Views/Shared/Index.cshtml.", + ex.Message); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj b/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj index 5f09412809..030e6bfb80 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Microsoft.AspNet.Mvc.FunctionalTests.kproj @@ -30,6 +30,8 @@ + + diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs new file mode 100644 index 0000000000..1ddb481e24 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/TestHelper.cs @@ -0,0 +1,56 @@ +// 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.IO; +using System.Reflection; +using System.Threading.Tasks; +using InlineConstraints; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Routing; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; +using Microsoft.Framework.Runtime; +using Microsoft.Framework.Runtime.Infrastructure; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public static class TestHelper + { + public static IServiceProvider CreateServices(string applicationWebSiteName) + { + var originalProvider = CallContextServiceLocator.Locator.ServiceProvider; + IApplicationEnvironment appEnvironment = originalProvider.GetService(); + + // When an application executes in a regular context, the application base path points to the root + // directory where the application is located, for example MvcSample.Web. However, when executing + // an aplication as part of a test, the ApplicationBasePath of the IApplicationEnvironment points + // to the root folder of the test project. + // To compensate for this, we need to calculate the original path and override the application + // environment value so that components like the view engine work properly in the context of the + // test. + string appBasePath = CalculateApplicationBasePath(appEnvironment, applicationWebSiteName); + var provider = new ServiceCollection() + .AddInstance(typeof(IApplicationEnvironment), + new TestApplicationEnvironment(appEnvironment, appBasePath)) + .BuildServiceProvider(originalProvider); + + return provider; + } + + // Calculate the path relative to the application base path. + public static string CalculateApplicationBasePath(IApplicationEnvironment appEnvironment, string applicationWebSiteName) + { + // Mvc/test/Microsoft.AspNet.Mvc.FunctionalTests + var appBase = appEnvironment.ApplicationBasePath; + + // Mvc/test + var test = Path.GetDirectoryName(appBase); + + // Mvc/test/WebSites/applicationWebSiteName + return Path.GetFullPath(Path.Combine(appBase, "..", "WebSites", applicationWebSiteName)); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/config/InlineConstraintTestsConfig.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/config/InlineConstraintTestsConfig.json new file mode 100644 index 0000000000..95d271a2d8 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/config/InlineConstraintTestsConfig.json @@ -0,0 +1,10 @@ +{ + "TemplateCollection": { + "areaRoute": { + "TemplateValue": "" + }, + "actionAsMethod": { + "TemplateValue": "" + } + } +} \ 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 de453d5f84..cee07bd612 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -5,12 +5,14 @@ }, "dependencies": { "BasicWebSite": "", + "InlineConstraintsWebSite": "", "Microsoft.AspNet.TestHost": "0.1-alpha-*", "Microsoft.Framework.Runtime.Interfaces": "0.1-alpha-*", "xunit.abstractions": "2.0.0-aspnet-*", "xunit.assert": "2.0.0-aspnet-*", "xunit.core": "2.0.0-aspnet-*", - "Xunit.KRunner": "0.1-alpha-*" + "Xunit.KRunner": "0.1-alpha-*", + "Microsoft.Framework.ConfigurationModel.Json": "0.1-alpha-*" }, "commands": { "test": "Xunit.KRunner" diff --git a/test/WebSites/BasicWebSite/BasicWebSite.kproj b/test/WebSites/BasicWebSite/BasicWebSite.kproj index 6da9f930bc..595e02ecc3 100644 --- a/test/WebSites/BasicWebSite/BasicWebSite.kproj +++ b/test/WebSites/BasicWebSite/BasicWebSite.kproj @@ -29,6 +29,7 @@ + diff --git a/test/WebSites/BasicWebSite/Controllers/UsersController.cs b/test/WebSites/BasicWebSite/Controllers/UsersController.cs new file mode 100644 index 0000000000..9bb858489a --- /dev/null +++ b/test/WebSites/BasicWebSite/Controllers/UsersController.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNet.Mvc; + +namespace BasicWebSite.Controllers +{ + public class UsersController : Controller + { + public IActionResult Index() + { + return Content("Users.Index"); + } + } +} \ No newline at end of file diff --git a/test/WebSites/BasicWebSite/Startup.cs b/test/WebSites/BasicWebSite/Startup.cs index ea3e18b75e..a411d0998d 100644 --- a/test/WebSites/BasicWebSite/Startup.cs +++ b/test/WebSites/BasicWebSite/Startup.cs @@ -21,8 +21,13 @@ namespace BasicWebSite // Add MVC to the request pipeline app.UseMvc(routes => { + routes.MapRoute("areaRoute", + "{area:exists}/{controller}/{action}", + new { controller = "Home", action = "Index" }); + routes.MapRoute("ActionAsMethod", "{controller}/{action}", defaults: new { controller = "Home", action = "Index" }); + }); } } diff --git a/test/WebSites/InlineConstraintsWebSite/App_Data/config.json b/test/WebSites/InlineConstraintsWebSite/App_Data/config.json new file mode 100644 index 0000000000..6ef90bc771 --- /dev/null +++ b/test/WebSites/InlineConstraintsWebSite/App_Data/config.json @@ -0,0 +1,10 @@ +{ + "TemplateCollection" : { + "areaRoute" : { + "TemplateValue" : "{area:exists}/{controller=Home}/{action=Index}" + }, + "actionRoute" : { + "TemplateValue" : "{controller=Home}/{action=Index}" + } + } +} \ No newline at end of file diff --git a/test/WebSites/InlineConstraintsWebSite/Controllers/HomeController.cs b/test/WebSites/InlineConstraintsWebSite/Controllers/HomeController.cs new file mode 100644 index 0000000000..7ab8b8e1bc --- /dev/null +++ b/test/WebSites/InlineConstraintsWebSite/Controllers/HomeController.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNet.Mvc; + +namespace InlineConstraints.Controllers +{ + public class HomeController : Controller + { + public IActionResult Index() + { + return View(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/InlineConstraintsWebSite/Controllers/UsersController.cs b/test/WebSites/InlineConstraintsWebSite/Controllers/UsersController.cs new file mode 100644 index 0000000000..ff873f3c4e --- /dev/null +++ b/test/WebSites/InlineConstraintsWebSite/Controllers/UsersController.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNet.Mvc; + +namespace InlineConstraints.Controllers +{ + public class UsersController : Controller + { + public IActionResult Index() + { + return Content("Users.Index"); + } + } +} \ No newline at end of file diff --git a/test/WebSites/InlineConstraintsWebSite/InlineConstraintsWebSite.kproj b/test/WebSites/InlineConstraintsWebSite/InlineConstraintsWebSite.kproj new file mode 100644 index 0000000000..d695d4c847 --- /dev/null +++ b/test/WebSites/InlineConstraintsWebSite/InlineConstraintsWebSite.kproj @@ -0,0 +1,47 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + Debug + AnyCPU + + + + ea34877f-1ac1-42b7-b4e6-15a093f40cae + Web + + + ConsoleDebugger + + + WebDebugger + + + + + 2.0 + + + 38821 + + + 624661 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/WebSites/InlineConstraintsWebSite/Startup.cs b/test/WebSites/InlineConstraintsWebSite/Startup.cs new file mode 100644 index 0000000000..081c14edcd --- /dev/null +++ b/test/WebSites/InlineConstraintsWebSite/Startup.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Routing; +using Microsoft.Framework.ConfigurationModel; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Runtime; + +namespace InlineConstraints +{ + public class Startup + { + public Action RouteCollectionProvider { get; set; } + public void Configure(IBuilder app) + { + // Set up application services + app.UseServices(services => + { + // Add MVC services to the services container + services.AddMvc(); + + // Add a custom assembly provider so that we add only controllers present in + // this assembly. + services.AddTransient(); + }); + + var config = new Configuration(); + config.AddEnvironmentVariables(); + + string appConfigPath; + if (config.TryGet("AppConfigPath", out appConfigPath)) + { + config.AddJsonFile(appConfigPath); + } + else + { + var basePath = app.ApplicationServices.GetService().ApplicationBasePath; + config.AddJsonFile(Path.Combine(basePath, @"App_Data\config.json")); + } + + // Add MVC to the request pipeline + app.UseMvc(routes=> { + foreach (var item in GetDataFromConfig(config)) + { + routes.MapRoute(item.RouteName, item.RouteTemplateValue); + } + }); + } + + private IEnumerable GetDataFromConfig(IConfiguration config) + { + foreach (var template in config.GetSubKey("TemplateCollection").GetSubKeys()) + { + yield return + new RouteConfigData() + { + RouteName = template.Key, + RouteTemplateValue = template.Value.Get("TemplateValue") + }; + } + } + + private class RouteConfigData + { + public string RouteName { get; set; } + public string RouteTemplateValue { get; set; } + } + } +} diff --git a/test/WebSites/InlineConstraintsWebSite/TestControllerAssemblyProvider.cs b/test/WebSites/InlineConstraintsWebSite/TestControllerAssemblyProvider.cs new file mode 100644 index 0000000000..66b08bec8d --- /dev/null +++ b/test/WebSites/InlineConstraintsWebSite/TestControllerAssemblyProvider.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Reflection; +using Microsoft.AspNet.Mvc; + +namespace InlineConstraints +{ + public class TestControllerAssemblyProvider : IControllerAssemblyProvider + { + public IEnumerable CandidateAssemblies + { + get + { + return new[] { typeof(TestControllerAssemblyProvider).GetTypeInfo().Assembly }; + } + } + } +} diff --git a/test/WebSites/InlineConstraintsWebSite/Views/Home/Index.cshtml b/test/WebSites/InlineConstraintsWebSite/Views/Home/Index.cshtml new file mode 100644 index 0000000000..11137a53dc --- /dev/null +++ b/test/WebSites/InlineConstraintsWebSite/Views/Home/Index.cshtml @@ -0,0 +1,8 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Inline Constraints Home Page"; +} + +
+

ASP.NET vNext

+
\ No newline at end of file diff --git a/test/WebSites/InlineConstraintsWebSite/Views/Shared/Error.cshtml b/test/WebSites/InlineConstraintsWebSite/Views/Shared/Error.cshtml new file mode 100644 index 0000000000..5d73bf597b --- /dev/null +++ b/test/WebSites/InlineConstraintsWebSite/Views/Shared/Error.cshtml @@ -0,0 +1,7 @@ +@{ + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Error"; +} + +

Error.

+

An error occurred while processing your request.

\ No newline at end of file diff --git a/test/WebSites/InlineConstraintsWebSite/Views/Shared/_Layout.cshtml b/test/WebSites/InlineConstraintsWebSite/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000000..73698bac3e --- /dev/null +++ b/test/WebSites/InlineConstraintsWebSite/Views/Shared/_Layout.cshtml @@ -0,0 +1,30 @@ + + + + + + @ViewBag.Title - My ASP.NET Application + + + +
+
+
+ @Html.ActionLink("InlineConstraintsWebApplication", "Index", "Home", new { area = "" }) +
+
+
    +
  • @Html.ActionLink("Home", "Index", "Home")
  • +
+
+
+
+
+ @RenderBody() +
+ +
+ + \ No newline at end of file diff --git a/test/WebSites/InlineConstraintsWebSite/project.json b/test/WebSites/InlineConstraintsWebSite/project.json new file mode 100644 index 0000000000..94e888cd5f --- /dev/null +++ b/test/WebSites/InlineConstraintsWebSite/project.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "Helios": "0.1-alpha-*", + "Microsoft.AspNet.Mvc": "", + "Microsoft.Framework.ConfigurationModel.Json": "0.1-alpha-*" + }, + "configurations": { + "net45": { }, + "k10": { } + } +}