четверг, 16 июня 2011 г.

Model scaffolding для MVC 3 – идеи и принципы

Эта статья является логическим продолжением статей ASP.NET MVC 3 + Entity Framework 4.1 Code First, MVC 3 + scaffolding и моей презентации на TechDays “Пример разработки сайта на MVC 3”. В ней я расскажу про свой пакет для scaffolding’а Model Scaffolding for ASP.NET MVC.

Как я уже говорил ранее, идея написать этот пакет появилась после знакомства с проектом MvcScaffolding. Если вкратце, это пакет NuGet (минимальную необходимую информацию по NuGet и MvcScaffolding я привел в предыдущей статье), позволяющий быстро генерировать контроллеры и представления. Для первого знакомства с MvcScaffolding достаточно прочитать статьи автора “Scaffold your ASP.NET MVC 3 project with the MvcScaffolding package” и “MvcScaffolding: Standard Usage”, а заинтересовавшимся после первого знакомства, рекомендую читать эту серию статей дальше.

Для понимания дальнейшего изложения знакомство с MvcScaffolding не требуется, однако для более глубоко понимания потенциальных возможностей как MvcScaffolding так и ModelScaffolding оно будет весьма полезно. Далее я расскажу про ModelScaffolding по порядку, не забыв объяснить идеи и цели, которые лежат в его основе.

Disclaimer

Я не претендую на особую оригинальность и/или полезность своего подхода к scaffolding’у моделей. Я просто знаю, что есть круг задач которые он решает хорошо, а, на момент создания проекта на CodePlex, сходу не нашел аналога в рамках используемых технологий.

К слову о “круге задач” – на мой взгляд, это прототипирование, причем необязательно “чистое прототипирование” (когда прототип гарантированно не будет использован в промышленной разработке). А самая идеальная задача – набор сущностей со стандартными названиями свойств и большим количеством простых справочников. К концу этой статьи вы поймете почему, а пока запаситесь терпением ;)

Еще один важный момент – для использования scaffolding’а вообще нужен определенный склад ума (даже там, где от него можно получить явный выигрыш). Причем я бы не сказал что те, кто не использует scaffolding чем-то лучше или хуже. Хотя я не очень понимаю их позицию – для меня это сродни тому, что кто-то отказывается от предварительной обработки деталей напильником только из-за того, что надфиль более точен и им, в принципе, можно сделать то же самое.

Зачем ускорять создание модели?

Вопрос резонный. По большому счету, модель пишется достаточно быстро. К тому же, если вспомнить, что есть сниппеты, макросы и прочая нечисть, то еще быстрее. Просто мне всегда нравилось сделать что-то еще быстрее – видимо это привычка, оставшаяся от нескольких лет плотной оптимизации SQL-запросов :)

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

Итак, с мотивацией разобрались, пора переходить к деталям. На всякий случай напомню (более подробно описано в предыдущей статье), что установить ModelScaffolding можно из Package Manager Console, выполнив:

PM> Install-Package ModelScaffolding

Отправная точка

С самого начала я решил, по возможности, оставаться в рамках PowerShell. Причина в том, что этот вариант проще отлаживать (не надо компилировать командлеты а можно просто скопировать файлы в соответствующую папку тестового solution’а). Также я подумал, что это поможет тем, кто скачает мой исходный код, проще разобраться с его логикой.

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

Список свойств с типами

Во-первых, очевидно, должна быть возможность указать названия свойств вместе с их типами. Этот вариант я реализовал максимально просто и гибко – можно указать любое название типа (нет никакого контроля).

У этого решения есть только один недостаток – ошибившись, можно получить не компилирующийся код. С другой стороны, по мне лучше так, чем бить кого-то по рукам (может он предпочитает потом пройтись ReSharper’ом и создать все что нужно). Например, выполнив (при условии отсутствия в пределах видимости класса “SuperTest”) в консоли команду:

PM> Scaffold Model Test Parent:SuperTest

Мы затем можем использовать один из вариантов, предложенных ReSharper'ом:

ResharPerCoontext

Помимо названия типа можно указать в квадратных скобках длину для строк. Таким образом, пример списка свойств с типами, на данный момент, выглядит так (параметр -Force позволяет перезаписать существующий файл):

PM> Scaffold Model Test Id:int,Name:string[50],BirthDate:System.DateTime -Force

В результате получим такой класс:

public class Test
{
public int Id {get; set;}
[StringLength(50)]
[Required]
public string Name {get; set;}
public System.DateTime BirthDate {get; set;}
}

Как видите, у нас уже есть одна полезная фишка – быстрое задание атрибута [StringLength], который, помимо валидации на странице, сможет использоваться при генерации базы данных с помощью Entity Framework 4.1 Code First (вместо NVARCHAR(MAX), предлагаемого по умолчанию).

Сначала, кстати сказать, я использовал “MaxLength” вместо “StringLength” (из-за большей наглядности первого). Однако последний вариант показался мне более правильным, потому что, во-первых, он поддерживается механизмом валидации в MVC, а, во-вторых, его при желании можно расширить, задав дополнительные параметры. Генерация же столбцов в базе данных по обоим атрибутам происходит идентично.

И все вроде бы неплохо, только вам не кажется, что как-то жестко было бы делать все свойства всегда обязательными, не так ли?

Необязательные свойства

Для того, чтобы свойство стало необязательным, достаточно написать знак вопроса после названия типа. Немного забегая вперед скажу, что, когда названия типа и длины строки нет, знак вопроса ставится сразу после названия свойства. Давайте сделаем все свойства кроме идентификатора необязательными:

PM> Scaffold Model Test Id:int,Name:string[50]?,BirthDate:System.DateTime? -Force

При этом получим такой класс:

public class Test
{
public int Id {get; set;}
[StringLength(50)]
public string Name {get; set;}
public System.DateTime? BirthDate {get; set;}
}

Как видите, у строкового свойства уже нет атрибута [Required], а у даты рождения nullable-тип. Если мы добавим этот класс в DbContext и сгенерируем базу данных, то столбцы в соответствующей таблице смогут принимать значения “null”.

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

Автоматическое определение типов

А как же convention over configuration? И действительно – ведь многие свойства однотипны – чаще всего, все идентификаторы либо int, либо Guid и т.п. Поэтому я решил сделать настройку для соответствия названий и типов.

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

Осталось придумать, где хранить эти настройки. Естественно, мне хотелось сделать их максимально настраиваемыми, поэтому идеальным вариантом был бы файл в проекте Visual Studio. К счастью, T4Scaffolding поддерживает команду “Scaffold CustomTemplate”, создающую в проекте папку “CodeTemplates”, в которую (точнее говоря, в соответствующие подпапки) складываются шаблоны. Под шаблонами подразумевается T4, однако никто ведь не мешает сделать расширение “.t4”, а хранить в файле любой текст ;)

Следуя принципу KISS (Keep It Simple Stupid), я не стал использовать XML и прочие фишки, а просто разделил составляющие пробелами. В результате, первоначальный файл настроек выглядел так:

string[50] .*Name .+_name name LastName name FirstName name Surname surname
System.DateTime .*Date .+_date date
int .*Count .+_count count .*Id .*ID .+_id id DisplayOrder display_order
decimal .*Price .+_price price
bool Is.+ is_.+ Required required Optional optional

Как видите, первое “слово” до пробела – это в точности описание свойства, которое мы использовали в списке свойств в предыдущей команде. Далее, разделенные пробелами, следуют регулярные выражения, при срабатывании которых и выбирается тип, указанный первым в этой строке.

Нетрудно догадаться, что типом по умолчанию является “string”. Таким образом первая строка в примере конфигурации уточняет, что имена должны генерироваться с длиной 50. Если бы этой строки не было, Entity Framework использовал бы максимально доступную длину строки.

Файл я назвал “TypePatterns.cs.t4”. Не спрашивайте, почему именно так – сначала, на ночь глядя, показалось очень логичным, а потом привык :) Этот файл можно сохранить к себе в проект с помощью следующей команды:

Scaffold CustomTemplate ModelScaffolding.Model TypePatterns

Важное замечание: для этой команды я крайне не рекомендую использовать ключ “–Force”, чтобы ваши изменения не переписались настройками по умолчанию (за исключениям случаев, когда именно этого вы и хотите). В любом случае, вы же используете систему контроля версий, ведь правда? :)

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

PM> Scaffold Model Test Id,Name?,BirthDate? -Force

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

Простейшие справочники

Вам приходилось создавать много мелких справочников, у которых одинаковые свойства? Мне приходилось, поэтому я добавил возможность не указывать список свойств при генерации модели:

PM> Scaffold Model Test -Force

Выполнение этой команды создаст для нас следующий класс:

public class Test
{
public int Id {get; set;}
[StringLength(50)]
[Required]
public string Name {get; set;}
}

Не так давно я подумал, что неплохо бы дать возможность настраивать список свойств, который будет использоваться по умолчанию. В этом нам поможет старый добрый “TypePatterns.cs.t4”.

Вспомнился мне один проект, где для всех справочников была возможность задать порядок сортировки. Вы ведь наверняка встречались со списками стран, где Россия была на первом месте, а дальше список был отсортирован по алфавиту? Для этого проекта имело бы смысл написать такую строчку в “TypePatterns.cs.t4”:

default Id Name DisplayOrder

После сохранения файла можно заново выполнить команду для генерации и убедиться, что добавилось новое целочисленное свойство “DisplayOrder”.

Создание контроллера

Для того, чтобы создать не только модель, но и всю MVC-обвязку (DbSet в DbContext, контроллер и представления), достаточно добавить параметр “-Controller”. Он действует точно также как и в MvcScaffolding (точнее сказать, после создания модели вызывается соответствующий Scaffolder для создания контроллера).

PM> Scaffold Model Test -Controller –Force

В результате получаем контроллер и представления для CRUD-операций.

Автоматическое создание справочников

Как я уже говорил, нередки случаи, когда необходимо создать несколько однотипных справочников. Сейчас не идет речь о подходе, когда в разрабатываемом приложении используется некоторая инфраструктура для универсального управления справочниками. Давайте возьмем постановки из предыдущей статьи:

  1. Пишем прототип, внешний вид нас пока не интересует.
  2. Нам нужно поддерживать список товаров (идентификатор, название).
  3. Товары разбиты на категории (плоский список)
  4. У каждого товара есть производитель.
  5. Дополнительно у товара будет количество на складе (не заморачиваемся на складской учет).

Автоматическое создание справочников работает, когда нет класса с соответствующим идентификатору названием. А теперь давайте просто посмотрим на одну команду, которая создает не только модель, но и справочники, а также может генерировать контроллеры и представления :)

Scaffold Model Product Id,Name,StockQty,CategoryId+,ManufacturerId?+ -Force –Controller

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

Справочники создадутся автоматически как простейшие (см. чуть выше). Когда смотришь на бегущие в PM консоли строки о создании файлов, вспоминается фраза “вкалывают роботы, счастлив человек” (с) :)

Вот именно о таких задачах я и говорил в самом начале – скорость разработки по сравнению с написанием кода вручную (даже со сниппетами) увеличивается в несколько раз и угроза туннельного синдрома запястий немного отступает. Впечатление портит только не очень быстрая генерация (у меня нет SSD-накопителя), зато можно успеть сходить за чаем или кофе :)

А если серьезно, оптимизацией скорости выполнения я пока не занимался. Даже не стал измерять, на моей ли стороне основные проблемы. Однако если у кого-нибудь после просмотра исходного кода будут идеи по его ускорению – буду премного благодарен. Мой опыт в PowerShell, если честно, ограничивается этим пакетом и одним скриптом для бэкапа.

Резюме

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

Если у вас есть замечания, пожелания или новые темы – пишите в комментариях или на olegaxenow.reformal.ru. Постараюсь учесть.

2 комментария:

  1. Пожалуйста!
    Надеюсь, во второй половине июля или августа получится его немного улучшить.

    ОтветитьУдалить