[Fixes #3732] Simplify controller discovery.
* Introduce ControllerAttribute and use it to mark base classes as controllers. * Changed rules for controller discovery to: * All controller types must be public, concrete, non open generic types. * NotController attribute is not applied to any type oif the hierarchy. * The type name ends with controller. * Controller attribute is applied to the type or to one of its ancestors.
This commit is contained in:
parent
680e9bb2d1
commit
de9ffb13c7
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) .NET Foundation. 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.AspNetCore.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that the type and any derived types that this attribute is applied to
|
||||
/// are considered a controller by the default controller discovery mechanism, unless
|
||||
/// <see cref="NonControllerAttribute"/> is applied to any type in the hierarchy.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
|
||||
public class ControllerAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <summary>
|
||||
/// A base class for an MVC controller without view support.
|
||||
/// </summary>
|
||||
[Controller]
|
||||
public abstract class ControllerBase
|
||||
{
|
||||
private ControllerContext _controllerContext;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
{
|
||||
var candidateAssemblies = new HashSet<Assembly>(_assemblyProvider.CandidateAssemblies);
|
||||
var types = candidateAssemblies.SelectMany(a => a.DefinedTypes);
|
||||
return types.Where(typeInfo => IsController(typeInfo, candidateAssemblies));
|
||||
return types.Where(typeInfo => IsController(typeInfo));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -44,79 +44,48 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
/// Returns <c>true</c> if the <paramref name="typeInfo"/> is a controller. Otherwise <c>false</c>.
|
||||
/// </summary>
|
||||
/// <param name="typeInfo">The <see cref="TypeInfo"/>.</param>
|
||||
/// <param name="candidateAssemblies">The set of candidate assemblies.</param>
|
||||
/// <returns><c>true</c> if the <paramref name="typeInfo"/> is a controller. Otherwise <c>false</c>.</returns>
|
||||
protected internal virtual bool IsController(
|
||||
TypeInfo typeInfo,
|
||||
ISet<Assembly> candidateAssemblies)
|
||||
protected internal virtual bool IsController(TypeInfo typeInfo)
|
||||
{
|
||||
if (typeInfo == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(typeInfo));
|
||||
}
|
||||
|
||||
if (candidateAssemblies == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(candidateAssemblies));
|
||||
}
|
||||
|
||||
if (!typeInfo.IsClass)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeInfo.IsAbstract)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// We only consider public top-level classes as controllers. IsPublic returns false for nested
|
||||
// classes, regardless of visibility modifiers
|
||||
if (!typeInfo.IsPublic)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeInfo.ContainsGenericParameters)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!typeInfo.Name.EndsWith(ControllerTypeName, StringComparison.OrdinalIgnoreCase) &&
|
||||
!DerivesFromController(typeInfo, candidateAssemblies))
|
||||
|
||||
if (typeInfo.IsDefined(typeof(NonControllerAttribute)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (typeInfo.IsDefined(typeof(NonControllerAttribute)))
|
||||
|
||||
if (!typeInfo.Name.EndsWith(ControllerTypeName, StringComparison.OrdinalIgnoreCase) &&
|
||||
!typeInfo.IsDefined(typeof(ControllerAttribute)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool DerivesFromController(TypeInfo typeInfo, ISet<Assembly> candidateAssemblies)
|
||||
{
|
||||
while (typeInfo != ObjectTypeInfo)
|
||||
{
|
||||
var baseTypeInfo = typeInfo.BaseType.GetTypeInfo();
|
||||
|
||||
// A base type will be treated as a controller if
|
||||
// a) it ends in the term "Controller" and
|
||||
// b) it's assembly is one of the candidate assemblies we're considering. This ensures that the assembly
|
||||
// the base type is declared in references Mvc.
|
||||
if (baseTypeInfo.Name.EndsWith(ControllerTypeName, StringComparison.Ordinal) &&
|
||||
candidateAssemblies.Contains(baseTypeInfo.Assembly))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// c). The base type is called 'Controller.
|
||||
if (string.Equals(baseTypeInfo.Name, ControllerTypeName, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
typeInfo = baseTypeInfo;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ namespace System.Web.Http
|
|||
[UseWebApiActionConventions]
|
||||
[UseWebApiParameterConventions]
|
||||
[UseWebApiOverloading]
|
||||
[Controller]
|
||||
public abstract class ApiController : IDisposable
|
||||
{
|
||||
private ControllerContext _controllerContext;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.True(isController);
|
||||
|
|
@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.True(isController);
|
||||
|
|
@ -53,10 +53,10 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.False(isController);
|
||||
Assert.True(isController);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.False(isController);
|
||||
|
|
@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.False(isController);
|
||||
|
|
@ -95,7 +95,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.False(isController);
|
||||
|
|
@ -109,7 +109,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.False(isController);
|
||||
|
|
@ -123,7 +123,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.False(isController);
|
||||
|
|
@ -137,7 +137,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.True(isController);
|
||||
|
|
@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.False(isController);
|
||||
|
|
@ -165,7 +165,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.False(isController);
|
||||
|
|
@ -179,7 +179,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.True(isController);
|
||||
|
|
@ -193,7 +193,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.True(isController);
|
||||
|
|
@ -207,7 +207,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.True(isController);
|
||||
|
|
@ -221,7 +221,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeInfo, CandidateAssemblies);
|
||||
var isController = provider.IsController(typeInfo);
|
||||
|
||||
// Assert
|
||||
Assert.True(isController);
|
||||
|
|
@ -230,18 +230,31 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
[Theory]
|
||||
[InlineData(typeof(DescendantLevel1))]
|
||||
[InlineData(typeof(DescendantLevel2))]
|
||||
public void IsController_ReturnsTrue_IfAncestorTypeNameHasControllerSuffix(Type type)
|
||||
public void IsController_ReturnsTrue_IfAncestorTypeHasControllerAttribute(Type type)
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(type.GetTypeInfo(), CandidateAssemblies);
|
||||
var isController = provider.IsController(type.GetTypeInfo());
|
||||
|
||||
// Assert
|
||||
Assert.True(isController);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsController_ReturnsFalse_IfAncestorTypeDoesNotHaveControllerAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(typeof(NoSuffixNoControllerAttribute).GetTypeInfo());
|
||||
|
||||
// Assert
|
||||
Assert.False(isController);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(BaseNonControllerController))]
|
||||
[InlineData(typeof(BaseNonControllerControllerChild))]
|
||||
|
|
@ -249,6 +262,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
[InlineData(typeof(BasePocoNonControllerControllerChild))]
|
||||
[InlineData(typeof(NonController))]
|
||||
[InlineData(typeof(NonControllerChild))]
|
||||
[InlineData(typeof(BaseNonControllerAttributeChildControllerControllerAttributeController))]
|
||||
[InlineData(typeof(PersonModel))] // Verifies that POCO type hierarchies that don't derive from controller return false.
|
||||
public void IsController_ReturnsFalse_IfTypeOrAncestorHasNonControllerAttribute(Type type)
|
||||
{
|
||||
|
|
@ -256,7 +270,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
|
|||
var provider = GetControllerTypeProvider();
|
||||
|
||||
// Act
|
||||
var isController = provider.IsController(type.GetTypeInfo(), CandidateAssemblies);
|
||||
var isController = provider.IsController(type.GetTypeInfo());
|
||||
|
||||
// Assert
|
||||
Assert.False(isController);
|
||||
|
|
@ -297,11 +311,19 @@ namespace Microsoft.AspNetCore.Mvc.DefaultControllerTypeProviderControllers
|
|||
{
|
||||
}
|
||||
|
||||
|
||||
[Controller]
|
||||
public abstract class Controller
|
||||
{
|
||||
}
|
||||
|
||||
public abstract class NoControllerAttributeBaseController
|
||||
{
|
||||
}
|
||||
|
||||
public class NoSuffixNoControllerAttribute : NoControllerAttributeBaseController
|
||||
{
|
||||
}
|
||||
|
||||
public class OpenGenericController<T> : Controller
|
||||
{
|
||||
}
|
||||
|
|
@ -327,17 +349,19 @@ namespace Microsoft.AspNetCore.Mvc.DefaultControllerTypeProviderControllers
|
|||
{
|
||||
}
|
||||
|
||||
public class CustomBaseController
|
||||
[Controller]
|
||||
public class CustomBase
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Controller]
|
||||
public abstract class CustomAbstractBaseController
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class DescendantLevel1 : CustomBaseController
|
||||
public class DescendantLevel1 : CustomBase
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -369,6 +393,12 @@ namespace Microsoft.AspNetCore.Mvc.DefaultControllerTypeProviderControllers
|
|||
|
||||
}
|
||||
|
||||
[Controller]
|
||||
public class BaseNonControllerAttributeChildControllerControllerAttributeController : BaseNonControllerController
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class BaseNonControllerControllerChild : BaseNonControllerController
|
||||
{
|
||||
|
||||
|
|
|
|||
|
|
@ -331,16 +331,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
Assert.Equal("This is a basic website.", responseData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TypesWithoutControllerSuffix_DerivingFromTypesWithControllerSuffix_CanBeAccessed()
|
||||
{
|
||||
// Act
|
||||
var response = await Client.GetStringAsync("appointments");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("2 appointments available.", response);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TypesMarkedAsNonAction_AreInaccessible()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TypesDerivingFromControllerPrefixedTypesAreRegistered()
|
||||
public async Task TypesDerivingFromTypesWithControllerAttributeAreRegistered()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "4";
|
||||
|
|
|
|||
|
|
@ -55,9 +55,9 @@ namespace System.Web.Http
|
|||
|
||||
// Assert
|
||||
var controllerType = typeof(TestControllers.Blog).GetTypeInfo();
|
||||
var actions = results.Where(ad => ad.ControllerTypeInfo == controllerType).ToArray();
|
||||
var actions = results.Where(ad => ad.ControllerTypeInfo == controllerType);
|
||||
|
||||
Assert.Empty(actions);
|
||||
Assert.Equal(2, actions.Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace BasicWebSite.Controllers
|
||||
{
|
||||
public abstract class ApplicationBaseController
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BasicWebSite.Controllers
|
||||
{
|
||||
[Route("/appointments")]
|
||||
public class Appointments : ApplicationBaseController
|
||||
{
|
||||
[HttpGet("")]
|
||||
public IActionResult Get()
|
||||
{
|
||||
return new ContentResult
|
||||
{
|
||||
Content = "2 appointments available."
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||
|
||||
namespace ControllersFromServicesClassLibrary
|
||||
{
|
||||
[Controller]
|
||||
[Route("/[controller]")]
|
||||
public class ResourcesController
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue