Comments 30
Я бы добавил, что для действительно большого проекта может быть полезным еще большее ветвление по подпапкам:
/feature/models/
/feature/controllers/
/feature/views/

И models в mvc проекте все же нужны — отдавать клиенту dto, а не объекты бизнес логики, будет хорошим решением.

В MVC уже есть (были?) так называемые Areas, которые и служат для решения описанных в статье проблем.

Области (Areas) обладали еще и своей конфигурацией. Создавать по области на фичу — замаяться можно.


Тут же приведено решение, когда конфигурация для всего проекта общая, только файлы перегруппированы.

В areas Так же можно оставить конфигурацию по умолчанию и она будет использовать общую конфигурацию. странное это решение с features. И плюс в «увеличении связанности» не такой уж и плюс.

А что если понадобится создать новую область со своей конфигурацией, в которой будет несколько фич? :)

Фактически сделано следующее:
— Переименована папка Views в Features
— Для каждого контроллера все что с ним связано перенесено в его папку в папке Views
Поделитесь опытом о расположении в проекте объектов, связанных с DI-контейнером (например, профили Autofac), объектов, связанных с Automapper (профили), различных инфраструктурных вещей (ActionFilters, ModelBinders, Helpers, etc). Интересует структура и иерархия директорий.
Все замечательно, но в действительно больших проектах — обычно не ограничиваются одним проектом, создаются библиотеки, в которые выносятся в т.ч. и view. С удовольствием бы почитал, как поступать в таком случае.

В старом ASP.NET MVC это делалось вот так:


<Target Name="GetModuleContent" DependsOnTargets="PipelineTransformPhase" Outputs="@(_Content)">
    <PropertyGroup>
        <ModuleName Condition="'$(ModuleName)'==''">$(AssemblyName)</ModuleName>
    </PropertyGroup>
    <ItemGroup>
        <_Content Remove="@(_Content)" />
        <_Content Include="@(FilesForPackagingFromProject)" Condition="'%(FilesForPackagingFromProject.FromTarget)' == 'CollectFilesFromContent'">
            <DestinationRelativePath>Areas\$(ModuleName)\%(FilesForPackagingFromProject.DestinationRelativePath)</DestinationRelativePath>
        </_Content>
        <_Content Include="@(FilesForPackagingFromProject)" Condition="'%(FilesForPackagingFromProject.FromTarget)' != 'CollectFilesFromContent'">
            <DestinationRelativePath>%(FilesForPackagingFromProject.DestinationRelativePath)</DestinationRelativePath>
        </_Content>
    </ItemGroup>
</Target>

<Target Name="CopyChildContent" BeforeTargets="CopyAllFilesToSingleFolderForMsdeploy">
    <ItemGroup>
        <_ChildContent Remove="@(_ChildContent)" />
    </ItemGroup>

    <MSBuild Projects="@(ProjectReference)" Targets="GetModuleContent" RebaseOutputs="true" Condition="'%(ProjectReference.CopyContent)'=='true'">
        <Output TaskParameter="TargetOutputs" ItemName="_ChildContent" />
    </MSBuild>

    <ItemGroup>
        <FilesForPackagingFromProject Include="@(_ChildContent)" Exclude="@(FilesForPackagingFromProject)" />
    </ItemGroup>
</Target>

Ну и для отладки в IIS надо еще виртуальный путь настроить.

Я правильно понимаю, что описанные в статье вещи в старом MVC реализовывались именно через IControllerFactory (самостоятельный поиск класса контроллера и создание экземпляра) и собственный ResourceProvider с регистрацией в HostingEnvironment.RegisterVirtualPathProvider?
А MVC найдет шаблон Index.cshtml в папке /Modules/Help/Views/ для контроллера Help автоматом, если в соседней /Modules/Admin/Views есть свой Index.cshtml?

Нет. За это отвечает интерфейс IViewEngine. Надо унаследоваться от RazorViewEngine и перегрузить методы FindPartialView и FindView.


А замена VirtualPathProvider для этих целей — из пушки по воробьям. Костылем.

В VirtualPathProvider можно реализовать работу с кешем представления, как если, например, изменили вручную представление и надо его заново перекомпилировать. RazorViewEngine это позволит сделать?

Э… что-то я не понял как вы собрались перекомпилировать представление из VirtualPathProvider.


Во-первых, VirtualPathProvider не управляет компиляцией, он только сообщает информацию о файлах!
Во-вторых, нет никакого смысла делать то, что уже реализовано в BuildManager.

