ASP.NET CoreでTypeScriptとwebpack

Visual Studio 2017でASP.NET Core + TypeScript + webpackを試せる環境を作るまでのメモです。

まずは試した環境から。

いつものようにASP.NET Coreの空のテンプレートから始めたいと思います。

プロジェクトを作成する

ASP.NET Core Web アプリケーションのプロジェクトを選択してプロジェクトを作ります。

Startupクラスを設定する

Razor Pagesと静的ファイルを扱うために、Startupクラスを次のように修正します。

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

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

        // MVC(Razor Pages)を使う
        app.UseMvc();

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

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

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

Pages/
   └─ Index.cshtml

Index.cshtmlではこれから作るJavaScriptを指定します。指定している~/js/bundle.jsを、このあとwebpackで出力するようにしていきたいと思います。

@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/bundle.js"></script>
</body>
</html>

まだJavaScriptは作っていませんが、これでデバッグ実行できるようになりました。実行してあいさつをすませておきましょう。

ちなみにここまでは以前の記事とほぼ同じです・・・。

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

プロジェクト直下にScriptsフォルダを作って、その中にapp.ts、lib.tsを作ります。

Scripts/
   ├─ app.ts
   └─ lib.ts

app.tsでは、lib.tsでエクスポートしたクラスをインポートするようにしています。

// lib.ts
// あいさつ文を作るクラスをエクスポート
export class Greeter {
    public static greet(message): string {
        return `Hello, ${message}!`;
    }
}

// app.ts
// lib.tsのGreeterクラスをインポート
import { Greeter } from "lib";

// あいさつする
alert(Greeter.greet("world"));

モジュールというやつですね。TypeScriptでは問題ないのですが、JavaScriptでモジュールのインポートをどうすればいいのかわからず苦労しました。今回はwebpackを使いましたが、他にもいい方法があるような気も。

TypeScriptのモジュールの説明はこちら。

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

TypeScript JSON 構成ファイル(tsconfig.json)を作ります。

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

npmでwebpackをインストールする

npmでwebpackとtypescript、ts-loaderをインストールします。typescriptとts-loaderは、webpackがTypeScriptを処理するために必要なものです。具体的にはnpm 構成ファイル(package.json)を次のようにします。

{
    "version": "1.0.0",
    "name": "asp.net",
    "private": true,
    "devDependencies": {
        "webpack": "3.10.0",
        "typescript": "2.6.2",
        "ts-loader": "3.2.0"
    }
}

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

プロジェクト直下にwebpack.config.jsを作ります。Scriptsフォルダにあるapp.tsとインポートしているib.tsをまとめて、wwwroot/js/bundle.jsを作成するように指定します。

const path = require("path");

const config = {
    devtool: "inline-source-map",
    // webpackがバンドルの構築を開始するエントリポイント
    entry: path.resolve(__dirname, "scripts/app.ts"),
    output: {
        // 出力するファイル名
        filename: "bundle.js",
        // 出力フォルダ
        path: path.resolve(__dirname, "wwwroot/js")
    },
    module: {
        rules: [
            // TypeScriptを処理するローダー
            { test: /\.ts$/, loader: "ts-loader" }
        ]
    },
    resolve: {
        extensions: [".ts", ".js"],
        // モジュールを探すフォルダ(node_modulesとscriptsフォルダを対象にする)
        modules: [
            "node_modules",
            path.resolve(__dirname, "scripts")
        ]
    }
};

module.exports = config;

webpackやTypeScriptのドキュメントを参考にしながら手探りで書きました。

WebPack Task Runnerをインストールする

Visual StudioにWebPack Task Runnerをインストールしておきます。インストールすると、タスク ランナー エクスプローラでwebpackを実行できるようになります。

webpackでバンドルする

タスク ランナー エクスプローラにあるRun - DevelopmentWatch Developmentをダブルクリックしてwebpackを実行すると、wwwroot/jsフォルダにbundle.jsファイルが作成されます。

デバッグ実行してアラートであいさつができれば今回の目標クリアです。

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