web-dev-qa-db-fra.com

MapMvcAttributeRoutes: cette méthode ne peut pas être appelée pendant la phase d'initialisation de pré-démarrage de l'application

J'ai un test très simple dans un projet de test dans une solution utilisant ASP MVC V5 et le routage d'attributs. Le routage d'attributs et la méthode MapMvcAttributeRoutes font partie de ASP MVC 5.

[Test]
public void HasRoutesInTable()
{
    var routes = new RouteCollection();
    routes.MapMvcAttributeRoutes();
    Assert.That(routes.Count, Is.GreaterThan(0));
}

Il en résulte:

System.InvalidOperationException : 
This method cannot be called during the applications pre-start initialization phase.

La plupart des réponses à ce message d'erreur impliquent la configuration des fournisseurs d'appartenance dans le fichier web.config. Ce projet n'a ni fournisseur d'appartenance ni fichier web.config, Donc l'erreur semble se produire pour une autre raison. Comment puis-je déplacer le code hors de cet état de "pré-démarrage" pour que les tests puissent s'exécuter?

Le code équivalent pour les attributs sur ApiController fonctionne très bien après l'appel de HttpConfiguration.EnsureInitialized().

35
Anthony

J'ai récemment mis à niveau mon projet vers ASP.NET MVC 5 et j'ai rencontré exactement le même problème. Lors de l'utilisation de dotPeek pour l'explorer, j'ai découvert qu'il existe une méthode d'extension interne MapMvcAttributeRoutes qui a un paramètre IEnumerable<Type> Qui attend une liste de types de contrôleurs. J'ai créé une nouvelle méthode d'extension qui utilise la réflexion et me permet de tester mes routes basées sur les attributs:

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 });
    }
}

Et voici comment je l'utilise:

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());
    }
}

Un problème est maintenant qu'à l'intérieur de RouteConfig.RegisterRoutes(route) je ne peux pas appeler routes.MapMvcAttributeRoutes() j'ai donc déplacé cet appel vers mon fichier Global.asax à la place.

Une autre préoccupation est que cette solution est potentiellement fragile car la méthode ci-dessus dans RouteCollectionAttributeRoutingExtensions est interne et peut être supprimée à tout moment. Une approche proactive serait de vérifier si la variable mapMvcAttributeRoutesMethod est nulle et de fournir un message d'erreur/d'exception approprié si tel est le cas.

REMARQUE: Cela ne fonctionne qu'avec ASP.NET MVC 5.0. Des modifications importantes ont été apportées au routage des attributs dans ASP.NET MVC 5.1 et la méthode mapMvcAttributeRoutesMethod a été déplacée vers une classe interne.

19
Steven

Dans ASP.NET MVC 5.1 cette fonctionnalité était déplacée dans sa propre classe appelée AttributeRoutingMapper.

(C'est pourquoi il ne faut pas compter sur le piratage de code dans les classes internes)

Mais c'est la solution de contournement pour 5.1 (et plus?):

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 });
}
9
Seb Nilsson

Eh bien, c'est vraiment moche et je ne sais pas si cela vaudra la complexité du test, mais voici comment vous pouvez le faire sans modifier votre code RouteConfig.Register:

[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;
        }
    }
}

Le mappage des routes d'attributs doit trouver tous les contrôleurs que vous utilisez pour obtenir leurs attributs, ce qui nécessite d'accéder au gestionnaire de build, qui ne fonctionne apparemment que dans les domaines d'application créés pour ASP.NET.

5
dmatson