Да, согласен.
Просто есть такой класс как CacheDependency и его можно использовать, чтобы уведомить об изменении представления (например, я редактирую представления «на лету» на продакшене или во время отладки).
А еще есть сильный минус в RazorViewEngine на первый взгляд. Сейчас на скорую руку попробовал использовать и сразу натолкнулся на проблему. На этапе разработки сайт представляет собой набор библиотек плюс основное приложение. БОльшая часть представлений во время компиляции копируется в конечную папку в bin в основное приложение. Но мне-то надо редактировать исходный вариант представления в рантайме. То есть мне надо редактировать не файл Projects/SiteMain/bin/Debug/Views/login.cshtml, а файл Projects/SiteAuthLibrary/Views/login.cshtml. А при попытке выдать путь выше каталога приложения я получаю ошибку, что нельзя указать такой путь.
Конечно, может быть, есть варианты обхода такого поведения, но на данный момент это основное, что останавливает)

Я все еще не понимаю зачем вам иметь доступ к CacheDependency. ASP.NET сама создает этот класс в дефолтном провайдере — и сама же утилизирует его в билд-менеджере… Перекомпиляция измененных представлений вообще-то работает "из коробки" (пока вы не заменили VirtualPathProvider)!


Что же для путей к представлениям, хранящимся в других библиотеках — тут я согласен. VirtualPathProvider — один из возможных вариантов их подключения. Но я бы советовал вам подключить вручную виртуальный каталог в IIS — это позволит находиться в библиотеках еще и статике. Кроме того, это уберет из проекта отладочный код, который не нужен в релизе.


Ну и файлы из bin я бы посоветовал перенести в Areas :)

Для девелоперской машины все решил через автоматическое создание Junctions в папке основного приложения. Но вылезла очередная проблема — нет метода для поиска Layout, если он лежит не в папке с представлением. Вот это уже печалька.
Именно такая организация проекта когда views / models и прочее каждого модуля находится в отдельном поддереве (подкаталоге) и принята по-умолчанию в Django. Там правда это еще вытекает из самой организации модулей для Python, которые есть подкаталог с файлом __init__.py.

До этого работал с Laravel, там по-умолчанию группировалось все controller в одном подкаталоге, все model в другом — приходилось чаще скакать по папкам. Так как все-таки в пределах одного модуля модель / контроллер / представление больше взаимосвязанны чем разные модели и контроллеры между собою.
Мы пошли по иному пути. Все стандартные папки оставили как есть (Controllers, Views, etc), а все остальное (DAL, DTO, Tools, etc) вынесли в отдельные проекты.
Проблема «скачков» действительно существует. Если в Студии еще более менее переходишь между контроллером и вьюшками, то в файловой системе при деплоях надо апдейтить из трех папок в три серверные папки.
К примеру, на сайте надо поправить какой то раздел. Условно — в каталоге оборудования забыли вывести параметр «Поддержка HDMI». Надо слазить в контроллер, иногда в модель, потом во вьюшке поправить — и скачешь по папкам.
Поэтому, если в Студию внесут нейтивную поддержку такого представления — было бы здорово.

Э… вы все еще деплоите вручную? Кстати, откуда у вас на сервере отдельные файлы с контроллерами и моделями?

Когда идут небольшие правки — да, руками. Если много изменений, бывает Publish делаю, прописывать Exclude главное не забывать. Других инструментов не знаю — подскажите, пож-та, если знаете.

Команда msbuild /t:Package /p:PackageLocation=... сгенерирует вам пакет для развертывания при помощи msdeploy (службы веб-развертывания). Ну и если покопаться в файле "C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v*\Web\Microsoft.Web.Publishing.targets", то там можно найти еще настроек. В том числе можно собрать архив, удобный для велосипедного развертывания.


Дальше — настраивается CI...

Ах да, если надо исключить из пакета какие-то файлы, то заводите в проекте свой Target, вешаетесь на BeforeTarget="ExcludeFilesFromPackage" и создаете Item с именем ExcludeFromPackageFiles:


<Target Name="ExcludeTopSecretFile" BeforeTarget="ExcludeFilesFromPackage">
    <ItemGroup>
      <ExcludeFromPackageFiles Include="TopSecretFile.txt">
        <FromTarget>ExcludeTopSecretFile</FromTarget>
      </ExcludeFromPackageFiles>
    </ItemGroup>
</Target>
Only those users with full accounts are able to leave comments. Log in, please.