MapMvcAttributeRoutes: This method cannot be called during the application's pre-start initialization phase

前提是你 提交于 2019-12-03 10:23:33

I recently upgraded my project to ASP.NET MVC 5 and experienced the exact same issue. When using dotPeek to investigate it, I discovered that there is an internal MapMvcAttributeRoutes extension method that has a IEnumerable<Type> as a parameter which expects a list of controller types. I created a new extension method that uses reflection and allows me to test my attribute-based routes:

public static class RouteCollectionExtensions
{
    public static void MapMvcAttributeRoutesForTesting(this RouteCollection routes)
    {
        var controllers = (from t in typeof(HomeController).Assembly.GetExportedTypes()
                            where
                                t != null &&
                                t.IsPublic &&
                                t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
                                !t.IsAbstract &&
                                typeof(IController).IsAssignableFrom(t)
                            select t).ToList();

        var mapMvcAttributeRoutesMethod = typeof(RouteCollectionAttributeRoutingExtensions)
            .GetMethod(
                "MapMvcAttributeRoutes",
                BindingFlags.NonPublic | BindingFlags.Static,
                null,
                new Type[] { typeof(RouteCollection), typeof(IEnumerable<Type>) },
                null);

        mapMvcAttributeRoutesMethod.Invoke(null, new object[] { routes, controllers });
    }
}

And here is how I use it:

public class HomeControllerRouteTests
{
    [Fact]
    public void RequestTo_Root_ShouldMapTo_HomeIndex()
    {
        // Arrange
        var routes = new RouteCollection();

        // Act - registers traditional routes and the new attribute-defined routes
        RouteConfig.RegisterRoutes(routes);
        routes.MapMvcAttributeRoutesForTesting();

        // Assert - uses MvcRouteTester to test specific routes
        routes.ShouldMap("~/").To<HomeController>(x => x.Index());
    }
}

One problem now is that inside RouteConfig.RegisterRoutes(route) I cannot call routes.MapMvcAttributeRoutes() so I moved that call to my Global.asax file instead.

Another concern is that this solution is potentially fragile since the above method in RouteCollectionAttributeRoutingExtensions is internal and could be removed at any time. A proactive approach would be to check to see if the mapMvcAttributeRoutesMethod variable is null and provide an appropriate error/exceptionmessage if it is.

NOTE: This only works with ASP.NET MVC 5.0. There were significant changes to attribute routing in ASP.NET MVC 5.1 and the mapMvcAttributeRoutesMethod method was moved to an internal class.

In ASP.NET MVC 5.1 this functionality was moved into its own class called AttributeRoutingMapper.

(This is why one shouldn't rely on code hacking around in internal classes)

But this is the workaround for 5.1 (and up?):

public static void MapMvcAttributeRoutes(this RouteCollection routeCollection, Assembly controllerAssembly)
{
    var controllerTypes = (from type in controllerAssembly.GetExportedTypes()
                            where
                                type != null && type.IsPublic
                                && type.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)
                                && !type.IsAbstract && typeof(IController).IsAssignableFrom(type)
                            select type).ToList();

    var attributeRoutingAssembly = typeof(RouteCollectionAttributeRoutingExtensions).Assembly;
    var attributeRoutingMapperType =
        attributeRoutingAssembly.GetType("System.Web.Mvc.Routing.AttributeRoutingMapper");

    var mapAttributeRoutesMethod = attributeRoutingMapperType.GetMethod(
        "MapAttributeRoutes",
        BindingFlags.Public | BindingFlags.Static,
        null,
        new[] { typeof(RouteCollection), typeof(IEnumerable<Type>) },
        null);

    mapAttributeRoutesMethod.Invoke(null, new object[] { routeCollection, controllerTypes });
}

Well, it's really ugly and I'm not sure if it'll be worth the test complexity, but here's how you can do it without modifying your RouteConfig.Register code:

[TestClass]
public class MyTestClass
{
    [TestMethod]
    public void MyTestMethod()
    {
        // Move all files needed for this test into a subdirectory named bin.
        Directory.CreateDirectory("bin");

        foreach (var file in Directory.EnumerateFiles("."))
        {
            File.Copy(file, "bin\\" + file, overwrite: true);
        }

        // Create a new ASP.NET host for this directory (with all the binaries under the bin subdirectory); get a Remoting proxy to that app domain.
        RouteProxy proxy = (RouteProxy)ApplicationHost.CreateApplicationHost(typeof(RouteProxy), "/", Environment.CurrentDirectory);

        // Call into the other app domain to run route registration and get back the route count.
        int count = proxy.RegisterRoutesAndGetCount();

        Assert.IsTrue(count > 0);
    }

    private class RouteProxy : MarshalByRefObject
    {
        public int RegisterRoutesAndGetCount()
        {
            RouteCollection routes = new RouteCollection();

            RouteConfig.RegisterRoutes(routes); // or just call routes.MapMvcAttributeRoutes() if that's what you want, though I'm not sure why you'd re-test the framework code.

            return routes.Count;
        }
    }
}

Mapping attribute routes needs to find all the controllers you're using to get their attributes, which requires accessing the build manager, which only apparently works in app domains created for ASP.NET.

What are you testing here? Looks like you are testing a 3rd party extension method. You shouldn't be using your unit tests to test 3rd party code.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!