Архитектура большого проекта ASP.NET MVC

Это вопрос, связанный с тем, как структурировать проект ASP.NET MVC для приложения среднего и большого размера.

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

Моя путаница возникает, когда я пытаюсь подумать о том, как структурировать приложение, следуя рекомендациям "лучших практик" (из многих и многочисленных источников, включая печатные и веб-страницы).

Попытка уважать такие вещи, как

  • Контроллеры должны быть очень простыми
  • Принципы TDD (или, по крайней мере, подход, который облегчит тестирование в будущем)
  • Разделение беспокойства
  • Услуги и репозитории
  • Включение зависимостей

Теперь при создании небольших (простых, простых) MVC-приложений все это в значительной степени выполняется в одном проекте (в данном случае речь идет о Visual Studio Project), а также разделение между MVC "Слои" это довольно просто папки в проекте VS (а также отдельные пространства имен).

В некоторых наших других проектах мы приняли стиль Service- > repository, поэтому этот не будет отличаться.

Мы используем Entity Framework как постоянство базы данных (первый подход DB).

Мы отделили наш доступ к БД (материал EF) в другой проект VS, поэтому у нас есть проект веб-проектов и моделей (или данных) в решении.

В веб-проекте есть контроллеры и представления, а в проекте данных есть службы, репозитории и элементы EF.

Моя путаница с моделями (или, возможно, с пониманием модели Domain Model vs View)

Если бы я попытался следовать методологии (я думаю), у меня была бы модель домена (модель, с которой связаны слои EF и репозитория), и тогда у меня будет модель представления? (модель, с которой Контроллер и представление будет иметь дело), ​​теперь они не будут на 90% одинаковыми? Разве это не значит, что вы отделяете проблемы, просто заставляя писать код модели дважды? Как я уверен, я где-то читал, что контроллеры и представления не должны иметь модель домена?

Один из способов, к которому мы подошли, - это EF, который делает все его классы моделей частичными. Затем мы расширяем этот класс и добавляем к нему класс MetaDataType, чтобы сделать "View Model" (добавить DataAnnotations к свойствам), а затем по существу одна и та же модель передается по всем слоям, но это "лучшая" практика (там это осколок в моем сознании, что это просто не так)

например,

[MetadataType(typeof(Product_Metadata))]
public partial class Product
{
    //Pretty much deliberately kept empty, just so
    // the EF model class can have the attribute added
    //The other side of this partial class is of course in the EF models
}

public class Product_Metadata
{
    [Required]
    [Display(Name = "Product name")]
    public string Name { get; set; }

    [Required]
    [Display(Name = "Unit Cost")]
    public decimal Cost { get; set; }

    //etc... for the rest of the properties on the product EF model
}

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

Мы создаем все службы и хранилища как интерфейсы и используем карту структуры в качестве контейнера IoC. Еще одна вещь, которую я допускаю, даже несмотря на то, что мы используем зависимость Injection, которую я все еще пытаюсь примирить с TDD, похоже, что нужно написать все дважды (целая точка DI, которую я думаю)

Я полагаю, что в конечном итоге я обращаюсь к желающим здесь, в SO, которые знают больше, чем я, о архитектуре больших приложений ASP.NET MVC для некоторой помощи и рекомендаций. Там, кажется, огромное количество информации, но все, кажется, очень концептуально. Когда я, наконец, прихожу к реализации, я теряюсь в понятиях.

ИЗМЕНИТЬ

В ответ на г-на Карла Андерсона

  • Пейджинг данных в представлении - Да, полностью согласен с тем, где это место, где имеет место viewmodel и имеет смысл, но опять же является CategoryListViewModel, который имеет свойство List, является ли он списком категории viewmodel или категории модели домена
  • Уязвимость при массовом присвоении - я бы подумал, что эта уязвимость будет существовать с моделью домена или моделью представления (ведь как вы настроите IsAdmin, если это действительно необходимо установить, наверняка это все равно будет на ViewModel). Я бы подумал, что это нужно будет решать на другом уровне, то есть авторизации, так что только использование роли занавеса может только установить IsAdmin
  • Отображение информации о просмотре в определенном формате. Конечно, это просто связано с привязкой к модели и/или просмотром html-помощников для форматирования - то есть только с привязкой к представлению и модели. В конце концов, все модели, которые визуализируются через представление, имеют свои свойства в html и все они являются строками в этой точке, поэтому возвращаемые значения все равно должны быть проанализированы, основной принцип привязки модели, поэтому, если мне нужен пользовательский, просто напишите новое связующее устройство.
  • Использование вашей модели домена больше, чем просто объекты передачи данных (DTO). На самом деле я стараюсь избегать этого как можно больше, пытаясь придерживаться того факта, что модели именно это, DTO. Но если бы этот сценарий возник, я бы, вероятно, написал метод расширения в модели домена, поскольку все методы не сериализуются в любом случае, или да, добавьте модель представления, но она, вероятно, будет содержать модель домена
  • Наличие разных абстракций одной и той же информации о модели домена - согласитесь частично. У меня был бы PagedAccountListViewModel (по-прежнему будут содержать модели домена), но я бы использовал только одну модель для новой и обновленной учетной записи (я рассматриваю новую, такую ​​же, как обновление, только что предварительно заполненную), и это будет модель домена

Ответ 1

Нет лучшей практики/архитектуры. У каждого проекта есть недостатки. Что касается вашей целевой архитектуры и 90% дублирования кода, вот мои мысли. Он разделен как Entity (DTO/model) или службы/репозиторий.

Фон

Основным понятием, которое я обычно придерживаюсь, является дизайн N-Tier archiecture. В основном это указано как "отделить слой домена/бизнеса от другого уровня (UI/Data Access). Основная цель заключается в том, что когда ваше приложение переносится в другую систему (UI/Storage), бизнес-уровень остается тем же.

Если вы поместите 95% логики домена в бизнес-уровень (другие 5, возможно, в базе данных, такие как создание транзакций/отчетов), вам почти ничего не нужно менять и по-прежнему иметь такое же правило домена. Проблема решена для доменного уровня, и вам нужно только сконфигурировать ее в пользовательском интерфейсе или хранилище (хранилище).

Обычно структура N-уровня выглядит следующим образом:

          entity

        interfaces

DataAccess  |  BusinessLogic

           UI

Каждый слой разделяется сборкой (проект/решение), поэтому никакая связь между каждым слоем не подчеркивается.

Дублирование сущностей

Теперь представьте себе общее "сообщение операции". Я представляю себе этот класс следующим образом:

public class OperationMessage{
    public bool IsError{get;set;}
    public string OperationMessage{get;set;}
}

Не стесняйтесь модифицировать класс, чтобы добавить перечисление для предупреждения и т.д. (это запах кода с использованием свойства auto, не следует, если вы отправляетесь на техническое обслуживание).

Скажите, что ваше приложение MVC имеет css с именем "message_error", в котором есть свойство color:red; и font-weight:bold;. Обычно вы хотите назначить его классу с таким свойством, как CssClassName. У вас есть 3 варианта:

  • Измените базовый OperationMessage в слое сущности

    Это самая простая вещь. Однако вы нарушаете n-уровневую архитектуру, потому что теперь ваш бизнес-уровень обладает знаниями о "css" и относится к веб-архитектуре. Он добавляет логику fo ui-specific (назначение CssClassName в бизнес-слое). Если когда-нибудь вы захотите перенести его на C# Winform или, возможно, Windows Mobile/Azure, он испортил архитектуру.

  • Добавьте новый класс под названием WebOperationMessage

    Это то, что я называю ViewModel. Теперь он стал дублироваться, потому что он похож на 90% с классом OperationMessage. Но ваша архитектура N-уровня поддерживается в порядке. После получения объекта OperationMessage с вашего бизнес-уровня вам нужно сделать некоторое преобразование. Это преобразование - это то, что я назвал Presentation Logic.

  • Наследовать класс OperationMessage

    Это, возможно, лучший подход для класса сущностного типа. Это гарантирует, что ваша архитектура N-уровня поддерживается в порядке и не дублирует 90% кода. Я еще не нашел недостатка в этом дизайне, но, возможно, в стиле defensive-code есть какой-либо объект стиля. Однако вам все равно нужно преобразовать.

Дублирование служб

Сервисы уже дублируются в интерфейсе. Однако это связано с достижением архитектуры N-уровня, создающей неизменный код. Это облегчает выполнение unit test и макет. Я надеюсь, что читатель уже понял, что касается насмешливого и модульного тестирования, поэтому этот ответ по-прежнему имеет значение.

Но скажите, если вы не проводите модульное тестирование или насмешливость, тогда сделайте это "расслоение" или "дублирование" , которое стоит усилий? Как указано в статье,

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

Короче говоря, как только вы нарушаете или разбиваете слои, вы теряете переносимость. Если вы нарушили слоирование BLL/DAL, вы потеряли гибкость при изменении repostory. Если вы нарушили BLL/PL, вы потеряли гибкость при переносе своих приложений с одного типа пользовательского интерфейса на другой.

Стоит ли это того? В некоторых случаях да. В других случаях, таких как корпоративные приложения, как правило, они более жесткие, а миграция менее вероятна. Однако в большинстве случаев предприятие может расширяться, и требуется мобильность. Итак, zookeepers должны стать рейнджерами.

Мои 2 цента

Ответ 2

Когда я, наконец, прихожу к реализации, я теряюсь в понятиях.

