Архитектура подключаемого модуля для ASP.NET MVC

Я проводил некоторое время, глядя на статью Фила Хаака на Группировочные контроллеры, очень интересные вещи.

В настоящий момент я пытаюсь выяснить, можно ли использовать те же идеи для создания плагинов/модульной архитектуры для проекта, над которым я работаю.

Итак, мой вопрос: возможно ли, чтобы разделы Areas в Phil разделились на несколько проектов?

Я вижу, что пространства имен будут работать сами, но я обеспокоен тем, что взгляды заканчиваются в нужном месте. Это что-то, что можно разобраться с правилами сборки?

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

У меня есть опыт работы с архитектурой плагинов, но не с массами, поэтому любое руководство в этой области было бы полезно.

Ответ 1

Я сделал доказательство концепции несколько недель назад, когда я поместил полный стек компонентов: класс модели, класс контроллера и связанные с ними представления в DLL, добавленный/измененный один из примеров классов VirtualPathProvider, которые извлекают представления, чтобы они соответствовали соответствующим образом в DLL.

В конце концов, я просто уронил DLL в соответствующее настроенное приложение MVC, и он работал так же, как если бы он был частью приложения MVC с самого начала. Я подтолкнул его немного дальше, и он работал с 5 из этих маленьких мини-MVC-плагинов. Очевидно, что вы должны следить за своими ссылками и конфигурационными зависимостями, перетасовывая их все вокруг, но это сработало.

Упражнение было нацелено на функциональность плагина для платформы MVC, которую я создаю для клиента. Есть основной набор контроллеров и представлений, которые дополняются дополнительными в каждом экземпляре сайта. Мы собираемся сделать эти дополнительные бит в этих модульных DLL-плагинах. Пока все хорошо.

Я написал обзор моего прототипа и образцовое решение для плагинов ASP.NET MVC на моем сайте.

EDIT: 4 года спустя, я делал довольно много приложений ASP.NET MVC с плагинами и больше не использую описанный выше метод. На этом этапе я запускаю все свои плагины через MEF и вообще не устанавливаю контроллеры в плагины. Скорее, я создаю универсальные контроллеры, которые используют информацию о маршрутизации для выбора плагинов MEF и передают работу плагину и т.д. Просто подумал, что добавлю, так как этот ответ попадает в честный бит.

Ответ 2

Im фактически работает над расширением, чтобы использовать поверх ASP.NET MVC. Моя основа расширяемости основана на известном контейнере Ioc: Structuremap.

Практический пример, который я пытаюсь выполнить, прост: создайте приложение, которое должно иметь некоторые базовые функции, которые могут быть расширены для каждого клиента (= multi-tenancy). Там должен быть только один экземпляр приложения, но этот экземпляр может быть адаптирован для каждого клиента без внесения каких-либо изменений на основной сайт.

Я был вдохновлен статьей о многогранности, написанной Айенде Рахиеном: http://ayende.com/Blog/archive/2008/08/16/Multi-Tenancy--Approaches-and-Applicability.aspx Другим источником вдохновения была книга Эрика Эванса о разработке под управлением домена. Рамка Extensibility основана на шаблоне репозитория и концепции корневых агрегатов. Чтобы иметь возможность использовать фреймворк, хостинг-приложение должно строиться вокруг репозиториев и объектов домена. Контроллеры, репозитории или объекты домена связываются во время выполнения с помощью ExtensionFactory.

Плагин - это просто asselmbly, который содержит контроллеры или репозитории или объекты домена, которые относятся к определенному соглашению об именах. Соглашение об именах прост, каждый класс должен иметь префикс с идентификатором клиента, например: AdventureworksHomeController.

