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

『Extensive Xamarin』を読んだ

『Extensive Xamarin』の Kindle 版を読んだので読書メモなど。

第1章 Embeddinator-4000の設計と実装

Embeddinator-4000 が想定する「ネイティブアプリケーション開発者が .NET のライブラリを利用する」ユースケースの需要は、キラーライブラリの出現次第かも。.NET Standard 対応のライブラリは今後増えていくだろうから、可能性はあると思いたい。

あと、Xamarin プラットフォームの仕組みについても書かれていて、Xamarin.iOS と Xamarin.Android の復習もできた。 中の人の記事が読めるのは、ホント貴重。

第2章 Xamarin. Macアプリケーションの配布方法

まったく無名の状態で macOS アプリをリリースするなら、AppStore で配布もありだが、SNS などで自力で宣伝できるなら AppStore で配布するメリットがたいしてなさそう。AppStore 必須にでもならないかぎり、自分も使わないだろうな。

第3章 Plugins for Xamarin & Unit Test

Xamarin 向けの NuGet パッケージで使われるテクニック Bait and Switch は、名前だけ知っている状態だったので、実際どんなものか知ることができて良かった。

第4章 MonkeyFest 2017参加レポート

海外のカンファレンスに応募するところからの記事を読めるのが同人誌ならでは。

第5章 世界を広げる Microsoft Cognitive Services

Cognitive Services の導入方法はわかったが、これを使ってどんな面白いことができるか思いつかなかった。

第6章 IL2Cプロジェクト

.NET の IL を C 言語のソースコードにトランスレートするツールの開発記事は、非常に面白く読めた。ちょうど、コンパイラやトランスパイラを書いてみたいと思っていたところだったので、この記事で出てきたノウハウは参考になりそう。評価スタックのところで、スタックをローカル変数に置き換えたのは、なるほどその手があるのかと感心した。

Extensive Xamarin ─ひろがるXamarinの世界─ Essential Xamarinシリーズ (技術書典シリーズ(NextPublishing))

Extensive Xamarin ─ひろがるXamarinの世界─ Essential Xamarinシリーズ (技術書典シリーズ(NextPublishing))

『金の彼女 銀の彼女(1)〜(9)』を読んだ

月刊少年マガジンで連載されている『金の彼女 銀の彼女』を Kindle でまとめ買いして読んだ。

どんなマンガかというと

Wikipedia から引用。

安田登郎が憧れる綾之峰財閥のお嬢様である綾之峰英里華。彼女は綾峰山の伝説の泉に落ちた事によって、泉の女神によって金と銀二人の英里華に分裂してしまった。二人の英里華は登郎に迫られながら、かつ他の少女たちを巻き込んだ登郎争奪戦を展開することになる。

ヒロインが2人に分裂するというトンデモ設定だが、主人公とヒロインの身体が入れ替わったりする作品もあるわけだし、これもアリか。

金のほうは真性お姫様

金の方の英里華は対外的には分裂する前の英里華を引き継いでいるので、綾之峰の家や姫の肩書きという荊棘に縛られていて、少々ヒロインとしての魅力に欠けていたように思う。

ただ、主人公の活躍もあって荊棘が切れたころから、いろんな表情を見せるようになって、黒くなったり嫉妬したりと、なかなかいいキャラになった。

銀の方は俗に言うツンデレというやつ

銀香は主人公と同じ家で暮らすようになるという圧倒的に有利な立場でありながら、素直になれないせいで、悪友的ポジションに収まってしまった。でも元は同じ人間、性格は真逆でも好みは同じで、 結局は予想通り巻き返しを誓うわけだが。

一緒に暮らしているだけあって、出番としては銀香の方が圧倒的で、現状こっちが真のメインヒロインに思える。

そして主人公の登郎

月刊とはいえ一応少年向けのマンガの主人公としてどうなのかと思うくらいの、流れるようなセクハラの数々。 作中でもセクハラ王子という称号を持ってたりするし。ラッキースケベというより、むしろ積極的にセクハラしているように思える。ここまでくるともはや潔い。

ただ、セクハラだけの男ではなく、持ち前の行動力でヒロインたちのピンチをことごとく救っていく様は格好良い。まさに行動力の化身。

この手のラブコメって

主人公に好意を持つヒロインが4〜5人出てきてハーレム状態になることが多い。 このマンガでも出てはくるが、メインの2人が強すぎて、即モブキャラと化してしまっている。 もとは主人公が憧れていた同じ人物だったわけで、その2人が争ったら他のヒロインは出る幕がない。

なんと 10巻が最終刊らしい

9巻の巻末にそう書いてあって、非常に驚いた。 ダラダラ引き延ばさないところは好印象。 はたして金と銀どちらが勝つのか。 自分としては、むしろハーレムルートしかないのではと思っている。 舞台の綾之峰は日本の中にあって違う国みたいな扱いだし、治外法権でしょ。

Azure SQL Database でデータベースをまたがった SQL を実行可能にする手順メモ

Azure SQL Database でも、 外部データソースと外部テーブルを使ってデータベースをまたがった SQL を実行できるので、 手順をメモしておく。

マスターキーの作成

CREATE MASTER KEY ENCRYPTION BY PASSWORD = '<パスワード>'

データベース スコープ ベースの資格情報の作成

CREATE DATABASE SCOPED CREDENTIAL [<資格情報名>]
WITH IDENTITY='<ログイン名>', SECRET='<パスワード>'

外部データソースの作成

CREATE EXTERNAL DATA SOURCE [<外部データソース名>]
WITH
(
  TYPE=RDBMS,
  LOCATION='<SQL Server 名>.database.windows.net',
  DATABASE_NAME='<データベース名>',
  CREDENTIAL=[<資格情報名>]
)

外部テーブルの作成

CREATE EXTERNAL TABLE [<外部テーブル名>] (
  -- 対象のテーブルと同じ列を定義する
) WITH (
  DATA_SOURCE = [<外部データソース名>]
)

上記すべてを、外部テーブルを作成するデータベースに対して行う。