Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
* Modify DefaultControllerTypeProvider to look at the object graph to
Browse files Browse the repository at this point in the history
determine if any ancestor has the "Controller" suffix.

* Introduce NonControllerAttribute to opt out of Controller detection.

Fixes #1274
  • Loading branch information
pranavkm committed Feb 13, 2015
1 parent a33e83f commit 264371f
Show file tree
Hide file tree
Showing 11 changed files with 344 additions and 9 deletions.
35 changes: 32 additions & 3 deletions src/Microsoft.AspNet.Mvc.Core/DefaultControllerTypeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace Microsoft.AspNet.Mvc
/// </summary>
public class DefaultControllerTypeProvider : IControllerTypeProvider
{
private const string ControllerTypeName = nameof(Controller);
private readonly IAssemblyProvider _assemblyProvider;
private readonly ILogger _logger;

Expand Down Expand Up @@ -76,17 +77,45 @@ protected internal virtual bool IsController([NotNull] TypeInfo typeInfo)
{
return false;
}
if (typeInfo.Name.Equals("Controller", StringComparison.OrdinalIgnoreCase))
if (typeInfo.Name.Equals(ControllerTypeName, StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (!typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
!typeof(Controller).GetTypeInfo().IsAssignableFrom(typeInfo))
if (!typeInfo.Name.EndsWith(ControllerTypeName, StringComparison.OrdinalIgnoreCase) &&
!DerivesFromController(typeInfo))
{
return false;
}
if (typeInfo.IsDefined(typeof(NonControllerAttribute)))
{
return false;
}

return true;
}

private static bool DerivesFromController(TypeInfo typeInfo)
{
// A type is a controller if it derives from a type that is either named "Controller" or has the suffix
// "Controller". We'll optimize the most common case of types deriving from the Mvc Controller type and
// walk up the object graph if that's not the case.
if (typeof(Controller).GetTypeInfo().IsAssignableFrom(typeInfo))
{
return true;
}

while (typeInfo != typeof(object).GetTypeInfo())
{
var baseTypeInfo = typeInfo.BaseType.GetTypeInfo();
if (baseTypeInfo.Name.EndsWith(ControllerTypeName, StringComparison.Ordinal))
{
return true;
}

typeInfo = baseTypeInfo;
}

return false;
}
}
}
16 changes: 16 additions & 0 deletions src/Microsoft.AspNet.Mvc.Core/NonControllerAttribute.cs
Original file line number Diff line number Diff line change
@@ -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 System;

namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Indicates that the type and any derived types that this attribute is applied to
/// is not considered a controller by the default controller discovery mechanism.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class NonControllerAttribute : Attribute
{
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// 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 System;
using System.Reflection;
using Microsoft.AspNet.Mvc.DefaultControllerTypeProviderControllers;
using Xunit;
Expand Down Expand Up @@ -108,6 +108,20 @@ public void IsController_OpenGenericClass()
Assert.False(isController);
}

[Fact]
public void IsController_WithoutSuffixOrAncestorWithController()
{
// Arrange
var typeInfo = typeof(NoSuffixPoco).GetTypeInfo();
var provider = GetControllerTypeProvider();

// Act
var isController = provider.IsController(typeInfo);

// Assert
Assert.False(isController);
}

[Fact]
public void IsController_ClosedGenericClass()
{
Expand Down Expand Up @@ -164,6 +178,55 @@ public void IsController_NoControllerSuffix()
Assert.True(isController);
}

public static TheoryData TypesWithPocoControllerAncestor
{
get
{
return new TheoryData<TypeInfo>
{
typeof(ChildWithoutSuffix).GetTypeInfo(),
typeof(DescendantLevel1).GetTypeInfo(),
typeof(DescendantLevel2).GetTypeInfo()
};
}
}

[Theory]
[InlineData(typeof(ChildWithoutSuffix))]
[InlineData(typeof(DescendantLevel1))]
[InlineData(typeof(DescendantLevel2))]
public void IsController_ReturnsTrue_IfAncestorTypeNameHasControllerSuffix(Type type)
{
// Arrange
var provider = GetControllerTypeProvider();

// Act
var isController = provider.IsController(type.GetTypeInfo());

// Assert
Assert.True(isController);
}

[Theory]
[InlineData(typeof(BaseNonControllerController))]
[InlineData(typeof(BaseNonControllerControllerChild))]
[InlineData(typeof(BasePocoNonControllerController))]
[InlineData(typeof(BasePocoNonControllerControllerChild))]
[InlineData(typeof(NonController))]
[InlineData(typeof(NonControllerChild))]
[InlineData(typeof(PersonModel))]
public void IsController_ReturnsFalse_IfTypeOrAncestorHasNonControllerAttribute(Type type)
{
// Arrange
var provider = GetControllerTypeProvider();

// Act
var isController = provider.IsController(type.GetTypeInfo());

// Assert
Assert.False(isController);
}