Чтобы расширить приложение, вы копируете сборку подключаемого модуля в папку расширения приложения. Когда пользователь запрашивает страницу под корневой папкой клиента, например:  http://multitenant-site.com/[customerID]/[controller]/[action] фреймворк проверяет наличие плагина для этого конкретного клиента и создает экземпляр настраиваемых классов подключаемого модуля, иначе он загружает значение по умолчанию один раз. Пользовательскими классами могут быть контроллеры - репозитории или объекты домена. Этот подход позволяет распространять приложение на всех уровнях, от базы данных до пользовательского интерфейса, через модель домена, репозитории.

Если вы хотите расширить некоторые существующие функции, вы создаете подключаемый модуль, содержащий подклассы основного приложения. Когда вы создаете совершенно новые функции, вы добавляете новые контроллеры внутри подключаемого модуля. Эти контроллеры будут загружены инфраструктурой MVC при запросе соответствующего URL-адреса. Если вы хотите расширить пользовательский интерфейс, вы можете создать новый вид внутри папки расширения и ссылаться на представление с помощью нового или подкласса контроллера. Чтобы изменить существующее поведение, вы можете создавать новые репозитории или объекты домена или подкласс, выходящий из них. Обязанностью структуры является определение того, какой контроллер/репозиторий/объект домена должен быть загружен для конкретного клиента.
Я советую посмотреть структуру структуры (http://structuremap.sourceforge.net/Default.htm) и особенно на функции реестра DSL http://structuremap.sourceforge.net/RegistryDSL.htm.

Это код, который я использую при запуске приложения для регистрации всех подключаемых контроллеров/репозиториев или объектов домена:

protected void ScanControllersAndRepositoriesFromPath(string path)
        {
            this.Scan(o =>
            {
                o.AssembliesFromPath(path);
                o.AddAllTypesOf<SaasController>().NameBy(type => type.Name.Replace("Controller", ""));
                o.AddAllTypesOf<IRepository>().NameBy(type => type.Name.Replace("Repository", ""));
                o.AddAllTypesOf<IDomainFactory>().NameBy(type => type.Name.Replace("DomainFactory", ""));
            });
        }

Я также использую ExtensionFactory, наследующий от System.Web.MVC. DefaultControllerFactory. Этот factory отвечает за загрузку объектов расширения (контроллеров/реестров или объектов домена). Вы можете подключить свои собственные фабрики, зарегистрировав их при запуске в файле Global.asax:

protected void Application_Start()
        {
            ControllerBuilder.Current.SetControllerFactory(
                new ExtensionControllerFactory()
                );
        }

Эта структура как полностью работоспособный образец сайта может быть найдена на: http://code.google.com/p/multimvc/

Ответ 3

Итак, я немного поиграл с примером из J Wynia выше. Большое спасибо за это.

Я изменил так, что расширение VirtualPathProvider использовало статический конструктор для создания списка всех доступных ресурсов, заканчивающихся на .aspx в различных dll в системе. Это трудоемко, но только мы делаем это только один раз.

Это, вероятно, полное злоупотребление тем, что VirtualFiles также предполагается использовать; -)

в итоге получится:

закрытый статический ресурс IDictionaryVirtualFile;

когда строка является виртуальным путем.

приведенный ниже код делает некоторые предположения о пространстве имен файлов .aspx, но он будет работать в простых случаях. Это приятно, что вам не нужно создавать сложные пути просмотра, созданные ими из имени ресурса.

class ResourceVirtualFile : VirtualFile
{
    string path;
    string assemblyName;
    string resourceName;

    public ResourceVirtualFile(
        string virtualPath,
        string AssemblyName,
        string ResourceName)
        : base(virtualPath)
    {
        path = VirtualPathUtility.ToAppRelative(virtualPath);
        assemblyName = AssemblyName;
        resourceName = ResourceName;
    }

