MapMvcAttributeRoutes: этот метод не может быть вызван во время фазы инициализации перед запуском приложения

У меня очень простой тест в тестовом проекте в решении с использованием ASP MVC V5 и маршрутизации атрибутов. Маршрутизация атрибутов и метод MapMvcAttributeRoutes являются частью ASP MVC 5.

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

Это приводит к:

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

Большинство ответов на это сообщение об ошибке включают настройку поставщиков членства в файле web.config. Этот проект не имеет ни поставщиков членства, ни файла web.config, поэтому ошибка, по-видимому, происходит по какой-то другой причине. Как переместить код из этого состояния "перед запуском", чтобы тесты могли выполняться?

Эквивалентный код атрибутов на ApiController отлично работает после вызова HttpConfiguration.EnsureInitialized().

Ответ 1

Недавно я обновил свой проект до ASP.NET MVC 5 и испытал ту же самую проблему. При использовании dotPeek для его изучения я обнаружил, что существует внутренний метод расширения MapMvcAttributeRoutes, который имеет IEnumerable<Type> как параметр, который ожидает список типов контроллеров. Я создал новый метод расширения, который использует отражение и позволяет мне тестировать маршруты, основанные на атрибутах:

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

И вот как я его использую:

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

Одна проблема теперь в том, что внутри RouteConfig.RegisterRoutes(route) я не могу вызвать routes.MapMvcAttributeRoutes(), поэтому вместо этого я перебрал этот вызов в мой файл Global.asax.

Другая проблема заключается в том, что это решение потенциально является хрупким, поскольку вышеупомянутый метод в RouteCollectionAttributeRoutingExtensions является внутренним и может быть удален в любое время. Проактивным подходом было бы проверить, является ли переменная mapMvcAttributeRoutesMethod пустой и предоставляет соответствующую ошибку/исключение, если она есть.

ПРИМЕЧАНИЕ.. Это работает только с ASP.NET MVC 5.0. Были существенные изменения в маршрутизации атрибутов в ASP.NET MVC 5.1, а метод mapMvcAttributeRoutesMethod был перемещен во внутренний класс.

Ответ 2

В ASP.NET MVC 5.1 эта функциональность была перемещена в собственный класс под названием AttributeRoutingMapper.

(Вот почему не следует полагаться на взлом кода во внутренних классах)

Но это обходной путь для 5.1 (и вверх?):

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

Ответ 3

Ну, это действительно уродливо, и я не уверен, что это будет стоить сложности теста, но вот как вы можете это сделать, не изменяя свой код 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 what you want, though I'm not sure why you'd re-test the framework code.

            return routes.Count;
        }
    }
}

Для сопоставления маршрутов атрибутов необходимо найти все контроллеры, которые вы используете, чтобы получить их атрибуты, что требует доступа к диспетчеру сборки, который, по-видимому, работает только в доменах приложений, созданных для ASP.NET.

Ответ 4

Что вы здесь тестируете? Похоже, вы тестируете сторонний метод расширения. Вы не должны использовать свои модульные тесты для тестирования стороннего кода.