Октябрь 2016

Том 31, номер 10

Доступ к данным - Работа EF Core как в .NET Framework, так и в .NET Core

Джули Лерман

Джули Лерман В этой статье используются предварительные версии DotNet CLI и инструментария EF. Любая информация, изложенная в этой статье, может быть изменена.

Технология, ранее известная как Entity Framework 7 (EF7), в начале 2016 года была переименована в Entity Framework Core (EF Core). В EF Core 1.0.0 вводятся некоторые грандиозные новые возможности, хотя в целом она имеет меньший набор функциональности, чем EF6. Но это не означает, что EF работает только в .NET Core. Вы можете использовать EF Core в API и приложениях, требующих полной .NET Framework, а также в программном обеспечении, ориентированном только на кросс-платформенную .NET Core. В этой статье я проведу вас по двум проектам, в которых исследуются эти варианты. Моя цель — снять все озабоченности, ассоциируемые с приставкой «Core»: дескать, EF Core работает только в .NET Core. Попутно я объясню этапы создания каждого решения.

EF Core в проектах для полной .NET

Я начну с проекта, ориентированного на полную .NET Framework. Учтите, что в Visual Studio 2015 нужный инструментарий требует наличия Visual Studio 2015 Update 3, а также новейшей версии расширения Microsoft ASP.NET and Web Tools. На момент написания этой статьи (август 2016 года) лучшим руководством по их установке была документация (bit.ly/2bte6Gu).

Чтобы отделить доступ к данным от операций над ними, которые будет выполнять приложение, я создам свою библиотеку классов. На рис. 1 видно, что можно выбрать шаблон, специально ориентированный на библиотеку классов для .NET Core, но я выбираю Class Library — стандартный вариант, который всегда был доступен для .NET. Полученный проект (рис. 2) тоже является «обычным»: в нем нет никаких файлов project.json или любых ресурсов проекта для .NET Core. Все выглядит так же, как и всегда.

Создание библиотеки классов для полной .NET
Рис. 1. Создание библиотеки классов для полной .NET

Обычная (и привычная) библиотека классов для .NET
Рис. 2. Обычная (и привычная) библиотека классов для .NET

Пока ничто никак не связано с EF. Я могла бы выбрать EF6 в этот момент, но добавлю EF Core в проект. Как всегда, для поиска и выбора EF Core можно использовать либо NuGet Package Manager, либо окно Package Manager Console. Я буду использовать консоль. Помните, что пакет entityframework предназначен для EF6. Чтобы получить EF Core, вы должны установить один из пакетов Microsoft.EntityFrameworkCore. Я выберу пакет SqlServer, в котором есть все необходимое для взаимодействия EF с SqlServer:

install-package Microsoft.EntityFrameworkCore.SqlServer

Так как этот пакет зависит от основного пакета Microsoft.EntityFrameworkCore, а также от пакета Microsoft.EntityFrameworkCore.Relational, NuGet автоматически установит их за меня. А поскольку пакет EF Core зависит от других пакетов, они тоже будут установлены. Всего этот процесс добавляет три пакета EF Core и 23 других из более новой модульной .NET, на которую полагается EF Core. Вместо меньшего количества крупных пакетов я получаю больше малых пакетов, но только тех, которые реально нужны моему приложению. Все они нормально работают со стандартными .NET-библиотеками, уже находящимися в проекте.

Затем я добавлю простой класс предметной области (Samurai.cs) и DbContext (SamuraiContext.cs), чтобы EF Core могла сохранять мои данные в базе данных, как показано на рис. 3. В EF Core нет магической строки подключения, как в EF6, поэтому я должна дать знать, какой провайдер и какую строку подключения я использую. Для простоты я включу это прямо в новый виртуальный метод DbContext: OnConfiguring. Я также создала перегруженную версию конструктора, позволяющую передавать провайдер и другие необходимые детали. Вскоре я воспользуюсь этой версией.

Рис. 3. Класс Samurai и DbContext-класс SamuraiContext

public class Samurai
  {
    public int Id { get; set; }
    public string Name { get; set;}
  }
