Использование Entity Framework Core code-first с СУБД SQLite при разработке WinForms-приложений в VisualStudio 2015

в 14:09, , рубрики: .net, .net frameowrk, entity framework core, sqlite, Visual Studio, visual studio 2015, WinForms, десктоп, метки:

На первый взгляд нижеприведённый материал может показаться ещё одним банальным «хэллоууорлдом», «туториалом от Капитана Очевидность», коих уже предостаточно на просторах Сети, но это впечатление обманчиво. На деле чтобы добиться того же результата с нуля у WinForms-программиста, не работавшего ранее с Entity Framework Core (не путать с классической Entity Framework) и вооружённого только уже валяющимися в изобилии на просторах Сети туториалами по EF Core и документацией может уйти куда больше времени, чем он наивно ожидал до того, как взялся за дело. Так случилось и со мной. Посему хочу поделиться опытом.

Основная проблема заключается в том, что имеющиеся материалы по EF Core в подавляющем большинстве не подразумевают использование Visual Studio (вместо этого подразумевается использование легковесного кроссплатформенного тулинга .NET Core) и уж точно не берут в расчёт, что кому-то захочется использовать EF Core в дэсктопном приложении на основе фрэймворка Windows Forms (вместо этого как правило подразумевается использование ASP.NET core). Но задачи и ситуации, в которых предпочтительно (или и вовсе необходимо) решение в виде дэсктопного приложения всё-ещё встречаются, и свои преимущества (как, разумеется, и недостатки) у Entity Framework Core по сравнению с Entity Framework 6 есть. Кроме того в доступных в Сети примерах как правило рассматриваются только наиболее элементарные действия типа добавления записи в БД, при этом даже вопрос последующего извлечения данных зачастую не рассматривается, а в нём уже есть не очевидные моменты. Также можно заметить, что EF Core достаточно активно развивается и многие инструкции теряют актуальность в т.ч. по этой причине.

В своих попытках решить такую элементарную на первый взгляд задачу, как разработка минимального демонстрационного приложения WinForms с использованием EF Core в VisualStudo 2015 я натолкнулся на целый проблем типа исключений, неожиданного поведения, непонимания как сделать нечто, что в туториалах подразумевается как самоочевидное и т.п. В результате некоторые шаги из тех, что я приведу ниже пришлось нащупывать практически в слепую, гугля, задавая вопросы и экспериментируя.

В качестве платформы я выбрал .NET Framework 4.6.1 (версия 4.6.2 почему-то не появляется в списке доступных у меня в Visual Studio, но и при выборе 4.6.1 всё работает, возможно будет работать и с более ранними версиями, но я не проверял), в качестве среды разработки — Visual Studio Community Edition 2015 Update 3, в качестве СУБД — SQLite, в качестве ОС — Windows 7 64-bit.

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

0. Проверяем, что установлены последние версии .NET Framework 4.6.2 (web installer, offline installer) и Visual Studio 2015 Update 3 (update web installer, update offline installer ISO, web installer полной версии Visual Studio 2015 Update Community Edition 3, offline installer ISO полной версии Visual Studio 2015 Update Community Edition 3 с интергрированным обновлением)

1. Устанавливаем Windows Management Framework 5.0 чтобы обновить PowerShell до версии 5.0. У меня (после установки Windows 7 и Visual Studio 2015 и всех обновлений к ним) в системе была версия 2.0 и далее в процессе я получил сообщение об ошибке с требованием более новой версии

2. Устанавливаем .NET Core 1.0.1 tools Preview 2 (web installer, может использоваться для создания offline-дистрибутива при помощи ключа /layout).

3. Обновляем расширение Visual Studio для работы с репозиториями NuGet. Для этого либо скачиваем актуальную на данный момент версию 3.5.0.1996 по прямой ссылке либо добавляем в настройки Visual Studio соответствующий репозиторий расширений для автоматического обновления.

Visual Studio 2015 'Tools' - 'Options' - 'Extensions and Updates' dialogue

4. Создаём новый проект типа Windows Forms Application. Я назвал его «Vs2015WinFormsEfcSqliteCodeFirst20170304Example» (актуальный на 04.03.2017 пример WinForms-приложения с использованием Entity Framework Core, подхода «code-first» и СУБД SQLite в Visual Studio 2015). В качестве target framework выбираем .NET Framework 4.6.1.

5. Нажимаем правой кнопкой мыши по нашему проекту в панели Solution Explorer, выбираем Manage NuGet Packages..., переходим на вкладку Browse, устанавливаем Microsoft.EntityFrameworkCore.Sqlite.Design и Microsoft.EntityFrameworkCore.Tools. Для установки актуальных версий может потребоваться установить галочку «Include prerelease», в стабильных версиях может чего-то не хватать или наличествовать неисправленные баги. Я установил последние на данный момент Microsoft.EntityFrameworkCore.Sqlite.Design 1.1.0 и Microsoft.EntityFrameworkCore.Tools 1.1.0-preview4-final. Если возникнут проблемы с установкой Microsoft.EntityFrameworkCore.Tools можно попробовать сделать это через командную строку NuGet: в меню выбрать Tools — NuGet Package Manager — Package Manager Console, в появившейся консоли (которая, кстати, ещё понадобится нам далее) после приглашения «PM>» ввести «Install-Package Microsoft.EntityFrameworkCore.Tools -Pre»

PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Pre

6. Создаём файлы исходного кода классов модели данных. Для порядка я поместил их в подпапку «Model» (некоторые называют её «Entities», некоторые кидают все классы в корень проекта, а некоторые и вовсе в один файл). Модель описывает учебный пример базы данных, хранящей список городов и людей в/из них. Каждый человек может быть связан только с одним городом, может быть неизвестно из какого он города вообще.

Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleContext.cs

using Microsoft.EntityFrameworkCore;

namespace Vs2015WinFormsEfcSqliteCodeFirst20170304Example.Model
{
    public class Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleContext : DbContext
    {
        public DbSet<City> Cities { get; set; }

        public DbSet<Person> People { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite("Filename=Vs2015WinFormsEfcSqliteCodeFirst20170304Example.sqlite");
        }
    }
}

City.cs

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace Vs2015WinFormsEfcSqliteCodeFirst20170304Example.Model
{
    public class City
    {
        public int Id { get; set; }

        public string Name { get; set; }

        [InverseProperty("City")]
        public virtual ICollection<Person> People { get; set; }
    }
}

Person.cs

using System.ComponentModel.DataAnnotations.Schema;

namespace Vs2015WinFormsEfcSqliteCodeFirst20170304Example.Model
{
    public class Person
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public string Surname { get; set; }

        public int? CityId { get; set; }

        [InverseProperty("People")]
        public virtual City City { get; set; }
    }
}

7. Разрешаем скрипты PowerShell, если этого не сделать может возникнуть ошибка "...packagesMicrosoft.EntityFrameworkCore.Tools.1.1.0-preview4-finaltoolsinit.ps1 cannot be loaded because running scripts is disabled on this system." Для этого переходим в командную строку NuGet (выбрать Tools — NuGet Package Manager — Package Manager Console в меню) и выполняем следующую команду

PM> Set-ExecutionPolicy RemoteSigned

8. Создаём «миграции». Для этого, сохранив и откомпилировав наш код (просто чтобы удостовериться в отсутствии явных опечаток) переходим в командную строку NuGet и выполняем следующую команду.

PM> Add-Migration -Name "Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleMigration" -Context "Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleContext"

в результате у нас в проекте должна появиться папка «Migrations» и два файла в ней: «Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleContextModelSnapshot.cs» и «20170304204355_Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleMigration.cs» (разумеется число в начале последнего у вас будет другое — это ни что иное, как дата и время в момент генерации в очевидном формате, я её потом вообще удалил оставив только «Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleMigration.cs»).

9. Редактируем сгенерированные файлы чтобы добавить условие уникальности (unique constraint, также известное как вторичный ключ) на имя города (в реальности, конечно, бывают города с одинаковыми именами, но для примера будет не лишним)

Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleMigration.cs

using Microsoft.EntityFrameworkCore.Migrations;

namespace Vs2015WinFormsEfcSqliteCodeFirst20170304Example.Migrations
{
    public partial class Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleMigration : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Cities",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    Name = table.Column<string>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Cities", x => x.Id);

                    // Эта строка добавлена вручную
                    table.UniqueConstraint("UQ_Cities_Name", x => x.Name);
                });

            migrationBuilder.CreateTable(
                name: "People",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    CityId = table.Column<int>(nullable: true),
                    Name = table.Column<string>(nullable: true),
                    Surname = table.Column<string>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_People", x => x.Id);
                    table.ForeignKey(
                        name: "FK_People_Cities_CityId",
                        column: x => x.CityId,
                        principalTable: "Cities",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateIndex(
                name: "IX_People_CityId",
                table: "People",
                column: "CityId");
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "People");

            migrationBuilder.DropTable(
                name: "Cities");
        }
    }
}

Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleContextModelSnapshot.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Vs2015WinFormsEfcSqliteCodeFirst20170304Example.Model;

namespace Vs2015WinFormsEfcSqliteCodeFirst20170304Example.Migrations
{
    [DbContext(typeof(Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleContext))]
    partial class Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleContextModelSnapshot : ModelSnapshot
    {
        protected override void BuildModel(ModelBuilder modelBuilder)
        {
            modelBuilder
                .HasAnnotation("ProductVersion", "1.1.0-rtm-22752");

            modelBuilder.Entity("Vs2015WinFormsEfcSqliteCodeFirst20170304Example.Model.City", b =>
                {
                    b.Property<int>("Id")
                        .ValueGeneratedOnAdd();

                    b.Property<string>("Name");

                    b.HasKey("Id");

                    // Эта строка добавлена вручную
                    b.HasIndex("Name").IsUnique();

                    b.ToTable("Cities");
                });

            modelBuilder.Entity("Vs2015WinFormsEfcSqliteCodeFirst20170304Example.Model.Person", b =>
                {
                    b.Property<int>("Id")
                        .ValueGeneratedOnAdd();

                    b.Property<int?>("CityId");

                    b.Property<string>("Name");

                    b.Property<string>("Surname");

                    b.HasKey("Id");

                    b.HasIndex("CityId");

                    b.ToTable("People");
                });

            modelBuilder.Entity("Vs2015WinFormsEfcSqliteCodeFirst20170304Example.Model.Person", b =>
                {
                    b.HasOne("Vs2015WinFormsEfcSqliteCodeFirst20170304Example.Model.City", "City")
                        .WithMany("People")
                        .HasForeignKey("CityId");
                });
        }
    }
}

10. Генерируем файл БД. Для этого в командной строке NuGet выполняем следующую команду

PM> Update-Database -Context "Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleContext"

Файл БД создастся в той же директории, где находится скомпилированный exe-шник нашего приложения, у меня это "...Vs2015WinFormsEfcSqliteCodeFirst20170304ExamplebinDebugVs2015WinFormsEfcSqliteCodeFirst20170304Example.sqlite".

На данном этапе мы уже можем заглянуть внутрь созданного файла, например с помощью официального консольного клиента sqlite3 или бесплатной кроссплатформенной GUI оболочки DB Browser for SQLite, и удостовериться, что таблицы создались корректно. Помимо наших таблиц «Cities» и «People» мы также найдём там таблицу «__EFMigrationsHistory» со служебной информацией EF Core и «sqlite_sequence» со служебной информацией SQLite.

11. Теперь перейдём к дизайнеру нашей формы, разместим на ней кнопочку, щёлкнем и на ней дважды чтобы создать обработчик события нажатия на кнопку и перейти к редактированию его кода. Ниже привожу свой код, демонстрирующий основные действия с записями БД. Я ещё имею привычку всегда переименовывать Form1 в MainForm и контролы аналогично по смыслу (в данном случае единственный контрол button1 в mainButton), но это дело вкуса и принятых в вашей команде стандартов именования.

MainForm.cs

private void mainButton_Click(object sender, EventArgs e)
{
    // Удаляем все записи из обеих таблиц
    using (var context = new Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleContext())
    {
        foreach (var person in context.People)
            context.Remove(person);

        foreach (var city in context.Cities)
            context.Remove(city);

        context.SaveChanges();
    }

    // Добавляем новые записи в таблицу городв и затем
    // новые записи в таблицу людей, ссылаясь на добавленные города
    using (var context = new Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleContext())
    {
        var praha = new City { Name = "Praha" };

        var london = new City { Name = "London" };

        var madrid = new City { Name = "Madrid" };

        var jan = new Person { Name = "Jan", City = praha };

        var john = new Person { Name = "John", City = london };

        var juan = new Person { Name = "Juan", City = madrid };

        context.Cities.AddRange(praha, london, madrid);

        context.People.AddRange(jan, john, juan);
        
        context.SaveChanges();
    }

    // Загружаем запись о добавленном ранее городе из БД,
    // добавляем нового человека в этот город,
    // изменяем добавленную ранее запись о человеке (добавляем фамилию)
    using (var context = new Vs2015WinFormsEfcSqliteCodeFirst20170304ExampleContext())
    {
        // Обратите внимание на конструкцию Include(city => city.People)
        // если написать просто context.Cities.Single(city => city.Name == "London");
        // то город найдётся, но его список .People будет равен null.
        // В production коде при использовании .Single также необходимо добавить обработку случаев,
        // когда удовлетворяющих запросу записей нет или когда их болше одной
        var london = context.Cities.Include(city => city.People)(city => city.Name == "London");

        var peter = new Person { Name = "Peter", City = london };

        var john = london.People.Single(person => person.Name == "John");

        john.Surname = "Smith";

        context.Add(peter);

        context.Update(john);

        context.SaveChanges();
    }
}

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

Скачать весь приведённый выше и сопутствующий код можно тут.

Автор: StrangeAttractor

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js