ASP.NET CoreでTypeScriptを使う(続き・改)

前回のTypeScriptでjQueryを使うの別パターンです。

ichiroku11.hatenablog.jp

前回はjQueryを取得するのにBowerを使いましたが、npm+Gulpでできないかなと思ったので試してみました。まずは前回の続きからbower.jsonとwwwroot/libフォルダを削除しておきましょう。

npmでjQueryを取得

npmを使ってjQueryを取得します。具体的にはnpm 構成ファイル(package.json)に以下を追加します。

"dependencies": {
    "jquery": "3.2.1",
},

package.jsonは次のようになります。

{
    "version": "1.0.0",
    "name": "asp.net",
    "private": true,
    "dependencies": {
        "jquery": "3.2.1"
    },
    "devDependencies": {
        "@types/jquery": "3.2.16",
        "gulp": "3.9.1"
    }
}

ファイルを保存して少し待つとnode_modules/jqueryフォルダができると思います。

GulpでjQueryをコピー

Gulp 構成ファイル(gulpfile.js)を次のように修正します。publishタスクでnode_modules/jquery/distにあるjQueryファイルをwwwroot/lib/jquery/distにコピーするようにしています。

const gulp = require("gulp");

// publishタスク
gulp.task("publish", () => {
    // jQueryのjsとかをまるっとwwwroot/libにコピーする
    gulp.src(["node_modules/jquery/dist/*", ])
        .pipe(gulp.dest("wwwroot/lib/jquery/dist"));

    // Scriptsフォルダ内のts、jsファイルをwwwroot/jsフォルダにコピーする
    gulp.src(["scripts/**/*.ts", "scripts/**/*.js", "scripts/**/*.map"])
        .pipe(gulp.dest("wwwroot/js"));
});

// detaultタスク
gulp.task("default", () => {
    // Scriptsフォルダ内のjsファイルの変更を監視して、"publish"タスクを実行する
    gulp.watch("scripts/**/*.js", ["publish"]);
});

スクランナーエクスプローラーでpublishタスクを実行しましょう。Index.cshtmlを変更する必要はなく、そのままデバッグ実行するとalertが表示されるはずです。

こんな感じでいいのかなと。

ASP.NET CoreでTypeScriptを使う(続き)

前回の続きでもうちょっとします。

ichiroku11.hatenablog.jp

今回はjQueryを導入して、TypeScriptで次のコードを実行できるようにします。

// app.ts
$(event => {
    alert("Hello!");
});

app.tsを上記コードに書き換えてもjQueryを読み込んでいないので動きません。そのまえにエラーも出ていると思います。これらを解消していきます。

Bower構成ファイルを準備する

Bowerを使ってjQueryをプロジェクトにインストールします。npmではできないの?という気もしますし、webpack?Parcel?というツールも耳にしますが正直わからないのでおとなしくBowerを使うことにします。

まずBower 構成ファイル(bower.json)を作りましょう。中身はこんな感じです。

{
    "name": "asp.net",
    "private": true,
    "dependencies": {
        "jquery": "3.2.1"
    }
}

ファイルを保存して少し待つと、wwwrootフォルダの中にlib/jquery/フォルダができます。フォルダの中にたくさんファイルがありますが、jQueryのjsファイルはここにあります。

wwwroot/lib/jquery/dist/jquery.js

Index.cshtmlでjQueryを読み込む

Index.cshtmlにscript要素を追加して、src属性でjquery.jsを指定しましょう。<script src="~/lib/jquery/dist/jquery.js"></script>の部分ですね。

@page
@model WebApp.Pages.IndexModel
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
   <meta name="viewport" content="width=device-width" />
   <title>Index</title>
</head>
<body>
    <h1>Hello!</h1>
    @* jQueryを読み込む *@
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/js/app.js"></script>
</body>
</html>

TypeScriptの定義ファイルを使う

app.tsのエラーを回避するためにTypeScriptの定義ファイルを使います。npmを使ってインストールするといいみたいです。package.json"@types/jquery": "3.2.16"を足します。

{
    "version": "1.0.0",
    "name": "asp.net",
    "private": true,
    "devDependencies": {
        "@types/jquery": "3.2.16",
        "gulp": "3.9.1"
    }
}

ファイルを保存して少し待つと、node_modulesフォルダにインストールされます。TypeScriptのエラーも消えるはずです。消えない場合はソリューションを開き直すといいかもしれません。

デバッグ実行するとalertが表示されます。

目標クリア。

参考

ASP.NET CoreでTypeScriptを使う

Visual Studio 2017でASP.NET Core Web アプリケーションプロジェクトからTypeScriptを試せる環境を作るまでのメモです。

ASP.NET Coreもそうですが、npmもgulpもbowerもわからない状況だったので、空のプロジェクトから始めると理解できるかなと。色々やってみて少しわかってきたので書き残しておきます。(個人的にこんなにツールを使わないといけないの?感があります・・・。)

まず参考にしたのはこちらのドキュメントです。

ASP.NET Core · TypeScript

+αでRazor Pages(やっぱりhtmlよりcshtmlの方がいいので)やgulp.watchも試しました。

ASP.NET CoreでTypeScript

ASP.NET Coreの空のプロジェクトからはじめて、次のTypeScriptのコードが実行できるまでを目指します。

// 実行したいコード
document.addEventListener("DOMContentLoaded", event => {
    alert("Hello!");
});

手順はこんな感じです。

  1. プロジェクトを作成する
  2. Startupクラスを設定する
  3. TypeScriptを実行するcshtmlを準備する
  4. TypeScriptの構成ファイルを準備する
  5. 実行するTypeScriptファイルを準備する
  6. npm構成ファイルを準備する
  7. Gulp構成ファイルを準備する

順番に見ていきましょう。

プロジェクトを作成する

ASP.NET Core Web アプリケーションのプロジェクトを選択してプロジェクトを作ります。上記ドキュメントと同じなので端折ります。

Startupクラスを設定する

プロジェクトで静的なファイルを扱えるようにして、Razor Pagesも動くようにします。具体的にはStartupクラス(Startup.cs)を次のようにします。

public class Startup {
    // 使用するサービスを設定
    public void ConfigureServices(IServiceCollection services) {
        // MVCを追加
        services.AddMvc();
    }

    // ミドルウェアを設定
    public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
        if (env.IsDevelopment()) {
            app.UseDeveloperExceptionPage();
        }

        // 静的なファイルを扱う
        app.UseStaticFiles();

        // MVCを扱う(Razor Pagesを使う場合もこれ)
        app.UseMvc();
    }
}

Configureメソッドは、HTTPリクエストを処理する方法を指定するといった感じかなと。

詳しくはこのあたり。

ASP.NET Core でのアプリケーションの起動 | Microsoft Docs

TypeScriptを実行するcshtmlを準備する

TypeScriptを実行するcshtmlを用意しましょう。プロジェクトの直下にPagesフォルダを作って、そのフォルダにIndex.cshtmlを作ります。

Pages/
   └─ Index.cshtml

ソリューションエクスプローラーでPagesフォルダを選択してポップアップメニューの追加 > Razor ページから作るといい気がします。

Index.cshtmlの中身はこんな感じです。これから作るTypeScript(というかJavaScript)を読み込むためにscript要素を足しておきます。

@page
@model WebApp.Pages.IndexModel
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
   <meta name="viewport" content="width=device-width" />
   <title>Index</title>
</head>
<body>
    <h1>Hello!</h1>
    @* これから作るjsファイルを指しておく *@
    <script src="~/js/app.js"></script>
</body>
</html>

この段階でデバッグ実行するとページが表示されるはずです。

TypeScriptの構成ファイルを準備する

TypeScript JSON 構成ファイル(tsconfig.json)を作ります。compileOnSavetrueにしておくと、tsファイルを保存するとjsファイルができるようになります。

{
    "compilerOptions": {
        "noImplicitAny": true,
        "noEmitOnError": true,
        "removeComments": false,
        "sourceMap": true,
        "target": "es2015"
    },
    "exclude": [
        "node_modules",
        "wwwroot"
    ],
    "compileOnSave": true
}

細かい部分はよくわからないので、このあたりを確認しながらかなと。

試してませんが、tsconfig.jsonの代わりにプロジェクトのプロパティにあるTypeScript ビルドで設定してもいいのかもしれません。

実行するTypeScriptファイルを準備する

プロジェクト直下にScriptsフォルダを作ります。その中にapp.tsファイルも作りましょう。

Scripts/
   └─ app.ts

app.tsの中身は最初にでてきたコードです。やっとですね。

document.addEventListener("DOMContentLoaded", event => {
    alert("Hello!");
});

app.tsを保存するとapp.jsができてるはずです。

npm構成ファイルを準備する

npm 構成ファイル(package.json)を作ります。このあと説明するGulpを取得するためです。

{
    "version": "1.0.0",
    "name": "asp.net",
    "private": true,
    "devDependencies": {
        "gulp": "3.9.1"
    }
}

ファイルを保存してしばらくすると、プロジェクト直下にnode_modulesというフォルダができるみたいです。ソリューションエクスプローラーですべてのファイルを表示すると見えると思います。

Gulp構成ファイルを準備する

Gulp 構成ファイル(gulpfile.js)を作ります。Scriptsフォルダ内のts、jsファイルをwwwroot/jsフォルダにコピーするタスクを設定します。

/// <binding ProjectOpened='default' />

const gulp = require("gulp");

// publishタスク
// Scriptsフォルダ内のts、jsファイルをwwwroot/jsフォルダにコピーする
gulp.task("publish", () => {
    gulp.src(["scripts/**/*.ts", "scripts/**/*.js", "scripts/**/*.map"])
        .pipe(gulp.dest("wwwroot/js"));
});

// detaultタスク
// Scriptsフォルダ内のjsファイルの変更を監視して、publishタスクを実行する
// 監視するのはjsファイルでいいのかな?
gulp.task("default", () => {
    gulp.watch("scripts/**/*.js", ["publish"]);
});

スクランナーエクスプローラー(ソリューションエクスプローラーでgulpfile.jsを選択してポップアップメニューのスクランナー エクスプローラ)を開きます。publishタスクができているので、ダブルクリックなどでタスクを実行すると、wwwroot/jsフォルダにts、jsファイルがコピーされます。

wwwroot/
   └─ js/
      ├─ app.ts
      ├─ app.js
      └─ app.js.map

デバッグ実行すると、ページが表示されてalertが表示されます。

スクランナーエクスプローラーでdefaultタスクを選択してポップアップメニューのバインド > プロジェクトを開くを選択しておきます。ソリューションを開くと、defaultタスクが実行されてファイルの変更が監視されるようになります。tsファイルを保存するたびにwwwroot/jsフォルダにコピーされるので、開発が捗るんじゃないかなと思います。gulpfile.jsの1行目にある/// <binding ProjectOpened='default' />がそれなのかなと。

Gulpについてはこのへんを参考にしました。

これで目標達成です。

試した環境

Entity Framework Core - 楽観的同時実行制御のサンプル

EF Coreでrowversionデータ型を使った楽観的同時実行制御(排他制御)を試してみました。EF6と大体同じかなと思います。

Concurrency Tokens - EF Core | Microsoft Docs

EF Coreで楽観的同時実行制御するには、Data Annotationsで指定する方法とFluent APIで指定する方法があります。

まずはデータを用意しましょう。

-- テーブル作成
drop table if exists dbo.Monster;
create table dbo.Monster(
    Id int,
    Name nvarchar(20) not null,
    Version rowversion not null,  -- バージョン情報(トークンとも言ったり)
    constraint PK_Monster primary key(Id)
);

-- データ投入
insert into dbo.Monster(Id, Name)
values(1, N'スライム');

続いてモデル。Data Annotationsの場合は、rowversionのプロパティにTimestamp属性を指定します。

// モンスター
public class Monster {
    public int Id { get; set; }
    public string Name { get; set; }

    // Data Annotationsの場合
    [Timestamp]
    public byte[] Version { get; set; }
}

Fluent APIの場合は、Timestamp属性の代わりにDBコンテキストのOnModelCreatingメソッド内で次のように指定します。

// DBコンテキストの一部
public class AppDbContext : DbContext {
    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        // テーブルのマッピング
        modelBuilder.Entity<Monster>().ToTable(nameof(Monster));

        // Fluent APIの場合
        // Timestamp属性の代わりにこちらでも
        modelBuilder.Entity<Monster>()
            .Property(monster => monster.Version)
            .ValueGeneratedOnAddOrUpdate()
            .IsConcurrencyToken();
    }
}

準備が整ったので楽観的同時実行制御によって更新に失敗することを試してみましょう。

class Program {
    static void Main(string[] args) {
        using (var dbContext = new AppDbContext()) {
            // ロガープロバイダーを設定
            var serviceProvider = dbContext.GetInfrastructure();
            var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
            loggerFactory.AddProvider(new AppLoggerProvider());
        }

        // 同時に更新する1つ目のエンティティ
        var monster1 = default(Monster);
        using (var dbContext = new AppDbContext()) {
            monster1 = dbContext.Set<Monster>().Find(1);
        }

        // 同時に更新する2つ目のエンティティ
        // 楽観的同時実行制御による更新失敗を確認したいため、コピーを作っておく
        var monster2 = new Monster {
            Id = monster1.Id,
            Name = monster1.Name,
            Version = monster1.Version.Clone() as byte[],
        };

        // 1つ目のエンティティを更新 => 成功する
        using (var dbContext = new AppDbContext(tracking: true)) {
            monster1.Name = "スライムベス";
            dbContext.Set<Monster>().Attach(monster1).State = EntityState.Modified;

            dbContext.SaveChanges();

            // 実行されるSQL
            // WHERE句にVersionカラムが含まれている
            /*
           Executed DbCommand (0ms) [Parameters=[@p1='?', @p0='?' (Size = 4000), @p2='?' (Size = 8)], CommandType='Text', CommandTimeout='30']
           SET NOCOUNT ON;
           UPDATE [Monster] SET [Name] = @p0
           WHERE [Id] = @p1 AND [Version] = @p2;
           SELECT [Version]
           FROM [Monster]
           WHERE @@ROWCOUNT = 1 AND [Id] = @p1;
           */
        }

        // 2つ目のエンティティを更新 => 失敗する(上記SaveChangesでVersionカラムが更新されているため)
        using (var dbContext = new AppDbContext(tracking: true)) {
            monster2.Name = "バブルスライム";
            dbContext.Set<Monster>().Attach(monster2).State = EntityState.Modified;

            try {
                // SaveChangesを呼び出すと例外が発生する
                dbContext.SaveChanges();
            } catch (DbUpdateConcurrencyException exception) {
                Console.WriteLine(exception.Message);
                // 例外メッセージ
                /*
               Database operation expected to affect 1 row(s) but actually affected 0 row(s).
               Data may have been modified or deleted since entities were loaded.
               See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
               */
            }
        }
    }
}

まとめ

rowversionとTimestamp属性(またはFluent API)を使うと、

  • SaveChangesメソッドで実行されるUPDATE文のWHERE句にVersionカラムが含まれる
  • すでにVersionカラムが更新されていた場合、SaveChangesメソッドを呼び出すと更新に失敗してDbUpdateConcurrencyExceptionがスローされる

ということを確認できました。

コードとクエリ全体はこちらに。

EF Coreで楽観的同時実行制御 · GitHub

Entity Framework Core - Owned typesのサンプル

EF Coreの「Owned types」を試してみました。日本語訳はどうなるんでしょう。所有型?かな?

What is new in EF Core 2.0 - EF Core | Microsoft Docs

「Owned types」はEF6の複合型に似た機能のようで、テーブルにある複数カラムをプロパティクラスにマッピングするものです。

ということで早速。次のテーブルがあるとします。

-- テーブル作成
drop table if exists dbo.Character;
create table dbo.Character(
    Id int,
    Name nvarchar(4) not null,   -- 名前
    Level int not null,   -- レベル
    Hp int not null,  -- HP
    Mp int not null,  -- MP
    constraint PK_Character primary key(Id)
);

次のモデルにマッピングしてみましょう。HpとMpのカラムは再利用したいという体でStatusクラスとしています。

// ステータス
public class Status {
    public int Hp { get; set; }
    public int Mp { get; set; }
}

// キャラクター
public class Character {
    public int Id { get; set; }
    public string Name { get; set; }
    public int Level { get; set; }
    // HPとMPを持つステータス
    public Status Status { get; set; }
}

マッピングするにはDBコンテキストのOnModelCreatingメソッド内でOwnsOneメソッドを使ってStatusプロパティを指定します。

OwnsOneメソッドの2つ目の引数では入れ子になったStatusクラスのマッピングも指定できます。マッピングを指定しないとStatusクラスの各プロパティは「Status_Hp」「Status_Mp」といったカラム名マッピングされるので、下記コードでは実際のカラム名に合うように調整しました。

// DBコンテキストの一部
public class AppDbContext : DbContext {
    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        // Characterテーブルへのマッピング
        modelBuilder.Entity<Character>().ToTable(nameof(Character));

        // CharacterがStatusを所有する
        modelBuilder.Entity<Character>().OwnsOne(
            character => character.Status,
            statusBuilder => {
                // StatusをHpカラムとMpカラムへマッピング
                // (デフォルトだと「Status_Hp」「「Status_Mp」」といったカラムにマッピングされる)
                statusBuilder.Property(status => status.Hp).HasColumnName(nameof(Status.Hp));
                statusBuilder.Property(status => status.Mp).HasColumnName(nameof(Status.Mp));
            });
    }
}

これで準備が整ったので、うまくマッピングできているか確認してみましょう。

class Program {
    // 結果出力用
    private static void Dump(IEnumerable<Character> characters) {
        foreach (var character in characters) {
            Console.WriteLine($"{character.Name}");
            Console.WriteLine($"Lv {character.Level}");
            Console.WriteLine($"HP {character.Status.Hp}");
            Console.WriteLine($"MP {character.Status.Mp}");
            Console.WriteLine();
        }
    }

    static void Main(string[] args) {
        // データ投入
        using (var dbContext = new AppDbContext(tracking: true)) {
            dbContext.Set<Character>().AddRange(
                new Character {
                    Id = 1,
                    Name = "エイト",
                    Level = 10,
                    Status = new Status { Hp = 59, Mp = 32 },
                },
                new Character {
                    Id = 2,
                    Name = "ゼシカ",
                    Level = 10,
                    Status = new Status { Hp = 47, Mp = 28 },
                });

            dbContext.SaveChanges();
        }

        // 確認
        using (var dbContext = new AppDbContext()) {
            var characters = dbContext.Set<Character>().ToList();
            Dump(characters);
            /*
           エイト
           Lv 10
           HP 59
           MP 32
           ゼシカ
           Lv 10
           HP 47
           MP 28
           */
        }

        // データ更新
        using (var dbContext = new AppDbContext(tracking: true)) {
            // エイトを取得
            var character = dbContext.Set<Character>().Find(1);

            // エイトはレベルがあがった!
            character.Level += 1;
            character.Status.Hp += 12;
            character.Status.Mp += 2;

            dbContext.SaveChanges();
        }

        // 確認
        using (var dbContext = new AppDbContext()) {
            var characters = dbContext.Set<Character>().ToList();
            Dump(characters);
            /*
           エイト
           Lv 11
           HP 71
           MP 34
           ゼシカ
           Lv 10
           HP 47
           MP 28
           */
        }
    }
}

コードとクエリはこちら。

EF CoreのOwned typesのサンプル · GitHub