『かぐや様は告らせたい(9)』を読んだ

9巻でようやく、重要なエピソードの1つである石上の過去が明らかに。 石上を救う言葉をかけた会長にはシビれたし、石上がひた隠しにしていたものを独自調査で突き止めた生徒会はさすが。 あと、かぐやもしれっと暗躍していてウケた。 というか、かぐやは石上のこと可愛がり過ぎでは。 過去を乗り越えて前を向いた石上は、まさに裏主人公といった感じて好感持てる。

それ以外では、白銀家はとんでもない父親だったし、恒例の特訓回では藤原書記の新しい一面が見れたしと、収録されてる話のどれもが面白かった。満腹。

メタプログラミング事始め

はじめに

メタプログラミング.NET』を読んで、 C# で黒魔術もといメタプログラミングを習得したくなってしまったので、 まずは簡単なお題で練習してみた。

メタプログラミング.NET (アスキー書籍)

メタプログラミング.NET (アスキー書籍)

お題

任意のクラスのインスタンスを生成し Dictionary<string, object> に格納された値で初期化するファクトリーメソッドを作成する。

例えば

public class Product
{
    public long Id { get; set; }

    public string Code { get; set; }

    public string Name { get; set; }

    public decimal Price { get; set; }
}

のようなクラスがある場合に

public static Product Create(IDictionary<string, object> data)
{
    var product = new Product();
    product.Id = (long)data["Id"];
    product.Code = (string)data["Code"];
    product.Name = (string)data["Name"];
    product.Price = (decimal)data["Price"];
    return product;
}

という感じのファクトリーメソッドを動的に生成するのが目標。

リフレクション

まずは素直にリフレクションを使ってみる。

 public static T CreateUsingRefrection<T>(IDictionary<string, object> data)
 {
     var obj = Activator.CreateInstance<T>();

     var type = typeof(T);
     var properties = type.GetProperties(
         BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);
     foreach (var p in properties)
     {
         var value = data[p.Name];
         var convertedValue = Convert.ChangeType(value, p.PropertyType);
         p.SetValue(obj, convertedValue);
     }

     return obj;
 }

動的に生成していないけど、分かりやすいので、 パフォーマンスを求められない部分ならこれでもいいかと思ってる。

Expression

お次は System.Linq.Expressions にある Expression を使って、動的にデリケートを生成してみる。

public static Func<IDictionary<string, object>, T> CreateFuncUsingExpression<T>()
{
    var resultType = typeof(T);

    // IDictionary<string, object> data;
    var data = Expression.Parameter(typeof(IDictionary<string, object>), "data");

    // T result;
    var result = Expression.Parameter(resultType, "result");

    var expressions = new List<Expression>();

    // result = new T();
    expressions.Add(
        Expression.Assign(
            result,
            Expression.New(resultType)));

    var properties = resultType.GetProperties(
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);
    foreach (var p in properties)
    {
        // result.Property = (PropertyType)data.Item["Property"];
        // を組み立てる
        expressions.Add(
            Expression.Assign(
                Expression.PropertyOrField(
                    result,
                    p.Name),
                Expression.Convert(
                    Expression.Property(
                        data,
                        "Item",
                        Expression.Constant(p.Name)),
                    p.PropertyType)));
    }

    expressions.Add(result);

    var body = Expression.Block(
        resultType,
        new[] { result },
        expressions);

    var e = Expression.Lambda<Func<IDictionary<string, object>, T>>(
        body,
        data);

    var f = e.Compile();
    return f;
}

メソッド呼び出すだけなら Expression で良いけど、ブロックが必要になった場合はシンドイ。デバッガを使ったカンニングがそのまま使えず苦労した。

Refrection.Emit

最後は Refrection.Emit を使い、IL 手書きで動的デリケート生成。

public static Func<IDictionary<string, object>, T> CreateFuncUsingRefrectionEmit<T>()
{
    var resultType = typeof(T);
    var resultConstructor = resultType.GetConstructor(new Type[0]);
    var resultProperties = resultType.GetProperties(
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);

    var argType = typeof(IDictionary<string, object>);
    var itemProperty = argType.GetProperty(
        "Item",
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty);


    var dm = new DynamicMethod(
        Guid.NewGuid().ToString("N"),
        resultType,
        new[] { argType });

    var il = dm.GetILGenerator();

    il.Emit(OpCodes.Newobj, resultConstructor);

    foreach (var p in resultProperties)
    {
        // result.Property = (PropertyType)arg.Item["Property"];
        // を組み立てる。
        il.Emit(OpCodes.Dup);
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldstr, p.Name);
        il.Emit(OpCodes.Callvirt, itemProperty.GetMethod);
        if (p.PropertyType.IsValueType)
        {
            // 値型のときはボックス化解除
            il.Emit(OpCodes.Unbox_Any, p.PropertyType);
        }
        else
        {
            // クラスのときはキャスト
            il.Emit(OpCodes.Castclass, p.PropertyType);
        }
        il.Emit(OpCodes.Callvirt, p.SetMethod);
    }

    il.Emit(OpCodes.Ret);

    var f = (Func<IDictionary<string, object>, T>)dm.CreateDelegate(typeof(Func<IDictionary<string, object>, T>));
    return f;
}

Expression で苦労したブロックも、IL 手書きだと意外にスンナリ書けた。

コード全体

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;

namespace BlackMagicBenchmarks
{
    public class RefrectionVsExpressionVsIL
    {
        IDictionary<string, object> data;
        Func<IDictionary<string, object>, Product> expression;
        Func<IDictionary<string, object>, Product> refrectionEmit;

        [GlobalSetup]
        public void Setup()
        {
            data = new Dictionary<string, object>()
            {
                ["Id"] = 1L,
                ["Code"] = "0001",
                ["Name"] = "Visual Studio",
                ["Price"] = 100000m,
            };
            expression = ProductFactory.CreateFuncUsingExpression<Product>();
            refrectionEmit = ProductFactory.CreateFuncUsingRefrectionEmit<Product>();
        }

        [Benchmark]
        public Product Manual()
        {
            return ProductFactory.Create(data);
        }

        [Benchmark]
        public Product Refrection()
        {
            return ProductFactory.CreateUsingRefrection<Product>(data);
        }

        [Benchmark]
        public Product Expression()
        {
            return expression(data);
        }

        [Benchmark]
        public Product RefrectionEmit()
        {
            return refrectionEmit(data);
        }
    }

    public static class ProductFactory
    {
        public static Product Create(IDictionary<string, object> data)
        {
            var product = new Product();
            product.Id = (long)data["Id"];
            product.Code = (string)data["Code"];
            product.Name = (string)data["Name"];
            product.Price = (decimal)data["Price"];
            return product;
        }

        public static T CreateUsingRefrection<T>(IDictionary<string, object> data)
        {
            var obj = Activator.CreateInstance<T>();

            var type = typeof(T);
            var properties = type.GetProperties(
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);
            foreach (var p in properties)
            {
                var value = data[p.Name];
                var convertedValue = Convert.ChangeType(value, p.PropertyType);
                p.SetValue(obj, convertedValue);
            }

            return obj;
        }

        public static Func<IDictionary<string, object>, T> CreateFuncUsingExpression<T>()
        {
            var resultType = typeof(T);

            // IDictionary<string, object> data;
            var data = Expression.Parameter(typeof(IDictionary<string, object>), "data");

            // T result;
            var result = Expression.Parameter(resultType, "result");

            var expressions = new List<Expression>();

            // result = new T();
            expressions.Add(
                Expression.Assign(
                    result,
                    Expression.New(resultType)));

            var properties = resultType.GetProperties(
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);
            foreach (var p in properties)
            {
                // result.Property = (PropertyType)data.Item["Property"];
                // を組み立てる
                expressions.Add(
                    Expression.Assign(
                        Expression.PropertyOrField(
                            result,
                            p.Name),
                        Expression.Convert(
                            Expression.Property(
                                data,
                                "Item",
                                Expression.Constant(p.Name)),
                            p.PropertyType)));
            }

            expressions.Add(result);

            var body = Expression.Block(
                resultType,
                new[] { result },
                expressions);

            var e = Expression.Lambda<Func<IDictionary<string, object>, T>>(
                body,
                data);

            var f = e.Compile();
            return f;
        }

        public static Func<IDictionary<string, object>, T> CreateFuncUsingRefrectionEmit<T>()
        {
            var resultType = typeof(T);
            var resultConstructor = resultType.GetConstructor(new Type[0]);
            var resultProperties = resultType.GetProperties(
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);

            var argType = typeof(IDictionary<string, object>);
            var itemProperty = argType.GetProperty(
                "Item",
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty);


            var dm = new DynamicMethod(
                Guid.NewGuid().ToString("N"),
                resultType,
                new[] { argType });

            var il = dm.GetILGenerator();

            il.Emit(OpCodes.Newobj, resultConstructor);

            foreach (var p in resultProperties)
            {
                // result.Property = (PropertyType)arg.Item["Property"];
                // を組み立てる。
                il.Emit(OpCodes.Dup);
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldstr, p.Name);
                il.Emit(OpCodes.Callvirt, itemProperty.GetMethod);
                if (p.PropertyType.IsValueType)
                {
                    // 値型のときはボックス化解除
                    il.Emit(OpCodes.Unbox_Any, p.PropertyType);
                }
                else
                {
                    // クラスのときはキャスト
                    il.Emit(OpCodes.Castclass, p.PropertyType);
                }
                il.Emit(OpCodes.Callvirt, p.SetMethod);
            }

            il.Emit(OpCodes.Ret);

            var f = (Func<IDictionary<string, object>, T>)dm.CreateDelegate(typeof(Func<IDictionary<string, object>, T>));
            return f;
        }
    }

    public class Product
    {
        public long Id { get; set; }

        public string Code { get; set; }

        public string Name { get; set; }

        public decimal Price { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<RefrectionVsExpressionVsIL>();
        }
    }
}

ベンチマーク

BenchmarkDotNet を使ってベンチマークをとってみた。

f:id:griefworker:20180416172552p:plain

リフレクションは桁違いに遅い。 Expression と Refrection.Emit は大差無い。

おわりに

Expression ならデバッガー、Reflection.Emit ではアセンブリを dnSpy で解析することでカンニングできる。 一から全部自分で書かなくてもいい。 今回のケースでは、Expression よりも Reflection.Emit の方が書きやすかった。 Reflection.Emit に慣れたら、Expression を使うことは無くなるかもな。

『渡くんの××が崩壊寸前(1)〜(5)』を読んだ

月刊ヤングマガジンで連載中の『渡くんの××が崩壊寸前』を Kindle でまとめ買いして読んだ。ラブコメなんだけど、 ところどころサスペンスの香りが漂っていて、二つの意味でドキドキハラハラしている。登場人物みんな何らかの闇を抱えてそうで。

あらすじ

公式から引用。

ラーメン大好き小泉さん』の鳴見なるによる、“三角関係+妹”の日常崩壊ラブコメ

「最悪の幼なじみ」「憧れの同級生」「ブラコンの妹」の3人の女性に主人公・渡直人が振り回される!

紗月は最悪の幼なじみには思えない

6年前に渡家の畑を荒らし、渡くんが恋愛に臆病になった元凶で、さらにはストーカーじみた言動も目立つけど、言うほど最悪の幼なじみではない。 基本的に渡くんの味方だし。 渡くんの恋の応援さえしてしまう、 憎めないヒロイン。 6年前の事件は何か訳がありそうだけど、まだ明らかになってない。気になる。

正統派ヒロインの石原さんは危うさが見え隠れしている

渡くんは他の男の子とは違う、と一方的に潔癖なイメージを押し付けていると思ったら、一転、紗月に対抗心燃やしてキスより進みたがったり。 渡くん憧れの同級生で、外見良し性格良しという正統派ヒロインなんだけど、どこか危うさがある。

あと梅澤っていう後輩キャラにも期待

