Июль 2015

ТОМ 30 ВЫПУСК 7

Доступ к данным - Исследуем поведение Entity Framework в командной строке Scriptcs

Джули Лерман

Исходный код можно скачать по ссылке

Julie LermanЯ провела много времени, пытаясь обучить людей тому, как ведет себя Entity Framework в отношении взаимосвязей и отсоединенных данных. EF не всегда следует правилам, которые вы могли вообразить, поэтому я всегда ставлю цель помочь вам понять, чего следует ожидать, почему некоторые вещи работают определенным образом и как работать с конкретным поведением (или избегать его).

Когда я хочу что-то продемонстрировать разработчикам, у меня есть несколько вариантов рабочих процессов. Например, я могу создать интеграционные тесты и выполнять их по одному за раз, рассматривая каждое поведение по отдельности. Что мне не нравится в этом варианте, так это то, что мне часто нужна отладка, чтобы я могла глубже исследовать объекты и показать, что происходит за кулисами. Но отладка автоматизированных тестов может оказаться медленной из-за большого объема исходного кода, который Visual Studio потребуется загрузить.

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

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

using (var context = new AddressContext()) {
  aRegionFromDatabase=context.Regions.FirstOrDefault();
  context.Address.Add(newAddress);
  newAddress.Region=aRegionObjectFromDatabase;
}

этим:

using (var context = new AddressContext()) {
  context.Address.Add(newAddress);
  newAddress.Region=aRegionObjectFromDatabase;
}

и этим:

newAddress.Region=aRegionObjectFromDatabase;
using (var context = new AddressContext()) {
  context.Address.Add(newAddress);
}

Кстати, разница в том, что в первом примере, где EF получает Region в экземпляре того же контекста, EF распознает, что этот регион уже существует и не станет пытаться заново добавить его в базу данных. Во втором и третьем случаях EF предположит, что Region, как и его «родительский» Address, является новым, и добавит его в базу данных. В долгосрочной перспективе я рекомендую использовать значение внешнего ключа, а не экземпляр. Возможность продемонстрировать причину и эффект действительно очень помогает.

Разумеется, я могла бы продемонстрировать этот эффект с помощью тестов, но это было бы не столь наглядно и более запутанно. Кроме того, тесты предназначены не для доказывания теорий, а для проверки кода. Я также не люблю показывать различия с помощью консольного приложения, потому что приходится запускать приложение после каждого изменения в коде. Альтернатива получше — применение фантастического приложения LinqPad, и я не раз пользовалась им. Но теперь я больше склоняюсь к четвертому способу — на основе Scriptcs.

Scriptcs (scriptcs.net) появилась в 2013 году и имеет полностью открытый исходный код. Она написана поверх Roslyn и создает исполняющую среду командной строки, которая позволяет выполнять C#-код вне Visual Studio. Scriptcs также предоставляет очень облегченный механизм для создания скриптов на C# в любом текстовом редакторе с последующим выполнением этих скриптов из командной строки.

Я часто слышала о Scriptcs, поскольку одним из основных разработчиков этой утилиты является Гленн Блок (Glenn Block), к которому я испытываю огромное уважение. Но я никогда не смотрела ее, пока совсем недавно не услышала, как Блок рассказывает о светлом будущем Scriptcs в звуковом ролике на DotNetRocks (bit.ly/1AA1m4z). Первая же мысль, которая промелькнула у меня при просмотре этой утилиты, заключалась в том, что она могла бы дать мне возможность выполнять интерактивные демонстрации прямо в командной строке. А в комбинации с EF мне было бы легче давать пояснения читателям моей рубрики.

Очень краткое введение в Scriptcs

По Scriptcs есть много отличных ресурсов. Я далеко не эксперт, поэтому просто подчеркну некоторые важные вещи, а за более подробной информацией отправлю вас на сайт scriptcs.net. Я также нашла короткий видеоролик от Латиша Сенгала (Latish Sengal) на bit.ly/1R6mF8s — он дает хорошее первое представление о Scriptcs.

Сначала вам понадобится установить Scriptcs на свой компьютер для разработок, используя Chocolatey, а значит, вы должны установить на этот компьютер и Chocolatey. Если у вас еще нет Chocolatey, то знайте, что это невероятно удобная утилита для установки инструментальных средств. Chocolatey использует NuGet для установки утилит (а также средств и API, от которых они зависят) точно так же, как NuGet применяет пакеты для развертывания сборок.

Scriptcs предоставляет среду Read, Evaluate, Play, Loop (REPL), которую можно считать неким подобием исполняющей среды. Вы можете использовать Scriptcs для выполнения команд на C# (одной за другой) из командной строки или запускать файлы скриптов Scriptcs, созданные в текстовом редакторе. Для некоторых редакторов есть даже расширения IntelliSense под Scriptcs. Лучший способ увидеть Scriptcs в действии — начать с построчного выполнения. Давайте этим и займемся.

Установив Scriptcs, перейдите в папку, где вы хотите хранить любой сохраняемый код, а затем выполните команду scriptcs. Это приведет к запуску REPL-среды, и вы получите уведомление о том, что Scriptcs работает. Также появится приглашение командной строки в виде >.

В командной строке можно набрать любую C#-команду, которая находится в нескольких из наиболее часто используемых пространств имен .NET. Все они доступны тут же и из тех же сборок, которые имеются по умолчанию в типичном проекте консольного .NET-приложения. На рис. 1 показаны запуск Scriptcs из командной строки и последующее выполнение строки кода на C#, а также выводимые результаты.

Выполнение кода на C# в командной строке REPL-среды Scriptcs
Рис. 1. Выполнение кода на C# в командной строке REPL-среды Scriptcs

