diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs index efcefa8f32..7a503c84bf 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/ApiController.cs @@ -11,6 +11,7 @@ using Microsoft.Framework.DependencyInjection; namespace System.Web.Http { + [UseWebApiRoutes] [UseWebApiActionConventions] [UseWebApiOverloading] public abstract class ApiController : IDisposable diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiRoutes.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiRoutes.cs new file mode 100644 index 0000000000..51dae1638c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/IUseWebApiRoutes.cs @@ -0,0 +1,9 @@ +// 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. + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public interface IUseWebApiRoutes + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiRoutesAttribute.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiRoutesAttribute.cs new file mode 100644 index 0000000000..cfdf5f7fe0 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/UseWebApiRoutesAttribute.cs @@ -0,0 +1,12 @@ +// 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; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class UseWebApiRoutesAttribute : Attribute, IUseWebApiRoutes + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiRoutesGlobalModelConvention.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiRoutesGlobalModelConvention.cs new file mode 100644 index 0000000000..e9dff1f49f --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Conventions/WebApiRoutesGlobalModelConvention.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using Microsoft.AspNet.Mvc.ApplicationModel; + +namespace Microsoft.AspNet.Mvc.WebApiCompatShim +{ + public class WebApiRoutesGlobalModelConvention : IGlobalModelConvention + { + private readonly string _area; + + public WebApiRoutesGlobalModelConvention(string area) + { + _area = area; + } + + public void Apply(GlobalModel model) + { + foreach (var controller in model.Controllers) + { + if (IsConventionApplicable(controller)) + { + Apply(controller); + } + } + } + + private bool IsConventionApplicable(ControllerModel controller) + { + return controller.Attributes.OfType().Any(); + } + + private void Apply(ControllerModel model) + { + model.RouteConstraints.Add(new AreaAttribute(_area)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Routing/RouteBuilderExtensions.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Routing/RouteBuilderExtensions.cs new file mode 100644 index 0000000000..7cf2cadbd2 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/Routing/RouteBuilderExtensions.cs @@ -0,0 +1,69 @@ +// 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.Collections.Generic; +using Microsoft.AspNet.Mvc.WebApiCompatShim; +using Microsoft.AspNet.Routing.Constraints; + +namespace Microsoft.AspNet.Routing +{ + public static class RouteBuilderExtensions + { + public static IRouteBuilder MapWebApiRoute( + this IRouteBuilder routeCollectionBuilder, + string name, + string template) + { + return MapWebApiRoute(routeCollectionBuilder, name, template, defaults: null); + } + + public static IRouteBuilder MapWebApiRoute( + this IRouteBuilder routeCollectionBuilder, + string name, + string template, + object defaults) + { + return MapWebApiRoute(routeCollectionBuilder, name, template, defaults, constraints: null); + } + + public static IRouteBuilder MapWebApiRoute( + this IRouteBuilder routeCollectionBuilder, + string name, + string template, + object defaults, + object constraints) + { + return MapWebApiRoute(routeCollectionBuilder, name, template, defaults, constraints, dataTokens: null); + } + + public static IRouteBuilder MapWebApiRoute( + this IRouteBuilder routeCollectionBuilder, + string name, + string template, + object defaults, + object constraints, + object dataTokens) + { + var mutableDefaults = ObjectToDictionary(defaults); + mutableDefaults.Add("area", WebApiCompatShimOptionsSetup.DefaultAreaName); + + var mutableConstraints = ObjectToDictionary(constraints); + mutableConstraints.Add("area", new RequiredRouteConstraint()); + + return routeCollectionBuilder.MapRoute(name, template, mutableDefaults, mutableConstraints, dataTokens); + } + + private static IDictionary ObjectToDictionary(object value) + { + var dictionary = value as IDictionary; + if (dictionary != null) + { + return new RouteValueDictionary(dictionary); + } + else + { + return new RouteValueDictionary(value); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs index 142a66d2ad..6d3f576bb5 100644 --- a/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.WebApiCompatShim/WebApiCompatShimOptionsSetup.cs @@ -8,6 +8,8 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim { public class WebApiCompatShimOptionsSetup : IOptionsAction, IOptionsAction { + public readonly static string DefaultAreaName = "api"; + public int Order { // We want to run after the default MvcOptionsSetup. @@ -21,6 +23,7 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim // Add webapi behaviors to controllers with the appropriate attributes options.ApplicationModelConventions.Add(new WebApiActionConventionsGlobalModelConvention()); options.ApplicationModelConventions.Add(new WebApiOverloadingGlobalModelConvention()); + options.ApplicationModelConventions.Add(new WebApiRoutesGlobalModelConvention(area: DefaultAreaName)); // Add a model binder to be able to bind HttpRequestMessage options.ModelBinders.Insert(0, new HttpRequestMessageModelBinder()); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs index c9e2ff955b..77b819b3b4 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/WebApiCompatShimBasicTest.cs @@ -303,7 +303,6 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal(mediaType, response.Content.Headers.ContentType.MediaType); } - [Fact] public async Task ApiController_CreateResponse_HardcodedFormatter() { @@ -327,6 +326,42 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal("Test User", user.Name); Assert.Equal("text/json", response.Content.Headers.ContentType.MediaType); } + + [Theory] + [InlineData("http://localhost/Mvc/Index", HttpStatusCode.OK)] + [InlineData("http://localhost/api/Blog/Mvc/Index", HttpStatusCode.NotFound)] + public async Task WebApiRouting_AccessMvcController(string url, HttpStatusCode expected) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, url); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(expected, response.StatusCode); + } + + [Theory] + [InlineData("http://localhost/BasicApi/GenerateUrl", HttpStatusCode.NotFound)] + [InlineData("http://localhost/api/Blog/BasicApi/GenerateUrl", HttpStatusCode.OK)] + public async Task WebApiRouting_AccessWebApiController(string url, HttpStatusCode expected) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, url); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(expected, response.StatusCode); + } } } -#endif \ No newline at end of file +#endif diff --git a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs index 73a7f9b17c..7fbd35ed9c 100644 --- a/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs +++ b/test/Microsoft.AspNet.Mvc.WebApiCompatShimTest/ApiControllerActionDiscoveryTest.cs @@ -225,6 +225,31 @@ namespace System.Web.Http } } + [Fact] + public void GetActions_AllWebApiActionsAreInWebApiArea() + { + // Arrange + var provider = CreateProvider(); + + // Act + var context = new ActionDescriptorProviderContext(); + provider.Invoke(context); + + var results = context.Results.Cast(); + + // Assert + var controllerType = typeof(TestControllers.StoreController).GetTypeInfo(); + var actions = results + .Where(ad => ad.ControllerDescriptor.ControllerTypeInfo == controllerType) + .ToArray(); + + Assert.NotEmpty(actions); + foreach (var action in actions) + { + Assert.Single(action.RouteConstraints, c => c.RouteKey == "area" && c.RouteValue == "api"); + } + } + private INestedProviderManager CreateProvider() { var assemblyProvider = new Mock(); @@ -240,8 +265,9 @@ namespace System.Web.Http var conventions = new NamespaceLimitedActionDiscoveryConventions(); var options = new MvcOptions(); - options.ApplicationModelConventions.Add(new WebApiActionConventionsGlobalModelConvention()); - options.ApplicationModelConventions.Add(new WebApiOverloadingGlobalModelConvention()); + + var setup = new WebApiCompatShimOptionsSetup(); + setup.Invoke(options); var optionsAccessor = new Mock>(); optionsAccessor diff --git a/test/WebSites/WebApiCompatShimWebSite/Controllers/MvcController.cs b/test/WebSites/WebApiCompatShimWebSite/Controllers/MvcController.cs new file mode 100644 index 0000000000..aa3b75d4ec --- /dev/null +++ b/test/WebSites/WebApiCompatShimWebSite/Controllers/MvcController.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 WebApiCompatShimWebSite +{ + // This is reachable via our MVC routes, but not webapi routes + public class MvcController : Controller + { + public string Index() + { + return "Hello, World!"; + } + } +} \ No newline at end of file diff --git a/test/WebSites/WebApiCompatShimWebSite/Startup.cs b/test/WebSites/WebApiCompatShimWebSite/Startup.cs index c4dcf3d447..2bc4fd8915 100644 --- a/test/WebSites/WebApiCompatShimWebSite/Startup.cs +++ b/test/WebSites/WebApiCompatShimWebSite/Startup.cs @@ -2,7 +2,6 @@ // 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.Mvc; using Microsoft.AspNet.Routing; using Microsoft.Framework.DependencyInjection; @@ -23,12 +22,15 @@ namespace WebApiCompatShimWebSite app.UseMvc(routes => { + // This route can't access any of our webapi controllers + routes.MapRoute("default", "{controller}/{action}/{id?}"); + // Tests include different styles of WebAPI conventional routing and action selection - the prefix keeps // them from matching too eagerly. - routes.MapRoute("named-action", "api/Blog/{controller}/{action}/{id?}"); - routes.MapRoute("unnamed-action", "api/Admin/{controller}/{id?}"); - routes.MapRoute("name-as-parameter", "api/Store/{controller}/{name?}"); - routes.MapRoute("extra-parameter", "api/Support/{extra}/{controller}/{id?}"); + routes.MapWebApiRoute("named-action", "api/Blog/{controller}/{action}/{id?}"); + routes.MapWebApiRoute("unnamed-action", "api/Admin/{controller}/{id?}"); + routes.MapWebApiRoute("name-as-parameter", "api/Store/{controller}/{name?}"); + routes.MapWebApiRoute("extra-parameter", "api/Support/{extra}/{controller}/{id?}"); }); } }