Xamarin.iOS で EntityFrameworkCore を使う

Xamarin.iOS で EntityFrameworkCore を使うことができたのでメモしておく。 データベースは SQLite。 なのでパッケージは Microsoft.EntityFrameworkCore.Sqlite をインストールすることになる。

www.nuget.org

Visual Studio で NuGet パッケージをインストールしたら、 EntityFrameworkCore + SQLite を使うようにコードで構成する。 ASP.NET Core のときみたいに DI は使わない。

using Foundation;
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)
        {
            // SQLite を初期化
            SQLitePCL.Batteries_V2.Init();

            // データベースがなければ作る
            DbContext = new ApplicationDbContext();
            DbContext.Database.EnsureCreated();

            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();
        }
    }

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

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

    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext()
            : base()
        {
            Stamps = Set<Stamp>();
        }

        public DbSet<Stamp> Stamps { get; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            var dbPath = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
                "..",
                "Library",
                "helloxamarin.db");

            // SQLite を使う
            optionsBuilder.UseSqlite(
                connectionString: $"Filename={dbPath}");

            base.OnConfiguring(optionsBuilder);
        }
    }
}

SQLitePCL.Batteries_V2.Init()SQLite の初期化は必須。 あと、UseSqlite で名前付き引数 connectionString を明示的に指定しているのは、 これをしないと DbConnection を引数で受け取るオーバーロードが影響してビルドエラーになってしまったから。

少々ハマりもしたけど、思ったよりはすんなり使えた感じ。 Xamarin でデータベースが必要になったとき、 今までは SQLite-net PCL や Realm を選んでいたけど、 ASP.NET Core で慣れている EntityFrameworkCore も自分的にはアリだな。