Permalink
Browse files

Fixing issue 150: Added support for nested controller types.

Controller types no longer have to be public top-level types. As long as they are visible outside the assembly then they can be nested types as well.
  • Loading branch information...
alexander-myltsev authored and HenrikFrystykNielsen committed Jul 20, 2012
1 parent 315208d commit c44fb644c5fc3964a2b1c65663f58d2d659140b3
@@ -5,7 +5,7 @@
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
using System.Web.Http.Internal;
using System.Web.Http.Controllers;
namespace System.Web.Http.Dispatcher
{
@@ -50,10 +50,10 @@ internal static bool IsControllerType(Type t)
return
t != null &&
t.IsClass &&
t.IsPublic &&
t.Name.EndsWith(DefaultHttpControllerSelector.ControllerSuffix, StringComparison.OrdinalIgnoreCase) &&
t.IsVisible &&
!t.IsAbstract &&
TypeHelper.HttpControllerType.IsAssignableFrom(t);
typeof(IHttpController).IsAssignableFrom(t) &&
HasValidControllerName(t);
}
/// <summary>
@@ -106,5 +106,17 @@ public virtual ICollection<Type> GetControllerTypes(IAssembliesResolver assembli
return result;
}
/// <summary>
/// We match if type name ends with "Controller" and that is not the only part of the
/// name (i.e it can't be just "Controller"). The reason is that the route name has to
/// be a non-empty prefix of the controller type name.
/// </summary>
internal static bool HasValidControllerName(Type controllerType)
{
Contract.Assert(controllerType != null);
string controllerSuffix = DefaultHttpControllerSelector.ControllerSuffix;
return controllerType.Name.Length > controllerSuffix.Length && controllerType.Name.EndsWith(controllerSuffix, StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -1,13 +1,10 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
namespace System.Web.Http.Internal
{
@@ -18,7 +15,6 @@ internal static class TypeHelper
{
private static readonly Type TaskGenericType = typeof(Task<>);
internal static readonly Type HttpControllerType = typeof(IHttpController);
internal static readonly Type ApiControllerType = typeof(ApiController);
internal static Type GetTaskInnerTypeOrNull(Type type)
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using Microsoft.TestCommon;
namespace System.Web.Http.Dispatcher
{
public class CustomControllerTypeResolverTest : HttpServerTestBase
{
internal static readonly string ExpectedContent = "Hello World!";
public CustomControllerTypeResolverTest()
: base("http://localhost/")
{
}
protected override void ApplyConfiguration(HttpConfiguration configuration)
{
// Add default route
configuration.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// Set our own assembly resolver where we add the assemblies we need
CustomControllerTypeResolver customHttpControllerTypeResolver = new CustomControllerTypeResolver();
configuration.Services.Replace(typeof(IHttpControllerTypeResolver), customHttpControllerTypeResolver);
}
[Fact]
public void CustomControllerTypeResolver_ReplacesControllerTypeAndNameConvention()
{
// Arrange
string address = BaseAddress + "api/custominternal";
// Act
HttpResponseMessage response = Client.GetAsync(address).Result;
string expectedContent = response.Content.ReadAsStringAsync().Result;
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(ExpectedContent, expectedContent);
}
}
internal class CustomControllerTypeResolver : IHttpControllerTypeResolver
{
public ICollection<Type> GetControllerTypes(IAssembliesResolver assembliesResolver)
{
return new List<Type> { typeof(CustomInternalController) };
}
}
/// <summary>
/// Test controller which is declared internal so wouldn't not get picked up by
/// <see cref="DefaultHttpControllerTypeResolver"/>.
/// </summary>
internal class CustomInternalController : ApiController
{
public HttpResponseMessage Get()
{
HttpResponseMessage response = Request.CreateResponse();
response.Content = new StringContent(CustomControllerTypeResolverTest.ExpectedContent);
return response;
}
}
}
@@ -91,6 +91,7 @@
<Compile Include="Controllers\ActionReachabilityTest.cs" />
<Compile Include="Controllers\Apis\ValuesController.cs" />
<Compile Include="Controllers\ControllerConfigurationTest.cs" />
<Compile Include="Dispatcher\CustomHttpControllerTypeResolverTest.cs" />
<Compile Include="Tracing\ITestTraceWriter.cs" />
<Compile Include="Tracing\MemoryTraceWriter.cs" />
<Compile Include="Tracing\NeverTracesTraceWriter.cs" />
@@ -15,22 +15,46 @@ public static TheoryDataSet<Type> ValidControllerTypes
{
return new TheoryDataSet<Type>
{
typeof(validcontroller),
typeof(ValidController),
typeof(VALIDController),
typeof(VALIDCONTROLLER),
typeof(ValidSealedController),
typeof(ValidPartialController),
typeof(ValidInheritedController),
typeof(ControllerWrapper.ValidNestedController),
typeof(ControllerWrapper.ValidInheritedNestedController),
typeof(ControllerWrapper.ControllerNestedWrapper.ValidNestedNestedController),
typeof(InheritedControllerHidingWrapper.ValidNestedController)
};
}
}
public static TheoryDataSet<Type> InvalidControllerTypes
public static TheoryDataSet<Type> InvalidControllerTypesWithValidNames
{
get
{
return new TheoryDataSet<Type>
{
typeof(InvalidAbstractController),
typeof(ControllerWrapper.InvalidAbstractNestedController),
ControllerWrapper.TypeOfInvalidProtectedNestedController(), // NOTE: Unable to get through typeof(ControllerWrapper.InvalidProtectedNestedController),
ControllerWrapper.TypeOfInvalidPrivateNestedController(), // NOTE: Unable to get through typeof(ControllerWrapper.InvalidPrivateNestedController),
typeof(ControllerWrapper.InvalidInternalNestedController)
};
}
}
public static TheoryDataSet<Type> InvalidControllerTypesWithInvalidNames
{
get
{
return new TheoryDataSet<Type>
{
typeof(Ctrl),
typeof(Controller),
typeof(ControllerPrefix),
typeof(InvalidControllerStruct),
typeof(ControllerWrapper.InvalidNestedController),
typeof(InvalidControllerAbstract),
typeof(InvalidControllerWithInconsistentName),
typeof(InvalidControllerWithNoBaseType)
};
@@ -51,12 +75,26 @@ public void IsControllerType_AcceptsValidControllerTypes(Type validControllerTyp
}
[Theory]
[PropertyData("InvalidControllerTypes")]
[PropertyData("InvalidControllerTypesWithValidNames"), PropertyData("InvalidControllerTypesWithInvalidNames")]
public void IsControllerType_RejectsInvalidControllerTypes(Type invalidControllerType)
{
Assert.False(DefaultHttpControllerTypeResolver.IsControllerType(invalidControllerType));
}
[Theory]
[PropertyData("ValidControllerTypes")]
public void HasValidControllerName_AcceptsValidControllerNames(Type validControllerType)
{
Assert.True(DefaultHttpControllerTypeResolver.HasValidControllerName(validControllerType));
}
[Theory]
[PropertyData("InvalidControllerTypesWithInvalidNames")]
public void HasValidControllerName_RejectsInvalidControllerNames(Type invalidControllerType)
{
Assert.False(DefaultHttpControllerTypeResolver.HasValidControllerName(invalidControllerType));
}
[Fact]
public void GetControllerTypes_ThrowsOnNull()
{
@@ -70,31 +108,53 @@ public void GetControllerTypes_FindsTypes()
// Arrange
DefaultHttpControllerTypeResolver resolver = new DefaultHttpControllerTypeResolver();
Mock<IAssembliesResolver> mockAssemblyResolver = new Mock<IAssembliesResolver>();
mockAssemblyResolver.Setup(a => a.GetAssemblies()).Returns(new List<Assembly>
{
null,
new MockExportedTypesAssembly(ThrowException.ReflectionTypeLoadException,
typeof(InvalidControllerStruct),
typeof(ValidController),
typeof(ControllerWrapper.InvalidNestedController),
typeof(InvalidControllerWithInconsistentName)),
typeof(ControllerWrapper.ValidNestedController),
typeof(ControllerWrapper.ValidInheritedNestedController),
typeof(ControllerWrapper.ControllerNestedWrapper.ValidNestedNestedController),
typeof(InheritedControllerHidingWrapper.ValidNestedController)),
new MockExportedTypesAssembly(ThrowException.Exception,
typeof(InvalidControllerAbstract),
typeof(InvalidControllerWithNoBaseType)),
typeof(ControllerWrapper.InvalidAbstractNestedController),
ControllerWrapper.TypeOfInvalidProtectedNestedController(), // NOTE: Unable to get through typeof(ControllerWrapper.InvalidProtectedNestedController),
ControllerWrapper.TypeOfInvalidPrivateNestedController(), // NOTE: Unable to get through typeof(ControllerWrapper.InvalidPrivateNestedController),
typeof(ControllerWrapper.InvalidInternalNestedController)),
new MockExportedTypesAssembly(ThrowException.None,
typeof(VALIDController),
typeof(VALIDCONTROLLER),
typeof(InvalidControllerStruct)),
typeof(ValidSealedController),
typeof(ValidPartialController),
typeof(ValidInheritedController),
typeof(ValidController),
typeof(VALIDController),
typeof(VALIDCONTROLLER),
typeof(InvalidAbstractController),
typeof(InvalidControllerStruct),
typeof(InvalidControllerWithInconsistentName),
typeof(InvalidControllerWithNoBaseType))
});
// Act
ICollection<Type> actualControllerTypes = resolver.GetControllerTypes(mockAssemblyResolver.Object);
// Assert
Assert.Equal(3, actualControllerTypes.Count);
Assert.Equal(10, actualControllerTypes.Count);
Assert.True(actualControllerTypes.Contains(typeof(ControllerWrapper.ValidNestedController)));
Assert.True(actualControllerTypes.Contains(typeof(ControllerWrapper.ValidInheritedNestedController)));
Assert.True(actualControllerTypes.Contains(typeof(ControllerWrapper.ControllerNestedWrapper.ValidNestedNestedController)));
Assert.True(actualControllerTypes.Contains(typeof(ValidSealedController)));
Assert.True(actualControllerTypes.Contains(typeof(ValidPartialController)));
Assert.True(actualControllerTypes.Contains(typeof(ValidInheritedController)));
Assert.True(actualControllerTypes.Contains(typeof(ValidController)));
Assert.True(actualControllerTypes.Contains(typeof(VALIDController)));
Assert.True(actualControllerTypes.Contains(typeof(VALIDCONTROLLER)));
Assert.True(actualControllerTypes.Contains(typeof(InheritedControllerHidingWrapper.ValidNestedController)));
}
private static string GetDefaultControllerRouteName(Type controllerType)
{
return controllerType.Name.Substring(0, controllerType.Name.Length - "Controller".Length);
}
}
@@ -138,27 +198,96 @@ public override Type[] GetExportedTypes()
public class ControllerWrapper
{
public class InvalidNestedController : ApiController
public static Type TypeOfInvalidPrivateNestedController()
{
return typeof(InvalidPrivateNestedController);
}
public static Type TypeOfInvalidProtectedNestedController()
{
return typeof(InvalidProtectedNestedController);
}
public class ValidNestedController : ApiController
{
}
public abstract class InvalidAbstractNestedController : ApiController
{
}
public class ValidInheritedNestedController : InvalidAbstractNestedController
{
}
protected class InvalidProtectedNestedController : ApiController
{
}
private class InvalidPrivateNestedController : ApiController
{
}
internal class InvalidInternalNestedController : ApiController
{
}
public class ControllerNestedWrapper
{
public class ValidNestedNestedController : ApiController
{
}
}
}
public abstract class InvalidAbstractController : ApiController
{
}
public struct InvalidControllerStruct
{
}
public abstract class InvalidControllerAbstract
public class ControllerPrefix
{
}
public class InvalidControllerWithInconsistentName
public class InvalidControllerWithInconsistentName : ApiController
{
}
public class InvalidControllerWithNoBaseType
{
}
public sealed class ValidSealedController : ApiController
{
}
public partial class ValidPartialController : ApiController
{
}
public partial class ValidPartialController
{
}
public class ValidInheritedController : InvalidAbstractController
{
}
public class Controller : ApiController
{
}
public class Ctrl : ApiController
{
}
public class validcontroller : ApiController
{
}
public class ValidController : ApiController
{
}
@@ -170,4 +299,11 @@ public class VALIDController : ApiController
public class VALIDCONTROLLER : ApiController
{
}
public class InheritedControllerHidingWrapper : ControllerWrapper
{
public new class ValidNestedController : ControllerWrapper.ValidNestedController
{
}
}
}

0 comments on commit c44fb64

Please sign in to comment.