Xamarin.iOS で EntityFrameworkCore のマイグレーションを使う

はじめに

先日、Xamarin.iOS で EntityFrameworkCore が使えることがわかった。

tnakamura.hatenablog.com

そうなると、データベースのマイグレーションがやりたくなるのは自然な流れ。 今度はマイグレーションを試してみた。

マイグレーションファイルを生成したいが

マイグレーションファイルを手書きするのは人がやることじゃないので、 ツールで生成したいところ。 Microsoft.EntityFrameworkCore.Tools をパッケージ参照すれば、 生成コマンド Add-Migration が使えるようになる。

www.nuget.org

ただ、Xamarion.iOS プラットフォームをサポートしていなかったので、Add-Migration を実行するとエラー発生。 Xamarin.iOS プロジェクトにパッケージを追加できたから、期待してしまったじゃないか。

仕方ないのでマイグレーションファイル生成用に .NET Core プロジェクトを作成

.NET Core コンソールアプリケーションをプロジェクトに追加し、 DbContext はそちらに用意する。

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using System;

namespace HelloXamarin.Core
{
    public class Stamp
    {
        public string Id { get; set; } = Guid.NewGuid().ToString();

        public DateTime StampedAt { get; set; } = DateTime.UtcNow;
    }

    public class ApplicationDbContext : DbContext
    {
        readonly string databasePath;

        public ApplicationDbContext(string databasePath)
            : base()
        {
            this.databasePath = databasePath;
            Stamps = Set<Stamp>();
        }

        public DbSet<Stamp> Stamps { get; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            // SQLite を使う
            optionsBuilder.UseSqlite(
                connectionString: $"Filename={databasePath}");

            base.OnConfiguring(optionsBuilder);
        }
    }

    public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
    {
        public ApplicationDbContext CreateDbContext(string[] args)
        {
            //var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
            //optionsBuilder.UseSqlite("Data Source=test.db");

            return new ApplicationDbContext("test.db");
        }
    }
}

マイグレーションファイルを作成

.NET Core プロジェクトをスタートアッププロジェクトにして

Add-Migration CreateInitialSchema

を実行。

すると、下記のような内容の 20180406052413_CreateInitialSchema.cs が出力された。

using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;

namespace HelloXamarin.Core.Migrations
{
    public partial class CreateInitialSchema : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Stamps",
                columns: table => new
                {
                    Id = table.Column<string>(nullable: false),
                    StampedAt = table.Column<DateTime>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Stamps", x => x.Id);
                });
        }

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

他にも、20180406052413_CreateInitialSchema.Designer.cs と ApplicationDbContextModelSnapshot.cs も生成されたが、 誌面の都合で省略。

アプリ起動時にマイグレーションを実行するように修正

ApplicationDbContext とマイグレーション用のソースコード一式を、 Xamarin.iOS プロジェクトにリンクとして追加する。

using Foundation;
using HelloXamarin.Core;
using Microsoft.EntityFrameworkCore;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using UIKit;

namespace HelloXamarin
{
    public class Application
    {
        static void Main(string[] args)
        {
            UIApplication.Main(args, null, "AppDelegate");
        }
    }

    [Register("AppDelegate")]
    public class AppDelegate : UIApplicationDelegate
    {
        public override UIWindow Window { get; set; }

        public ApplicationDbContext DbContext { get; private set; }

        public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
        {
            // Xamarin.iOS で SQLite を使えるようにする
            SQLitePCL.Batteries_V2.Init();

            // データベースがなければ作る
            var dbPath = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
                "..",
                "Library",
                "helloxamarin.db");
            DbContext = new ApplicationDbContext(dbPath);
            DbContext.Database.Migrate();

            Window = new UIWindow(UIScreen.MainScreen.Bounds);
            Window.RootViewController = new UINavigationController(
                 new MainViewController(DbContext));
            Window.MakeKeyAndVisible();

            return true;
        }

        public override void WillTerminate(UIApplication application)
        {
            DbContext?.Dispose();
        }
    }

    public class MainViewController : UITableViewController
    {
        readonly ApplicationDbContext _dbContext;

        Stamp[] _stamps = new Stamp[0];

        UIBarButtonItem _addButton;

        public MainViewController(ApplicationDbContext dbContext)
            : base(UITableViewStyle.Plain)
        {
            Title = "Hello Xamarin";
            _dbContext = dbContext;
            _addButton = new UIBarButtonItem(
                UIBarButtonSystemItem.Add,
                HandleAddButtonClick);
        }

        async void HandleAddButtonClick(object sender, EventArgs e)
        {
            await AddStampAsync();
            await LoadStampsAsync();

            TableView.InsertRows(
                new[] { NSIndexPath.FromItemSection(0, 0) },
                UITableViewRowAnimation.Fade);
        }

        public override async void ViewDidLoad()
        {
            base.ViewDidLoad();

            NavigationItem.RightBarButtonItem = _addButton;

            await LoadStampsAsync();
        }

        public override nint RowsInSection(UITableView tableView, nint section)
        {
            return _stamps.Length;
        }

        public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
        {
            var cell = new UITableViewCell(UITableViewCellStyle.Default, "StampCell");
            var stamp = _stamps[indexPath.Row];
            cell.TextLabel.Text = stamp.StampedAt.ToString();
            return cell;
        }

        async Task AddStampAsync()
        {
            _dbContext.Stamps.Add(new Stamp());
            await _dbContext.SaveChangesAsync();
        }

        async Task LoadStampsAsync()
        {
            _stamps = await _dbContext.Stamps
                .OrderByDescending(s => s.StampedAt)
                .ToArrayAsync();
        }
    }
}

アプリを起動するとマイグレーションが実行された!

おわりに

EntityFrameworkCore のマイグレーションが使えるのは良かったが、 現状だとそのためにわざわざ .NET Core などの別プロジェクトを用意しないといけないので面倒。 正直使うかどうか悩ましいな。