Separate ApiControllers from MVC Controllers in routing

This commit is contained in:
Ryan Nowak 2014-10-10 12:57:06 -07:00
parent 3968df90e4
commit 9ad3d5e68f
10 changed files with 221 additions and 9 deletions

View File

@ -11,6 +11,7 @@ using Microsoft.Framework.DependencyInjection;
namespace System.Web.Http
{
[UseWebApiRoutes]
[UseWebApiActionConventions]
[UseWebApiOverloading]
public abstract class ApiController : IDisposable

View File

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

View File

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

View File

@ -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<IUseWebApiRoutes>().Any();
}
private void Apply(ControllerModel model)
{
model.RouteConstraints.Add(new AreaAttribute(_area));
}
}
}

View File

@ -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<string, object> ObjectToDictionary(object value)
{
var dictionary = value as IDictionary<string, object>;
if (dictionary != null)
{
return new RouteValueDictionary(dictionary);
}
else
{
return new RouteValueDictionary(value);
}
}
}
}

View File

@ -8,6 +8,8 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
{
public class WebApiCompatShimOptionsSetup : IOptionsAction<MvcOptions>, IOptionsAction<WebApiCompatShimOptions>
{
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());

View File

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

View File

@ -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<ControllerActionDescriptor>();
// 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<ActionDescriptorProviderContext> CreateProvider()
{
var assemblyProvider = new Mock<IControllerAssemblyProvider>();
@ -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<IOptionsAccessor<MvcOptions>>();
optionsAccessor

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 WebApiCompatShimWebSite
{
// This is reachable via our MVC routes, but not webapi routes
public class MvcController : Controller
{
public string Index()
{
return "Hello, World!";
}
}
}

View File

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