public class SamuraiContext : DbContext
  {
    public DbSet<Samurai> Samurais { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
      {
        if (optionsBuilder.IsConfigured == false) {
          optionsBuilder.UseSqlServer(
         @"Data Source=(localdb)\\mssqllocaldb;Initial Catalog=EFCoreFullNet;
                       Integrated Security=True;");
        }
        base.OnConfiguring(optionsBuilder);
      }
    }
    public SamuraiContext(DbContextOptions<SamuraiContext> options)
      : base(options) { }
  }

Поскольку я использую полную .NET, а это также означает, что я ориентируюсь на полнофункциональную версию Windows, мне доступна Windows PowerShell. То есть в моем распоряжении есть те же команды миграции, которые я всегда использовала: add-migration, update-database и т. д. Также появились некоторые новые команды, и вы можете узнать о них в моей статье за январь 2016 года (msdn.com/magazine/mt614250). Кроме того, помните мое упоминание о том, что пакеты имеют меньший размер и их можно составлять как модули? Что ж, если я хочу использовать миграции, мне нужно добавить пакет, содержащий эти команды. На момент написания этой статьи весь инструментарий все еще находился на стадии предварительных версий, так что придется указать параметр –pre. Добавив этот пакет, я смогу добавить новую миграцию:

install-package Microsoft.EntityFrameworkCore.Tools –pre
add-migration init

Это работает, как обычно: создаются новая папка Migrations и файл миграции (рис. 4). В EF Core изменен способ сохранения снимков модели (model snapshots), о чем можно прочитать в ранее упомянутой статье за январь 2016 года.

Миграции EF Core в моей библиотеке классов для полной .NET
Рис. 4. Миграции EF Core в моей библиотеке классов для полной .NET

После подготовки миграций команда update-database успешно создает за меня новую базу данных EFCoreFullNet в SQL Server localdb.

Наконец, я добавлю в решение тестовый проект из того же шаблона проекта Unit Test, которым я всегда пользовалась в Visual Studio. Затем добавлю ссылку на свою библиотеку классов EFCoreFullNet. Мне не нужно, чтобы мой тестовый проект использовал базу данных для проверки работы EF Core, поэтому вместо установки пакета SqlServer я выполню следующую NuGet-команду применительно к новому тестовому проекту:

install-package microsoft.EntityFrameworkCore.InMemory

Провайдер InMemory — настоящая благодать для тестирования при применении EF Core. Он использует данные в памяти для представления базы данных и кеша EF, а EF Core будет взаимодействовать с этим кешем во многом так же, как с базой данных: добавлять, удалять и обновлять данные.

Помните тот дополнительный конструктор, созданный мной в SamuraiContext? Тесты TestEFCoreFullNet, приведенные на рис. 5, используют его преимущества. Заметьте, что в конструкторе класса теста я создала средство формирования (builder) DbContextOptions для SamuraiContext, а затем указала, что он должен использовать провайдер InMemory. Далее при создании экземпляра SamuraiContext я передаю в метод эти параметры (options). Метод OnConfiguring в SamuraiContext предназначен для проверки того, сконфигурированы ли уже параметры. Если да, он будет использовать их (в данном случае провайдер InMemory), нет — он перейдет к настройке на работу с SqlServer и строкой подключения, «зашитой» мной в код метода.

Рис. 5. Тестирование с помощью EFCore

using EFCoreFullNet;
using Microsoft.EntityFrameworkCore;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
namespace Tests
{
  [TestClass]
  public class TestEFCoreFullNet
  {
    private DbContextOptions<SamuraiContext> _options;
    public TestEFCoreFullNet() {
      var optionsBuilder = new DbContextOptionsBuilder<SamuraiContext>();
      optionsBuilder.UseInMemoryDatabase();
      _options = optionsBuilder.Options;
    }
    [TestMethod]
    public void CanAddAndUpdateSomeData() {
      var samurai = new Samurai { Name = "Julie" };
      using (var context = new SamuraiContext(_options)) {
        context.Add(samurai);
        context.SaveChanges();
      }
      samurai.Name += "San";
      using (var context = new SamuraiContext(_options)) {
        context.Samurais.Update(samurai);
        context.SaveChanges();
      }
      using (var context = new SamuraiContext(_options)) {
        Assert.AreEqual("JulieSan", context.Samurais.FirstOrDefault().Name);
      }
    }
  }
}

Этот метод теста используется преимущества некоторых специфических средств EF Core, которых нет в EF6. Я писала об этих и других средствах отслеживания изменений в EF Core в своей статье за август 2016 года (msdn.com/magazine/mt767693). Например, после создания нового объекта самурая я добавляю его в контекст методом DbContext.Add, позволяя EF определить, с каким DbSet его нужно связать. Затем я сохраняю это в хранилище данных — в моем случае список какого-либо типа в памяти, управляемый провайдером InMemory. Далее я модифицирую объект самурая, создаю новый экземпляр DbContext и использую новую команду Update из EF Core, чтобы SaveChanges гарантированно обновил хранящийся объект самурая, а не создал новый такой объект. Наконец, я запрашиваю контекст для этого самурая и применяю Assert, чтобы убедиться, что контекст действительно возвращает обновленное имя.

Однако описание используемых мной конкретных средств было бы отклонением от темы статьи. Суть в том, что я проделываю всю эту работу с помощью EF Core в проекте для обычной старой .NET в Windows.

EF Core для CoreCLR: тот же код, разные зависимости

Я могла бы остаться в Windows и Visual Studio 2015 Update 3 для следующей демонстрации, где показала бы, как использовать те же EF Core API, тот же код и те же тесты с ориентацией на исполняющую среду CoreCLR, но это выглядело бы слишком похоже на первый вариант — для Windows. Поэтому я собираюсь впасть в другую крайность и создать CoreCLR-вариацию на своем MacBook, поясняя все этапы по мере их прохождения.

.NET Core не полагается на Windows или ее инструментарий. Помимо Visual Studio 2015, я могла бы использовать… ну, я полагаю, Emacs в прошлом был популярным редактором, отличным от Visual Studio. Однако существуют некоторые кросс-платформенные IDE, из которых есть, что выбрать, — не только для написания кода, но и для отладки и поддержки Git. Например, в номере «MSDN Magazine» за август 2016 года Алессандро Дель Соле (Alessandro Del Sole) продемонстрировал создание веб-сайта ASP.NET Core с использованием Visual Studio Code (msdn.com/magazine/mt767698). По его экранным снимкам я видела, что он работал в Windows, но в остальном среда для Mac в основном та же.

Другой кросс-платформенный вариант — Rider от JetBrains. Rider предназначен специально для C#, и лучший способ описать его: «ReSharper в собственной IDE».

Я уже использовала Visual Studio Code в Windows и OS X (не только для C#, но и для Node.js) и именно ее я задействую, чтобы показать вам EF Core в приложении, создаваемом под CoreCLR. По сути, поскольку я создаю решение в OS X, ориентация на CoreCLR — единственный мой вариант. Массив доступных для моей библиотеки API более ограничен. Однако EF Core — это тот же набор API, использовавшийся мной в библиотеке для полной .NET в первом проекте.

Как вы увидите, большая часть усилий будет связана с конфигурированием проектов и зависимостей, специфичных для CoreCLR. Но я могу использовать тот же класс SamuraiContext, чтобы определить свою модель данных EF Core, и тот же тестовый метод CanAddAndUpdateSomeData из предыдущего проекта для выполнения той же работы. Код идентичен, хотя я теперь ориентируюсь на более ограниченную исполняющую среду и работаю в среде, которая не способна использовать ничего, кроме .NET Core.

Создание библиотеки, похожей на .NET-библиотеку классов

Я создала папку для хранения проектов Library и Test с подпапками для каждого проекта. В подпапке Library я могу вызвать dotnet new для создания проекта Library. На рис. 6 показана эта команда наряду с подтверждением о создании проекта. Перечисление содержимого папки показывает, что создано только два файла, самый важный из которых — project.json — содержит список необходимых NuGet-пакетов и прочие детали проекта. Library.cs — это просто пустой файл класса, который я удалю.

Создание новой CoreCLR Library командой dotnet
Рис. 6. Создание новой CoreCLR Library командой dotnet

Затем я открою проект этой новой библиотеки в Visual Studio Code. Я могу просто ввести code в строке приглашения. Visual Studio Code открывает проект как целевую папку, автоматически распознает пакеты, перечисленные в json-файле и предлагает выполнить dotnet restore для исправления неразрешенных зависимостей. Я с радостью соглашаюсь на это предложение.

Файл project.json выглядит, как на рис. 7.

Рис. 7. Файл Project.json

{
  "version": "1.0.0-*",
  "buildOptions": {
    "debugType": "portable"
  },
  "dependencies": {},
  "frameworks": {
    "netstandard1.6": {
      "dependencies": {
        "NETStandard.Library": "1.6.0"
      }
    }
  }
}

Довольно просто. Библиотекам не нужна вся начинка ASP.NET Core, которой я привыкла пользоваться, — только .NET Standard Library. Она инкапсулирует общий функционал, который .NET может теперь выполнять на разных платформах. Цитата из документации на .NET Standard Library (bit.ly/2b1JoHJ): «.NET Standard Library является формальной спецификацией .NET API, которые должны быть доступны во всех исполняющих средах .NET». Так что создаваемую мной библиотеку можно использовать с .NET Core, ASP.NET Core и даже с .NET-приложениями, начиная с .NET 4.5. Таблицу совместимости см. на странице документации.

Мой следующий шаг — добавление EF Core в проект. Учтите: поскольку я работаю сейчас на Mac, SqlServer недоступен. Я задействую провайдер PostgreSQL для EF Core, который записывается в пустой на данный момент раздел project.json:

"dependencies": {
    "Npgsql.EntityFrameworkCore.PostgreSQL": "1.0.0-*"   
  },
  "tools": {
    "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
  },

Как и раньше, я планирую выполнять миграции. В обычной ситуации я добавила бы и зависимость от пакета Microsoft.EntityFrameworkCore.Tools, где содержатся команды, — как делала это в версии библиотеки для полной .NET. Но из-за текущих ограничений инструментария версии Preview 2 я отложу это до более позднего этапа в процессе. Тем не менее, мне действительно нужен доступ к командам из этой папки библиотеки, поэтому я добавляю данный пакет в особый раздел tools файла project.json, как показано в предыдущем фрагменте кода.

Восстановление пакетов приводит не только к извлечению этих двух пакетов, но и их зависимостей. Если вы просмотрите создаваемый при этом файл project.lock.json, вы увидите все эти пакеты, включая Microsoft.EntityFrameworkCore и Microsoft.EntityFrameworkCore.Relational, — те же, что добавлялись в предыдущее .NET-решение.

Теперь я просто копирую свои файлы Samurai.cs и SamuraiContext.cs. Я должна изменить класс OnConfiguring, чтобы задействовать PostgreSQL и его строку подключения вместо SQL Server. Та часть кода теперь выглядит так:

optionsBuilder.UseNpgsql(
  "123ser ID=julie;123assword=12345;Host=localhost;Port=5432;Database=EFCoreCoreCLR;
    Pooling=true;");

Теперь следовало бы выполнить миграции, но здесь вы наталкиваетесь на известное ограничение текущей версии Preview2 инструментария EF Core вне Visual Studio, а именно: для нахождения критически важных ресурсов нужен исполняемый проект. Поэтому поначалу это добавляет головной боли, но особых дополнительных усилий не требуется. Подробнее см. по ссылке bit.ly/2btm4OW.

Создание проекта Test

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

dotnet new -t xunittest

В Visual Studio Code вы увидите новый project.json в папке Test. Поскольку этот проект будет отвечать за проверку возможности выполнения командных строк EF, вы должны добавить в зависимости ссылку на пакеты EF Core Tools. Кроме того, тестовому проекту нужна ссылка на Library, поэтому ее тоже следует добавить в зависимости в файле project.json. Вот раздел dependencies после этих добавлений:

"dependencies": {
    "System.Runtime.Serialization.Primitives": "4.1.1",
    "xunit": "2.1.0",
    "dotnet-test-xunit": "1.0.0-rc2-192208-24",
    "Library": "1.0.0",
    "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
  },

Теперь я могу обращаться к командам EF Core из папки Library. Заметьте, что на рис. 8 команда указывает на проект в папке Test через параметр --startup-project. Я буду использовать его в каждой из команд миграций.

Исполняемый проект позволяет библиотеке использовать EF-команды миграцииMigrations Commands
Рис. 8. Исполняемый проект позволяет библиотеке использовать EF-команды миграции

Выполнение миграций применительно к модели EF из .NET-библиотеки

Когда я перечисляла в своей рубрике миграции EF Core, команды миграций dotnet ef выглядели иначе, чем команды PowerShell, но они ведут к одинаковой логике в API миграций.

Сначала я создам миграцию с помощью:

dotnet ef --startup-project ../Test migrations add init

Это дает тот же результат, что и в .NET-решении: добавляются новая папка Migrations с миграцией и снимок миграции.

Теперь можно создать базу данных командой:

dotnet ef --startup-project ../Test database update

Затем я проверила, что база данных PostgreSQL, таблицы и отношения действительно созданы. С этой целью в OS X можно использовать целый ряд утилит. На своем Mac я предпочла кросс-платформенный JetBrains DataGrip в качестве IDE базы данных.

Выполнение тестов в CoreCLR

Наконец, я копирую класс TestEFCoreFullNet из предыдущего решения в папку Test. И вновь мне придется внести инфраструктурные изменения для использования xUnit вместо MS Test: это потребует изменений нескольких пространств имен, удаления атрибута TestClass, замены атрибутов TestMethod на Fact и замены Assert.AreEqual на Assert.Equal. О, и конечно, я переименую класс в TestEFCoreCoreClr.

В project.json тоже должно быть известно о провайдере InMemory, поэтому я добавляю:

"Microsoft.EntityFrameworkCore.InMemory": "1.0.0"

в раздел dependencies, а затем снова выполняю команду dotnet restore.

Мой тестовый проект под xUnit использует средство запуска тестов xUnit из командной строки. Поэтому я возвращаюсь к окну терминала для запуска тестов командой dotnet test. На рис. 9 показан вывод теста, прошедшего с большим успехом с тем исключением, что средство запуска тестов из командной строки не обеспечивает удовлетворительного вывода зеленым цветом для успешно прошедших тестов.

Вывод успешно выполненного теста xUnit
Рис. 9. Вывод успешно выполненного теста xUnit

.NET или CoreCLR: те же API, тот же код

Итак, теперь вы понимаете, что код и сборки, относящиеся к EF Core, одинаковы независимо от того, ориентировано ваше программное обеспечение исключительно на Windows с полнофункциональной .NET Framework или на CoreCLR в любой поддерживаемой среде (Linux, OS X, Windows). Обе демонстрации я могла бы реализовать в Visual Studio 2015 на компьютере с Windows. Но сочла, что работа с CoreCLR в среде, где полнофункциональная .NET Framework недоступна, гораздо нагляднее продемонстрирует, что EF API и мой код, относящийся к EF, совершенно одинаковы в обоих случаях. Крупные различия и вся дополнительная работа относятся только к целевым платформам (.NET в сравнении с CoreCLR). Вы можете посмотреть, как я создаю полнофункциональный ASP.NET Core Web API, используя EF Core 1.0.0 на своем MacBook в видеоролике «First Look at EF Core 1.0» (bit.ly/2cmblqE). Сокращенный и более интересный вариант этого видеоролика с моим выступлением на DotNetFringe см. по ссылке bit.ly/2ci7q0T.


Джули Лерман (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 Джеффу Фрицу (Jeff Fritz).