波佐見陶器まつり

ゴールデンウィーク期間中に長崎県波佐見町で行われている波佐見陶器まつりに行ってきた。

60th 波佐見陶器まつり

陶器というと、同時期に開催されている有田陶器市の方が規模が大きく有名だけど、波佐見陶器まつりも中々。というか予想以上の賑わい。甘く見ていた。

会場は波佐見町やきもの公園一帯と、コンパクトにまとまっていて、家族で回るのにちょうどいい具合。 これ以上広いと、子供を連れて回るのはシンドイかもしれない。

大きなテントの中にはいろんな窯元のブースがあり、器を見て回るだけでも見応えがあった。 これぞザ・陶器といった窯元ばかりかと思っていたけど、モダンでオシャレな器を作っているところもあって、 老若男女問わず楽しめそうな印象。事実、若い人も結構見かけた。

芝生広場の方には、窯元のテントだけでなく、陶器以外の特産品も売り出していた。 ただこの日は暑かったので、アイスクリームやソフトクリーム、ジェラートなんかに目がいってしまいがち。

ちなみに戦利品は箸置き2種類。 1つ購入した後に、もっと良いものを見つけてしまい、もう1種類買ってしまった。 これ、陶器まつりあるあるだろうな。

PostgreSQL で Entity Framework Core を使っていて money 型でハマった

PostgreSQL で Entity Framework Core を使っていて、PostgreSQL の money 型の列を C# の decimal 型のプロパティにマップしたら、プロパティに格納される値がなんか変でハマった。テーブルに格納されている値の 100 分の 1 になってる。

試したのは下記のコード。

using Microsoft.EntityFrameworkCore;
using System;
using System.Threading.Tasks;

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

        public string Name { get; set; }

        public decimal Price { get; set; }
    }

    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext() : base()
        {
            Products = Set<Product>();
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseNpgsql(
                "Host=localhost;Username=postgres;Password=p@ssword;Database=sample");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Product>(b =>
            {
                b.Property(e => e.Id)
                    .UseNpgsqlSerialColumn()
                    .HasColumnName("id");

                b.Property(e => e.Name)
                    .IsRequired()
                    .HasColumnName("name");

                b.Property(e => e.Price)
                    .IsRequired()
                    .HasColumnName("price")
                    .HasColumnType("money");

                b.ToTable("products");
                b.HasKey(e => e.Id);
            });
        }

        public DbSet<Product> Products { get; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            MainAsync(args).GetAwaiter().GetResult();

            Console.WriteLine("Press Enter Key.");
            Console.ReadLine();
        }

        static async Task MainAsync(string[] args)
        {
            // テストデータを作りなおす
            using (var context = new ApplicationDbContext())
            {
                await context.Database.EnsureDeletedAsync();
                await context.Database.EnsureCreatedAsync();
                context.Products.Add(new Product()
                {
                    Name = "foo",
                    Price = 12345,
                });
                context.Products.Add(new Product()
                {
                    Name = "bar",
                    Price = 67890
                });
                await context.SaveChangesAsync();
            }

            // Entity Framework Core で普通に取得
            Console.WriteLine("EF Normal");
            using (var context = new ApplicationDbContext())
            {
                var products = await context.Products.ToListAsync();
                foreach (var p in products)
                {
                    Console.WriteLine(
                        $"id:{p.Id}\tname:{p.Name}\tprice:{p.Price}");
                }
            }

            // Entity Framework Core で SQL を指定して取得
            Console.WriteLine("FromSql");
            using (var context = new ApplicationDbContext())
            {
                var products = await context.Products
                    .FromSql("SELECT id, name, price FROM products")
                    .ToListAsync();
                foreach (var p in products)
                {
                    Console.WriteLine(
                        $"id:{p.Id}\tname:{p.Name}\tprice:{p.Price}");
                }
            }

            // Entity Framework Core で money を numeric にキャストする SQL を指定して取得
            Console.WriteLine("FromSql with convert");
            using (var context = new ApplicationDbContext())
            {
                var products = await context.Products
                    .FromSql("SELECT id, name, price::money::numeric FROM products")
                    .ToListAsync();
                foreach (var p in products)
                {
                    Console.WriteLine(
                        $"id:{p.Id}\tname:{p.Name}\tprice:{p.Price}");
                }
            }

            // NpgsqlConnection を直に使って取得
            Console.WriteLine("ExecuteReaderAsync");
            using (var context = new ApplicationDbContext())
            {
                using (var connection = context.Database.GetDbConnection())
                {
                    await connection.OpenAsync();
                    using (var command = connection.CreateCommand())
                    {
                        command.CommandText = "SELECT id, name, price FROM products";
                        using (var reader = await command.ExecuteReaderAsync())
                        {
                            while (await reader.ReadAsync())
                            {
                                Console.WriteLine(
                                    $"id:{reader[0]}\tname:{reader[1]}\tprice:{reader[2]}");
                            }
                        }
                    }
                }
            }

            // NpgsqlConnection を直に使い、money を numeric にキャストする SQL を指定して取得
            Console.WriteLine("ExecuteReaderAsync with convert");
            using (var context = new ApplicationDbContext())
            {
                using (var connection = context.Database.GetDbConnection())
                {
                    await connection.OpenAsync();
                    using (var command = connection.CreateCommand())
                    {
                        command.CommandText = "SELECT id, name, price::money::numeric FROM products";
                        using (var reader = await command.ExecuteReaderAsync())
                        {
                            while (await reader.ReadAsync())
                            {
                                Console.WriteLine(
                                    $"id:{reader[0]}\tname:{reader[1]}\tprice:{reader[2]}");
                            }
                        }
                    }
                }
            }
        }
    }
}

実行結果がこちら。

f:id:griefworker:20180426114225p:plain

Entity Framework Core の問題かと思ってたら、Npgsql を直に使った場合も同じだった。 Npgsql 内で PostgreSQL の money 型を C# の decimal 型にマップしているところに原因がありそう。 GitHub の Npgsql リポジトリで MoneyHandler のソースコードを見てみた。

https://github.com/npgsql/npgsql/blob/b8ee19e50884ad740ebc78086d90082818f8c2c5/src/Npgsql/TypeHandlers/NumericHandlers/MoneyHandler.cs

短いのでクラスを引用。

[TypeMapping("money", NpgsqlDbType.Money, dbType: DbType.Currency)]
class MoneyHandler : NpgsqlSimpleTypeHandler<decimal>
{
    public override decimal Read(NpgsqlReadBuffer buf, int len, FieldDescription fieldDescription = null)
        => buf.ReadInt64() / 100m;

    public override int ValidateAndGetLength(decimal value, NpgsqlParameter parameter)
        => value < -92233720368547758.08M || value > 92233720368547758.07M
            ? throw new OverflowException($"The supplied value ({value}) is outside the range for a PostgreSQL money value.")
            : 8;

    public override void Write(decimal value, NpgsqlWriteBuffer buf, NpgsqlParameter parameter)
        => buf.WriteInt64((long)(Math.Round(value, 2, MidpointRounding.AwayFromZero) * 100m));
}

Read 時に 100m で割っている。怪しい。こいつが犯人か。 なぜこんな実装になっているのか調べたけど、ちょっと時間切れ。 money ではなく numeric を使った方が良かったかもな。

『かぐや様は告らせたい(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 などの別プロジェクトを用意しないといけないので面倒。 正直使うかどうか悩ましいな。