渡くんが気になりだしたみたいだが、悲しいかな報われることはないんだろうな。結構好きなんだけど。 渡くんと紗月と石原さんの三角関係を掻き回してほしい。

まだ明らかになってない謎が多い

叔母の多摩代が渡兄妹を引き取ったのにも、理由があるみたいだ。少なくとも、親切とかじゃなさそう。 さらに、もっともマトモに見える友人の徳井も、人に言えない趣味嗜好とか持ってそうな描写があって、もうね…。おいおい、主要な登場人物みんな何か抱えてるのかよ、って思ったね。

とにかく最後までラブコメでいってほしい

各自が抱えてる爆弾が爆発して、 一転サスペンスとかは勘弁してほしい。 そういう意味でも続きが非常に気になっているんだけど、月刊なので、単行本は年に2冊がいいところ。 待ち遠しい。 読者としては週刊に移籍して欲しいくらいだ。

ASP.NET Core はインテグレーションテストが書きやすい

ASP.NET Core はインテグレーションテストをちゃんとサポートしているので、Web API のテストが書きやすい。

www.nuget.org

このパッケージを使えば、xUnit を使ったテストプロジェクトで Web API を簡単にホストしてテストできる。自分の場合、インテグレーションテスト用にベースクラスを作ることが多い。

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SampleApi.Data;
using System;
using System.Collections.Generic;

namespace SampleApi.Tests
{
    public abstract class IntegrationTestBase : IDisposable
    {
        protected TestServer Server { get; }

        protected IntegrationTestBase()
        {
            var config = new ConfigurationBuilder()
                .AddInMemoryCollection(new Dictionary<string, string>()
                {
                    ["ConnectionStrings:DefaultConnection"] = $"Server=(local);Database=SampleApi{Guid.NewGuid().ToString("N")};Integrated Security=SSPI;",
                })
                .Build();

            var host = new WebHostBuilder()
                .UseConfiguration(config)
                .UseEnvironment("Test")
                .UseStartup<Startup>();

            Server = new TestServer(host);

            GetService<ApplicationDbContext>().Database.EnsureCreated();
        }

        public void Dispose()
        {
            GetService<ApplicationDbContext>().Database.EnsureDeleted();
            Server.Dispose();
        }

        protected T GetService<T>() => Server.Host.Services.GetService<T>();
    }
}

インテグレーションテストでは実際にデータベースに接続したいので、テスト用のデータベースを毎回作り直している。当然ながらテストは遅くなるので、改善したいところ。 まぁ、インテグレーションテストはユニットテストほど高速に回す必要も今のところないかなとは思ってるんだけど。

実際に書くテストはこんな感じ。

using Newtonsoft.Json.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace SampleApi.Tests
{
    public class AccountApiTest : IntegrationTestBase
    {
        public AccountApiTest()
            : base()
        {
        }

        [Fact]
        public async Task Loginでアクセストークンを取得できる()
        {
            using (var client = Server.CreateClient())
            {
                var response = await client.PostAsync(
                    @"/api/users/login",
                    new StringContent(
                        @"{
                            'userName': 'testuser',
                            'password': 'p@ssword'
                        }",
                        Encoding.UTF8,
                        "application/json"));
                Assert.Equal(HttpStatusCode.OK, response.StatusCode);

                var content = await response.Content.ReadAsStringAsync();
                var jObj = JObject.Parse(content);
                Assert.NotNull((string)jObj["token"]);
            }
        }
    }
}

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 などの別プロジェクトを用意しないといけないので面倒。 正直使うかどうか悩ましいな。

『王様達のヴァイキング(15)』を読んだ

テロリストに占拠されたサミット会場から人質を救出する作戦は、一刻を争う緊迫した場面の連続で手に汗にぎった。最後は、このマンガのラスボスであるはずの蘇芳にすべて持っていかれてしまった感じ。今回の活躍で世論も味方につけて、より強大な存在になりそうな予感。

王様達のヴァイキング(15) (ビッグコミックス)

王様達のヴァイキング(15) (ビッグコミックス)

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 も自分的にはアリだな。