Концепции очень важны, но также абстрактны. Трудно представить, как лучше всего структурировать ваше решение, пока оно не завершится (то есть слишком поздно), и никто не может действительно рассказать вам, как его структурировать, потому что каждый проект настолько отличается.

У меня была бы модель домена [...], и тогда у меня была бы модель представления? [...] не будут ли они на 90% одинаковыми?

Я считаю, что все в порядке. Модель домена описывает фактический объект (обычно из базы данных). Модель просмотра описывает информацию, которая должна отображаться для правильного отображения. Оба они обычно не содержат логики и только список свойств. Я думаю, это прекрасно, если они будут почти одинаковыми. Используйте Automapper, чтобы легко сопоставить модели.

Контроллеры и представления не должны иметь модель домена?

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

EF делает все его модельные классы частичными. Затем мы расширяем этот же класс и добавляем к нему класс MetaDataType, чтобы сделать "View Model"

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

TDD, похоже, что нужно писать все дважды

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

Я призываю [...] за помощь и руководство

Ответ 3

Вы поднимаете несколько интересных вопросов и замечаний о множестве способов создания приложения ASP.NET MVC, не говоря уже о любом приложении. Я могу предоставить вам свои мысли по этой теме, и вы можете использовать ее, как хотите.

Вы сказали, что беспокоитесь, что при создании модели домена и модели просмотра вы будете делать то же самое дважды, потому что они будут выглядеть почти одинаково. Позвольте мне дать вам несколько сценариев, которые могут заставить вас передумать:

  • Пейджинг данных в представлении - вы можете запросить контроллер List<Category>, но не хотите, чтобы ваша модель домена отслеживала метаданные для подкачки, такие как общие записи, размер страницы и номер страницы, правильно? A CategoryListViewModel рассмотрит эту проблему.
  • Уязвимость при массовом назначении - это связано с тем, как ASP.NET MVC пытается помочь вашему контроллеру кодировать данные модели путем привязки модели. Скажем, например, у вас была страница учетной записи, в которой есть свойства, такие как имя, фамилия, номер телефона и т.д. В пользовательском интерфейсе. Представление вашей учетной записи в домене имеет IsAdmin Boolean value; если вы использовали модель домена для заполнения представления, тогда, если плохой пользователь понял, что свойство IsAdmin существует и передано IsAdmin=true в строке запроса, тогда связующее устройство модели подберет это и сделает учетную запись администратором даже хотя в представлении никогда не отображалось поле, позволяющее изменять это значение. Если вы привязаны к модели представления, которая не содержит этого свойства IsAdmin, а скорее только части данных, относящиеся к представлению, то эта уязвимость не будет существовать.
  • Отображение информации о просмотре в определенном формате - скажем, у вас есть представление, которое отображает номер телефона, а модель вашего домена хранит номер телефона в виде номера (т.е. int), а затем прямое привязывание модели вашего домена к вашему представлению потребует представление для разбора и форматирования значения int. Вместо этого модель представления может содержать номер телефона в виде строки, уже правильно отформатированную и просто отображающую значение.
  • Использование вашей модели домена не только для объектов передачи данных (DTO) - если вы хотите привязать поведение к вашей модели домена, а не просто хранить данные, тогда вам захочется иметь модели представлений, которые не содержат эту логику для отображение данных.
  • Наличие разных абстракций одной и той же информации о модели домена - это связано с сценарием поискового вызова, упомянутым ранее. Возможно, вы захотите иметь PagedAccountViewModel, AddNewAccountViewModel, UpdateAccountViewModel и т.д.

Ответ 4

Я столкнулся с той же дилеммой. Я не хотел создавать модели представлений, которые были бы почти идентичны моделям моего домена, я не хотел, чтобы мои просмотры и контроллеры иногда использовали модели домена и рассматривали модели еще раз, и я не всегда хотел показывать каждое свойство модели домена. Вместо этого я создал промежуточный слой между моделями моделей и моделями просмотра. Этот промежуточный уровень мог бы вернуть модель домена из любой модели представления и создать правильную модель представления из модели домена. Модели просмотра всегда имели свойство, являющееся фактической моделью домена. Например, моя AddressEditModel имела свойство Address, которое было моей моделью адресного домена. Если у модели адресной области были свойства, которые я не хотел показывать, тогда, когда я использую промежуточный уровень для возвращения модели домена адреса из модели редактирования адреса, он будет извлекать адрес из базы данных и устанавливать разрешенные свойства с помощью Свойство адреса модели просмотра; в противном случае (если бы все свойства могли быть открыты), он просто вернет объект в свойстве Address моей модели представления. Это решение позволило мне всегда использовать слой модели представления с моими представлениями и контроллерами, не имея двух наборов почти одинаковых классов, все еще способных контролировать, какие свойства могут быть выставлены.