В Scriptcs имеется набор директив, позволяющих делать такие вещи, как ссылаться на сборку (#r) или загружать существующий файл скрипта (#load).

Другая классная возможность — Scriptcs позволяет легко устанавливать NuGet-пакеты. Делается это из командной строки, используя параметр –install команды Scriptcs. Например, на рис. 2 показано, что происходит, когда я устанавливаю Entity Framework.

Установка NuGet-пакетов в Scriptcs
Рис. 2. Установка NuGet-пакетов в Scriptcs

Scriptcs создает папку scriptcs_packages, куда помещаются папки с содержимым релевантных пакетов, как видно на экранном снимке, приведенном на рис. 3.

Scriptcs NuGet Package Installer создает знакомые папки пакетов и конфигурационных файлов
Рис. 3. Scriptcs NuGet Package Installer создает знакомые папки пакетов и конфигурационных файлов

Создание модели и кода для некоторых распространенных настроек

Я буду выполнять свои эксперименты применительно к конкретной модели, которую я уже создала в Visual Studio, используя подход Code First. У меня одна сборка с классами (Monkey, Banana и Country) и другая сборка, содержащая MonkeysContext, который наследует от EF DbContext и предоставляет DbSet объектов Monkey, Banana и Country. Я убедилась в корректности модели с помощью средства View Entity Data Model из расширения Entity Framework Power Tools для Visual Studio (bit.ly/1K8qhkO). Благодаря этому я знаю, что могу полагаться на скомпилированные сборки, которые буду использовать в своих тестах командной строки.

Чтобы проводить эти эксперименты, я должна выполнять некоторые общие операции настройки. Мне нужны ссылки на мои сборки и сборку EF и требуется код, который создает экземпляры новых объектов monkey, country и MonkeysContext.

Вместо того чтобы постоянно повторять эту настройку в командной строке, я создам файл скрипта для Scriptcs. Файлы скриптов, имеющие расширение .csx, являются более распространенным способом использования преимуществ Scriptcs. Сначала я использовала Notepad++ с плагином Scriptcs, но потом мне предложили попробовать Sublime Text 3 (sublimetext.com/3). Переход на эту утилиту позволил мне задействовать не только плагин Scriptcs, но и OmniSharp — плагин C# IDE для Sublime Text 3, который создает великолепную среду для кодирования при условии применения текстового редактора, поддерживающего OSX и Linux, а также Windows.

OmniSharp дает возможность писать скрипты для Scriptcs и многих других систем. Установив Sublime, можно приступать к созданию файла .csx, который инкапсулирует общий настроечный код, необходимый мне для тестирования модели.

В текстовом редакторе я создаю новый файл, SetupForMonkeyTests.csx, добавляю некоторые команды Scriptcs для ссылки на нужные сборки, а затем пишу код на C# для создания объектов, как показано ниже:

#r "..\DataModel Assemblies\DataModel.dll"
#r "..\DataModel Assemblies\DomainClasses.dll"
#r "..\scriptcs_packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll"
using DomainClasses;
using DataModel;
using System.Data.Entity;
var country=new Country{Id=1,Name="Indonesia"};
var monkey=Monkey.Create("scripting gal", 1);
Database.SetInitializer(new NullDatabaseInitializer<MonkeysContext>());
var ctx=new MonkeysContext();

Вспомните, что команды #r добавляют ссылки на необходимые сборки. На ряд обязательных сборок System ссылки включаются по умолчанию, чтобы обеспечить готовую среду, похожую на ту, которую вы получаете при создании нового консольного проекта в Visual Studio. Поэтому мне не требуется указывать ссылки на эти сборки, но я должна ссылаться на собственные сборки, а также на сборку EntityFramework, установленную NuGet. Остальное — просто код на C#. Заметьте, что я не определяю здесь никаких классов. Это скрипт, и Scriptcs будет просто читать его и построчно выполнять. В комбинации с OmniSharp Sublime Text можно даже выполнить компиляцию, чтобы проверить правильность синтаксиса.

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

Переходим к экспериментам с EF в командной строке

В командной строке я снова запущу Scriptcs REPL командой scriptcs без параметров.

Как только REPL становится активной, я первым делом загружаю файл .csx. Затем использую команды :references и :vars, чтобы проверить корректность выполнения файла .csx в REPL. (В Scriptcs есть ряд команд REPL, начинающихся с двоеточия. Просмотреть их список можно командой :help.) На рис. 4 показан мой сеанс работы на данный момент; вы можете увидеть все API, на которые я ссылаюсь, а также созданные мной объекты.

Рис. 4. Запуск Scriptcs, загрузка файла .csx, проверка ссылок и существующих переменных

D:\ScriptCS Demo>scriptcs
scriptcs (ctrl-c to exit or :help for help)
> #load "SetupForMonkeyTests.csx"
> :references
[
  "System",
  "System.Core",
  "System.Data",
  "System.Data.DataSetExtensions",
  "System.Xml",
  "System.Xml.Linq",
  "System.Net.Http",
  "C:\\Chocolatey\\lib\\scriptcs.0.14.1\\tools\\ScriptCs.Core.dll",
  "C:\\Chocolatey\\lib\\scriptcs.0.14.1\\tools\\ScriptCs.Contracts.dll",
  "D:\\ScriptCS Demo\\scriptcs_packages\\EntityFramework.6.1.3\\lib\\   
    net45\\EntityFramework.dll",
  "D:\\ScriptCS Demo\\scriptcs_packages\\EntityFramework.6.1.3\\lib\\
    net45\\EntityFramework.SqlServer.dll",
  "D:\\ScriptCS Demo\\DataModel Assemblies\\DataModel.dll",
  "D:\\ScriptCS Demo\\DataModel Assemblies\\DomainClasses.dll"
]
> :vars
[
  "DomainClasses.Country country = DomainClasses.Country",
  "DomainClasses.Monkey monkey = DomainClasses.Monkey",
  "DataModel.MonkeysContext ctx = DataModel.MonkeysContext"
]
>

Кроме того, я могу просмотреть объекты, просто вводя имя переменной в приглашении командной строки. Вот что представляет собой, например, мой объект monkey:

> monkey
{
  "Id": 0,
  "Name": "scripting gal",
  "Bananas": [],
  "CountryOfOrigin": null,
  "CountryOfOriginId": 1,
  "State": 1
}
>

Теперь я готова к исследованию поведения EF. Возвращаясь к более ранним примерам, я проверю, как EF реагирует на присоединение объекта country к monkey, когда контекст отслеживает (или не отслеживает) monkey и когда он отслеживает (или не отслеживает) country.

В первом тесте я подключу уже существующий country к свойству CountryOfOrigin объекта monkey и добавлю monkey в контекст. Затем с помощью свойства DbContext.Entry().State я буду изучать, как EF воспринимает состояние объектов. Добавление monkey имеет смысл, но заметьте, что EF полагает, что добавлен и country. Вот так EF интерпретирует граф объектов. Поскольку я использовала метод Add применительно к корню графа (monkey), EF помечает все, что находится в графе как Added. Когда я вызову SaveChanges, country будет вставлен в таблицу базы данных и, как видно на рис. 5, у меня появятся две записи по Индонезии. Тот факт, что у country уже было значение ключа (Id), игнорируется.

Scriptcs позволяет сразу же увидеть, как EF реагирует на метод DbSet.Add применительно к графу
Рис. 5. Scriptcs позволяет сразу же увидеть, как EF реагирует на метод DbSet.Add применительно к графу

Далее я введу команду :reset для очистки журнала REPL и снова загружу файл .csx через #load. После этого я опробую новый рабочий процесс: добавление monkey в контекст и присоединение country к monkey, который уже отслеживается. Заметьте, что я не включаю все ответы в следующий листинг:

> :reset
> #load "SetupForMonkeyTests.csx"
> ctx.Monkeys.Add(monkey);
{...response...}
> monkey.CountryOfOrigin=country;
{...response...}
> ctx.Entry(monkey).State.ToString()
Added
> ctx.Entry(country).State.ToString()
Added

И вновь контекст назначает состояние Added объекту country, так как я присоединяю его к другой сущности, помеченной как Added.

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

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

Рис. 6. Назначение состояния Unchanged вручную

> :reset
> #load "SetupForMonkeyTests.csx"
> ctx.Entry(country).State=EntityState.Unchanged;
2
> ctx.Monkeys.Add(monkey);
{...response...}
> monkey.CountryOfOrigin=country;
{...response...}
> ctx.Entry(monkey).State.ToString()
Added
> ctx.Entry(country).State.ToString()
Unchanged
>

Поскольку контекст уже отслеживает country, состояние этого объекта не будет изменяться. EF изменяет состояние только связанного объекта, когда его состояние не известно.

Scriptcs станет для меня гораздо большим простой утилиты для обучения

Лично я предпочитаю полностью избегать путаницы вокруг этих правил и просто задаю значение внешнего ключа (CountryOfOriginId=country.Id) без присоединения ссылки с помощью навигационного свойства. По сути, благодаря этому шаблону я могу повнимательнее присмотреться к навигационному свойству CountryOfOrigin и подумать, а нужно ли оно мне здесь вообще. Демонстрация всех вариаций и того, как EF реагирует на каждый сценарий, для многих оказывается шокирующим уроком.

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


Джули Лерман (Julie Lerman) — Microsoft MVP, преподаватель и консультант по .NET, живет в Вермонте. Часто выступает на конференциях по всему миру и в группах пользователей по тематике, связанной с доступом к данным и другими технологиями Microsoft .NET. Ведет блог thedatafarm.com/blog и является автором серии книг «Programming Entity Framework» (O’Reilly Media, 2010), в том числе «Code First Edition» (2011) и «DbContext Edition» (2012), также выпущенных издательством O’Reilly Media. Вы можете читать ее заметки в twitter.com/julielerman и смотреть ее видеокурсы для Pluralsight на juliel.me/PS-Videos.

Выражаю благодарность за рецензирование статьи эксперту Microsoft Джастину Русбатчу (Justin Rusbatch).