    public override Stream Open()
    {
        assemblyName = Path.Combine(HttpRuntime.BinDirectory, assemblyName + ".dll");

        Assembly assembly = Assembly.ReflectionOnlyLoadFrom(assemblyName);
        if (assembly != null)
        {
            Stream resourceStream = assembly.GetManifestResourceStream(resourceName);
            if (resourceStream == null)
                throw new ArgumentException("Cannot find resource: " + resourceName);
            return resourceStream;
        }
        throw new ArgumentException("Cannot find assembly: " + assemblyName);
    }

    //todo: Neaten this up
    private static string CreateVirtualPath(string AssemblyName, string ResourceName)
    {
        string path = ResourceName.Substring(AssemblyName.Length);
        path = path.Replace(".aspx", "").Replace(".", "/");
        return string.Format("~{0}.aspx", path);
    }

    public static IDictionary<string, VirtualFile> FindAllResources()
    {
        Dictionary<string, VirtualFile> files = new Dictionary<string, VirtualFile>();

        //list all of the bin files
        string[] assemblyFilePaths = Directory.GetFiles(HttpRuntime.BinDirectory, "*.dll");
        foreach (string assemblyFilePath in assemblyFilePaths)
        {
            string assemblyName = Path.GetFileNameWithoutExtension(assemblyFilePath);
            Assembly assembly = Assembly.ReflectionOnlyLoadFrom(assemblyFilePath);  

            //go through each one and get all of the resources that end in aspx
            string[] resourceNames = assembly.GetManifestResourceNames();

            foreach (string resourceName in resourceNames)
            {
                if (resourceName.EndsWith(".aspx"))
                {
                    string virtualPath = CreateVirtualPath(assemblyName, resourceName);
                    files.Add(virtualPath, new ResourceVirtualFile(virtualPath, assemblyName, resourceName));
                }
            }
        }

        return files;
    }
}

Затем вы можете сделать что-то подобное в расширенном VirtualPathProvider:

    private bool IsExtended(string virtualPath)
    {
        String checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
        return resourceVirtualFile.ContainsKey(checkPath);
    }

    public override bool FileExists(string virtualPath)
    {
        return (IsExtended(virtualPath) || base.FileExists(virtualPath));
    }

    public override VirtualFile GetFile(string virtualPath)
    {
        string withTilda = string.Format("~{0}", virtualPath);

        if (resourceVirtualFile.ContainsKey(withTilda))
            return resourceVirtualFile[withTilda];

        return base.GetFile(virtualPath);
    }

Ответ 4

Я думаю, что можно оставить свои взгляды в проектах подключаемого модуля.

Это моя идея: вам нужен ViewEngine, который будет вызывать плагин (возможно, через интерфейс) и запрашивать представление (IView). Плагин затем создавал бы представление не через его url (как обычный ViewEngine делает -/Views/Shared/View.asp), а через его название), например, через рефлексию или контейнер DI/IoC).

Возвращение представления в плагине может быть даже жестко запрограммировано (следующий простой пример):

public IView GetView(string viewName)
{
    switch (viewName)
    {
        case "Namespace.View1":
            return new View1();
        case "Namespace.View2":
            return new View2();
        ...
    }
}

... это была просто идея, но я надеюсь, что она может работать или просто быть хорошим вдохновением.

Ответ 5

Это сообщение может быть немного запоздалым, но я играл с ASP.NET MVC2 и придумал прототип, используя функцию "Области".

Здесь ссылка для всех, кто интересуется: http://www.veebsbraindump.com/2010/06/asp-net-mvc2-plugins-using-areas/

Ответ 6

[сообщение в качестве ответа, потому что я не могу комментировать]

Отличное решение. Я использовал подход J Wynia и получил его для просмотра с отдельной сборки. Однако этот подход, по-видимому, только дает представление. Контроллеры внутри плагина, похоже, не поддерживаются, правильно? Например, если вид из плагина сделал сообщение назад, контроллер представлений в плагине будет вызываться не. Вместо этого он будет перенаправлен контроллеру в корневом приложении MVC. Я правильно понимаю это или есть обходной путь для этой проблемы?