Separate ApiControllers from MVC Controllers in routing
This commit is contained in:
parent
3968df90e4
commit
9ad3d5e68f
|
|
@ -11,6 +11,7 @@ using Microsoft.Framework.DependencyInjection;
|
||||||
|
|
||||||
namespace System.Web.Http
|
namespace System.Web.Http
|
||||||
{
|
{
|
||||||
|
[UseWebApiRoutes]
|
||||||
[UseWebApiActionConventions]
|
[UseWebApiActionConventions]
|
||||||
[UseWebApiOverloading]
|
[UseWebApiOverloading]
|
||||||
public abstract class ApiController : IDisposable
|
public abstract class ApiController : IDisposable
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,8 @@ namespace Microsoft.AspNet.Mvc.WebApiCompatShim
|
||||||
{
|
{
|
||||||
public class WebApiCompatShimOptionsSetup : IOptionsAction<MvcOptions>, IOptionsAction<WebApiCompatShimOptions>
|
public class WebApiCompatShimOptionsSetup : IOptionsAction<MvcOptions>, IOptionsAction<WebApiCompatShimOptions>
|
||||||
{
|
{
|
||||||
|
public readonly static string DefaultAreaName = "api";
|
||||||
|
|
||||||
public int Order
|
public int Order
|
||||||
{
|
{
|
||||||
// We want to run after the default MvcOptionsSetup.
|
// 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
|
// Add webapi behaviors to controllers with the appropriate attributes
|
||||||
options.ApplicationModelConventions.Add(new WebApiActionConventionsGlobalModelConvention());
|
options.ApplicationModelConventions.Add(new WebApiActionConventionsGlobalModelConvention());
|
||||||
options.ApplicationModelConventions.Add(new WebApiOverloadingGlobalModelConvention());
|
options.ApplicationModelConventions.Add(new WebApiOverloadingGlobalModelConvention());
|
||||||
|
options.ApplicationModelConventions.Add(new WebApiRoutesGlobalModelConvention(area: DefaultAreaName));
|
||||||
|
|
||||||
// Add a model binder to be able to bind HttpRequestMessage
|
// Add a model binder to be able to bind HttpRequestMessage
|
||||||
options.ModelBinders.Insert(0, new HttpRequestMessageModelBinder());
|
options.ModelBinders.Insert(0, new HttpRequestMessageModelBinder());
|
||||||
|
|
|
||||||
|
|
@ -303,7 +303,6 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
Assert.Equal(mediaType, response.Content.Headers.ContentType.MediaType);
|
Assert.Equal(mediaType, response.Content.Headers.ContentType.MediaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ApiController_CreateResponse_HardcodedFormatter()
|
public async Task ApiController_CreateResponse_HardcodedFormatter()
|
||||||
{
|
{
|
||||||
|
|
@ -327,6 +326,42 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
Assert.Equal("Test User", user.Name);
|
Assert.Equal("Test User", user.Name);
|
||||||
Assert.Equal("text/json", response.Content.Headers.ContentType.MediaType);
|
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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
private INestedProviderManager<ActionDescriptorProviderContext> CreateProvider()
|
||||||
{
|
{
|
||||||
var assemblyProvider = new Mock<IControllerAssemblyProvider>();
|
var assemblyProvider = new Mock<IControllerAssemblyProvider>();
|
||||||
|
|
@ -240,8 +265,9 @@ namespace System.Web.Http
|
||||||
var conventions = new NamespaceLimitedActionDiscoveryConventions();
|
var conventions = new NamespaceLimitedActionDiscoveryConventions();
|
||||||
|
|
||||||
var options = new MvcOptions();
|
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>>();
|
var optionsAccessor = new Mock<IOptionsAccessor<MvcOptions>>();
|
||||||
optionsAccessor
|
optionsAccessor
|
||||||
|
|
|
||||||
|
|
@ -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!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// 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.Builder;
|
||||||
using Microsoft.AspNet.Mvc;
|
|
||||||
using Microsoft.AspNet.Routing;
|
using Microsoft.AspNet.Routing;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
|
||||||
|
|
@ -23,12 +22,15 @@ namespace WebApiCompatShimWebSite
|
||||||
|
|
||||||
app.UseMvc(routes =>
|
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
|
// Tests include different styles of WebAPI conventional routing and action selection - the prefix keeps
|
||||||
// them from matching too eagerly.
|
// them from matching too eagerly.
|
||||||
routes.MapRoute("named-action", "api/Blog/{controller}/{action}/{id?}");
|
routes.MapWebApiRoute("named-action", "api/Blog/{controller}/{action}/{id?}");
|
||||||
routes.MapRoute("unnamed-action", "api/Admin/{controller}/{id?}");
|
routes.MapWebApiRoute("unnamed-action", "api/Admin/{controller}/{id?}");
|
||||||
routes.MapRoute("name-as-parameter", "api/Store/{controller}/{name?}");
|
routes.MapWebApiRoute("name-as-parameter", "api/Store/{controller}/{name?}");
|
||||||
routes.MapRoute("extra-parameter", "api/Support/{extra}/{controller}/{id?}");
|
routes.MapWebApiRoute("extra-parameter", "api/Support/{extra}/{controller}/{id?}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue