Ник Ковальский

Онбординг

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

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

Цель этой статьи — помочь избежать всех хлопот, связанных с поиском различных решений небольших проблем при реализации готовой к работе мобильной локальной базы данных и создании для нее миграций кода в первую очередь как в Windows, так и в >Mac. Я приглашаю вас просмотреть и повторно использовать исходный код примера этой статьи, вы найдете ссылку в конце. Как вы увидите, это шаблон приложения Maui с добавленной логикой базы данных EF.

Проверенным стандартом базы данных мобильного клиента является SQLite. Мы мгновенно найдем пакет nuget Microsoft.EntityFrameworkCore.Sqlite для установки вместе с SQLitePCLRaw.bundle_e_sqlite3 для собственных реализаций sqlite. Для создания миграции EF нам также потребуется установить Microsoft.EntityFrameworkCore.Tools.

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

Стартап-проект MauiEF.Client нацелен на платформу Android. Инструменты консоли Entity Framework Core Package Manager не поддерживают эту платформу. См. https://aka.ms/efcore-docs-pmc-tfms для получения дополнительной информации.

.. поэтому нам нужно найти способ обойти это.

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

Проекты установки

Теперь, когда мы знаем, что инструменты проектирования EF имеют неподдерживаемые платформы, нам нужно будет создать специальный проект «Мигратор», который будет напрямую запускаться EF и нацелен на чистую net-7.0, и мы сможем создавать миграции на виндовс или яблочная машина.

И Мигратору, и нашему Клиенту потребуется доступ к базе данных, поэтому мы переместим весь наш код, связанный с контекстом, в отдельный проект Общий. Тогда наша структура решения будет выглядеть так:

Проект Shared будет ссылаться

<!--Local database-->
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.1" ></PackageReference>
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.3" ></PackageReference>

в то время как Migrator потребуются дополнительные инструменты для создания миграции:

<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.1">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

Создать базу данных

Мы определим контекст нашего примера внутри проекта Shared следующим образом:

LocalDatabase.cs

    /// <summary>
    /// Constructor for creating migrations
    /// </summary>
    public LocalDatabase()
    {
        File = Path.Combine("../", "Data1.db3");
        Initialize();
    }
    /// <summary>
    /// Constructor for mobile app
    /// </summary>
    /// <param name="filenameWithPath"></param>
    public LocalDatabase(string filenameWithPath)
    {
        File = filenameWithPath;
        Initialize();
    }
    void Initialize()
    {
        if (!Initialized)
        {
            Initialized = true;
            SQLitePCL.Batteries_V2.Init();
            Database.Migrate();
        }
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlite($"Filename={File}");
    }

Обратите внимание на два конструктора: один для средства миграции EF, а другой — для нашего приложения. Database.Migrate(); создает файлы базы данных, если она не существует, и применяет предоставленные миграции.

Возможно, вы захотите реализовать метод Database.EnsureDeleted(); в целях отладки, чтобы стереть данные при запуске.

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

Хорошо, теперь мы можем внедрить контекст в наш MauiProgram.cs:

builder.Services.AddTransient<LocalDatabase>((services) =>
        {
            return new LocalDatabase(Path.Combine(FileSystem.AppDataDirectory, "SQLite001.db3"));
        });

Создание миграций

Предоставленный исходный код не включает миграции. Если вы просто скомпилируете и запустите решение, оно вызовет исключение, так как EF не будет знать, как вам его выполнить Migrate();.

Но, пожалуйста, не волнуйтесь, миграцию очень легко создать.

Если вы используете Visual Studio для Windows:

  1. Измените стартовый проект с Client на Migrator.
  2. Откройте Консоль диспетчера пакетов: перейдите в Вид->Другие Windows->Консоль диспетчера пакетов.
  3. Установите для проекта по умолчанию значение Shared, EF будет искать контекстную модель внутри и добавлять туда миграции.
  4. Введите следующую команду, чтобы создать начальную миграцию:
add-migration Initial -Context MauiEF.Shared.Services.LocalDatabase -Verbose

Следующий метод с помощью команды like будет проиллюстрирован с использованием Visual Studio для Mac.

1 Откройте консоль: щелкните правой кнопкой мыши имя своего решения и выберите «Открыть в терминале». Вы должны попасть в папку решения.

2 Введите следующую команду, чтобы создать начальную миграцию через командную строку:

dotnet ef migrations add Initial -s Migrator -p Shared  -c MauiEF.Shared.Services.LocalDatabase

Вы заметите, что мы указали вложенную папку стартового проекта -s Migrator и вложенную папку проекта по умолчанию -p Shared.

Если вы столкнулись с ошибкой из-за отсутствия команды ef, установите ее и добавьте в глобальный путь:

dotnet tool install --global dotnet-ef
export PATH="$PATH:/Users/YOUR_USERNAME/.dotnet/tools"

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

Чтобы создать новую миграцию, просто создайте для нее уникальное имя (пример Visual Studio для Windows):

add-migration Change1 -Context MauiEF.Shared.Services.LocalDatabase -Verbose

Пример приложения

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

MainPage.cs

public MainPage()
    {
        _context = App.Services.GetService<LocalDatabase>();
        InitializeComponent();
        var mainAuthor = _context.Authors
            .Include(i => i.Books)
            .FirstOrDefault(x => x.FirstName == "John" && x.LastName == "Doe");
        if (mainAuthor == null)
        {
            Task.Run(async () =>
            {
                mainAuthor = new Author()
                {
                    FirstName = "John",
                    LastName = "Doe"
                };
                _context.Authors.Add(mainAuthor);
                await _context.SaveChangesAsync();
                _author = mainAuthor;
                Update();
            }).ConfigureAwait(false);
        }
        else
        {
            _author = mainAuthor;
            Update();
        }
    }
    private void OnCounterClicked(object sender, EventArgs e)
    {
        count++;
        var title = $"My Story Part {count}";
        var book = _author.Books.FirstOrDefault(x => x.Title == title);
        if (book == null)
        {
            CounterBtn.Text = $"Wrote \"{title}\"";
            Task.Run(async () =>
            {
                _author.Books.Add(new Book
                {
                    Title = title
                });
                _context.Authors.Update(_author);
                await _context.SaveChangesAsync();
                Update();
            }).ConfigureAwait(false);
        }
        else
        {
            CounterBtn.Text = $"Reading \"{title}\"";
        }
        SemanticScreenReader.Announce(CounterBtn.Text);
    }

Заключительные слова

Когда вы компилируете свое первое приложение EF Maui для выпуска iOS, оно может аварийно завершать работу во время выполнения на реальном устройстве из-за того, что компиляция iOS AOT не поддерживает некоторые методы EF. Я бы не стал говорить здесь точнее, вы можете узнать об этом подробнее, но выход из ситуации заключается в том, чтобы добавить в ваш файл .csprj некую изюминку для этого конкретного случая:

<!--IOS RELEASE-->
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0-ios|AnyCPU'">
    <!--to be able to use some EF core methods-->
    <MtouchExtraArgs>--interpreter</MtouchExtraArgs>
    <UseInterpreter>True</UseInterpreter>
    <!--your codesign parameters will go below-->
</PropertyGroup>

Приложения, скомпилированные с такими настройками, уже были одобрены для AppStore, и о влиянии на производительность не сообщалось.

Я надеюсь, что вы найдете это полезным, пожалуйста, не стесняйтесь задавать вопросы, если они есть: @nickkovalsky

Исходный код: https://github.com/taublast/MauiEF