private static DefaultControllerTypeProvider GetControllerTypeProvider()
{
var assemblyProvider = new FixedSetAssemblyProvider();
Expand Down Expand Up @@ -211,7 +274,97 @@ public class NoSuffix : Mvc.Controller
{
}

public class NoSuffixPoco
{

}

public class PocoController
{
}

public class CustomBaseController
{

}

public abstract class CustomAbstractBaseController
{

}

public class ChildWithoutSuffix : CustomBaseController
{

}

public class DescendantLevel1 : CustomBaseController
{

}

public class DescendantLevel2 : DescendantLevel1
{

}

public class AbstractChildWithoutSuffix : CustomAbstractBaseController
{

}

[NonController]
public class BasePocoNonControllerController
{

}

public class BasePocoNonControllerControllerChild : BasePocoNonControllerController
{

}

[NonController]
public class BaseNonControllerController : Controller
{

}

public class BaseNonControllerControllerChild : BaseNonControllerController
{

}

[NonController]
public class PocoNonController
{

}

[NonController]
public class NonControllerChild : Controller
{

}

[NonController]
public class NonController : Controller
{

}

public class DataModelBase
{

}

public class EntityDataModel : DataModelBase
{

}

public class PersonModel : EntityDataModel
{

}
}
31 changes: 30 additions & 1 deletion test/Microsoft.AspNet.Mvc.FunctionalTests/BasicTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
Expand All @@ -18,7 +19,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public class BasicTests
{
private readonly IServiceProvider _provider = TestHelper.CreateServices("BasicWebSite");
private readonly IServiceProvider _provider = TestHelper.CreateServices(nameof(BasicWebSite));
private readonly Action<IApplicationBuilder> _app = new Startup().Configure;

// Some tests require comparing the actual response body against an expected response baseline
Expand Down Expand Up @@ -275,5 +276,33 @@ public async Task ConfigureMvcOptionsAddsOptionsProperly()
var responseData = await response.Content.ReadAsStringAsync();
Assert.Equal("This is a basic website.", responseData);
}

[Fact]
public async Task TypesWithoutControllerSuffix_DerivingFromTypesWithControllerSuffix_CanBeAccessed()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = new HttpClient(server.CreateHandler(), false);

// Act
var response = await client.GetStringAsync("http://localhost/appointments");

// Assert
Assert.Equal("2 appointments available.", response);
}

[Fact]
public async Task TypesMarkedAsNonAction_AreInaccessible()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = new HttpClient(server.CreateHandler(), false);

// Act
var response = await client.GetAsync("http://localhost/SqlData/TruncateAllDbRecords");

// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ public async Task TypesDerivingFromControllerAreRegistered()
Assert.Equal(expected, response);
}

[Fact]
public async Task TypesDerivingFromControllerPrefixedTypesAreRegistered()
{
// Arrange
var expected = "4";
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();

// Act
var response = await client.GetStringAsync("http://localhost/inventory/");

// Assert
Assert.Equal(expected, response);
}

[Fact]
public async Task TypesWithControllerSuffixAreRegistered()
{
Expand Down Expand Up @@ -84,17 +99,18 @@ public async Task TypesWithControllerSuffixAreConventionalRouted()
}

[Theory]
[InlineData("generic")]
[InlineData("nested")]
[InlineData("not-in-services")]
[InlineData("not-discovered/generic")]
[InlineData("not-discovered/nested")]
[InlineData("not-discovered/not-in-services")]
[InlineData("ClientUIStub/GetClientContent/5")]
public async Task AddControllersFromServices_UsesControllerDiscoveryContentions(string action)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();

// Act
var response = await client.GetAsync("http://localhost/not-discovered/" + action);
var response = await client.GetAsync("http://localhost/" + action);

// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// 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 BasicWebSite.Controllers
{
public abstract class ApplicationBaseController
{

}
}
20 changes: 20 additions & 0 deletions test/WebSites/BasicWebSite/Controllers/Appointments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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 BasicWebSite.Controllers
{
[Route("/appointments")]
public class Appointments : ApplicationBaseController
{
[HttpGet("")]
public IActionResult Get()
{
return new ContentResult
{
Content = "2 appointments available."
};
}
}
}

0 comments on commit 264371f

Please sign in to comment.