アドレス帳アプリケーションの作成

前回、とりあえず ASP.NET Core を Linux上で動かせたので、 ASP.NET Core MVC と EntityFramework Core を使って簡単な Web+DBアプリケーションを作成してみます。 あまり Linux 固有の話はありません。

アドレス帳プロジェクトを作る

generator-aspnet には、 ASP.NET Core Mvc 設定済のプロジェクトを生成するジェネレータも用意されていますが、学習のため Empty Web Application から初めていきます。

$ yo aspnet
? What type of application do you want to create? Empty Web Application
? What's the name of your ASP.NET application? Addressbook
$ cd Addressbook
$ dotnet restore

プロジェクトを生成したら、そのディレクトリに移動して dotnet restore まで実行しておきます。

MVCを使う準備

ASP.NET Core MVC を使うには、まず project.json にライブラリを追加します。

dependencies: {
    ...
    "Microsoft.AspNetCore.Mvc": "1.0.0"
}

追加したら、 dotnet restore でライブラリをインストールします。

$ dotnet restore

VS Code を利用している場合は、 dotnet: Restore Packages を実行すればよいでしょう。

WebアプリケーションでMVCを有効にする

ライブラリがインストールできたら、アプリケーションの Startup クラス内で ASP.NET Core MVC を有効にします。

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

ConfigureServices メソッド内で、 servicesAddMvc メソッドを実行します。 AddMvc メソッドは IserviceCollection への拡張メソッドで、 Microsoft.AspNetCore.Mvc をインストールすると使えるようになります。

さらにルーティングの設定も行います。 今回はとりあえずデフォルトのルーティングを利用します。

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseMvcWithDefaultRoute();
        }

Emptty Web Application では Run メソッドになっていた部分を UseMvcWithDefaultRoute の呼び出しに変更します。 これは {controller}/{action}/{id?} というルーティングを設定するメソッドです。

Controllerを追加する

ルーティングができたのでその先で実際に処理を行うコントローラーを追加します。 コントローラークラスの追加も generator-aspnet のサブジェネレータが用意されてるので、それを利用することにします。

$ mkdir Controllers
$ mkdir Views
$ yo aspnet:MvcController HomeController
$ yo aspnet:MvcView Home/Index.cs

コントローラーを追加すると、空の Index メソッドだけ用意されてるので、対応するビューのファイルも生成しています。 ビューの中身はなにもないので、単にエラーにならないだけの真っ白なページを表示できます。

Modelを定義する

アドレス帳の中身を作成していきます。 アドレス帳にのせる人物を Person クラスで実装しましょう。 Models ディレクトリ以下に aspnet:Class ジェネレータを使って単純なクラスを生成します。

$ mkdir Models
$ cd Models
$ yo aspnet:Class Person

Person クラスにいくつかのプロパティを実装します。

namespace Addressbook.Models
{
    public class Person
    {
        public int Id
        {
            get;
            set;
        }
        
        public string FirstName
        {
            get;
            set;
        }

        public string LastName
        {
            get;
            set;
        }

        public string FullName
        {
            get {return FirstName + " " + LastName;}
        }

        public string Email
        {
            get;
            set;
        }
    }
}

FullName プロパティは FirstName, LastName から算出するプロパティです。 またこのあとデータベースに保存するので、 Id プロパティも実装しておきます。

Modelにアトリビュートをつける

モデルクラスにアトリビュートをつけてプロパティの扱いを指定します。 FullName プロパティは算出されるものなので、データベースに保存しないように NotMappedAttribute を指定します。 Email プロパティは EmailAddressAttribute をつけて単なる文字列ではなくメールアドレスを持つプロパティであることを明示しておきます。

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

...
        [Required]
        public string FirstName
        {
            get;
            set;
        }

        [Required]
        public string LastName
        {
            get;
            set;
        }

        [NotMapped]
        public string FullName
        {
            get {return FirstName + " " + LastName;}
        }

        [Required,EmailAddress]
        public string Email
        {
            get;
            set;
        }

...

これらのアトリビュートは、 System.ComponentModel.Annotations ライブラリの System.ComponentModel.DataAnnotations 名前空間や System.ComponentModel.DataAnnotations.Schema 名前空間に含まれています。

View

モデルオブジェクトを作成してビューに表示してみます。 とりあえずデータベースのことは忘れて、アクションメソッドの中でインスタンスを生成してビューに渡すようにします。

HomeControllerIndex メソッドの中で Person の配列を作成してみます。

         public IActionResult Index()
         {
            var people = Enumerable.Range(0, 10)
                .Select(i => new Models.Person() {
                    FirstName = string.Format("Person{0}", i),
                    LastName = "Last",
                    Email = string.Format("person{0}@example.com", i)
                })
                .ToArray();
            return View(people);
         }

Linq を使って10個の Person 要素を持つ配列 people を作成しています。 この people をそのまま View メソッドの引数に渡すと、ビューのテンプレート内で Model として参照できます。

配列を渡しているので Home/Index.cshtml の中ではループして、それぞれの要素を表示します。

<table>
    <tr>
        <th>FullName</th>
        <th>Email</th>
    </tr>
@foreach (var person in Model)
{
    <tr>
        <td>@person.FullName</td>
        <td>@person.Email</td>
    </tr>
}
+</table>

@foreach を使って配列をループ処理します。 また、変数の参照は @ の後に変数名を書くとその値を評価した結果が表示されます。

モデルとフォーム

新規作成や編集フォームのビューも作ってみましょう。 それぞれ、 New, Edit アクションとして実装してみます。

HomeControllerNew メソッドを追加します。

        public IActionResult New()
        {
            return View();
        }

対応するビューを Home/New.cshtml に作成します。

$ yo aspnet:MvcView Home/New.cshtml

New.cshtml にフォームを作成します。

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model Addressbook.Models.Person
@{
    // ViewBag.Title = "New Page";
}
<form asp-action="create" method="post">
    <div>
        <label asp-for="FirstName"></label>
        <input asp-for="FirstName"/>
    </div>
    <div>
        <label asp-for="LastName"></label>
        <input asp-for="LastName"/>
    </div>
    <div>
        <label asp-for="Email"></label>
        <input asp-for="Email"/>
    </div>
</form>

フォームのフィールドで asp-for という属性を指定しています。 これはタグヘルパーという機能です。 使いたいタグヘルパーを @AddTagHelper で指定します。 また、モデルのクラスを @model で指定します。

フォームの中の inputlabel には asp-for タグヘルパーでプロパティを指定します。

同様に編集用のフォームを Edit アクションに作成します。

        public IActionResult Edit(int id)
        {
            var person = new Models.Person() {
                FirstName = "Edit First Name",
                LastName = "Edit Last Name",
                Id = id,
                Email = "edit@example.com",
            };
            return View(person);
        }

Edit メソッドでは、編集対象のオブジェクトをビューにモデルとして渡すようにしています。 ここでは、仮の内容で Person クラスのインスタンスを作成しています。

TagHelperを使ってリンクを作る

それぞれのフォームへのリンクを追加します。

<a asp-action="new">New</a>
<table>
    <tr>
        <th>FullName</th>
        <th>Email</th>
    </tr>
@foreach (var person in Model)
{
    <tr>
        <td>
            <a asp-action="edit" asp-route-id="@person.Id">@person.FullName</a>
        </td>
        <td>@person.Email</td>
    </tr>
}
</table>

フォームへのリンクも TagHelpers を利用します。 asp-action でアクション名を指定し、 asp-route-id でルートURL内の id パラメータに値を渡します。 現在のコントローラーと別のコントローラーへのリンクを作成する場合は asp-controller でコントローラー名を指定しますが、今回は同じ HomeController のアクションを利用するため指定しません。

デフォルトのルート設定は {controller}/{action}/{id?} となっているため、 id1 の場合では home/edit/1 というURLへのリンクが生成されます。

EntityFramework Core とツールをインストールする

モデルを永続化するために EntityFramework Core を利用します。

EntityFramework Core を依存ライブラリに追加します。

dependencies: {
    ...
    "Microsoft.EntityFrameworkCore": "1.0.0",
    "Microsoft.EntityFrameworkCore.Sqlite": "1.0.0"
}

今回はデータベースに Sqlite を使うことにします。

また、スキーママイグレーションをするために EntityFramework Core Tools をインストールします。 EntityFramework Core Toolsproject.jsontools セクションに追加します。

  "tools": {
    ...
    "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
  },

EntityFramework Core Tools に必要な EntityFramework Core Design を依存ライブラリに追加します。

dependencies: {
    ...
    "Microsoft.EntityFrameworkCore.Design": "1.0.0-preview2-final"
}

project.json にライブラリを追加したら、 dotnet restore でライブラリをインストールします。

EntityFramework Core でモデルを永続化する

DbContext を継承して ApplicationDbContext を作成します。

using Microsoft.EntityFrameworkCore;

namespace Addressbook.Data
{
    public class ApplicationDbContext: DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            :base(options)
        {

        }

    }
}

ApplicationDbContext のコンストラクタは DbContextOptions を受け取るようにします。 この値はスーパークラスの DbContext のコンストラクタにそのまま渡すようにします。

ApplicationDbContext クラスに、永続化したいクラスの DbSet をプロパティで定義します。

        public DbSet<Models.Person> People
        {
            get;
            set;
        }

データベース接続設定

appsettings.json を作成して、データベース接続を設定します。

{
    "ConnectionStrings": {
        "DefaultConnection": "DataSource = addressbook.sqlite"
    }
}

Startup クラスのコンストラクタで、 appsettings.json などから設定を読み込む処理を追加します。

using Microsoft.Extensions.Configuration;
using System.IO;

...

    public class Startup
    {
        IConfigurationRoot Configuration
        {
            get;
            set;
        }
        public Startup()
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddEnvironmentVariables();
            Configuration = builder.Build();
        }
...

ApplicationDbContextConfigureServices 中で services に追加します。

            services.AddDbContext<Data.ApplicationDbContext>(options => {
                options.UseSqlite(Configuration.GetConnectionString("DefaultConnection"));
            });
        }

ここで設定した optionsApplicationDbContext クラスのコンストラクタに渡されます。

Migrationする

ApplicationDbContextPerson 用の DbSet プロパティを追加したので、 EntityFramework Core Tools を使ってテーブルを作成します。

EntityFramework Core Tools の機能は dotnet コマンドの ef サブコマンドから利用できます。

試しに、 ApplicationDbContext がアプリケーション内のDbContextとして認識されているのか確認してみましょう。

$ dotnet ef dbcontext list
Project Addressbook (.NETCoreApp,Version=v1.0) was previously compiled. Skipping compilation.
Addressbook.Data.ApplicationDbContext

それではデータベースにスキーママイグレーションを実行してテーブルを作成してみます。

$ dotnet ef migrations add FirstModel
$ dotnet ef database update

実行すると、 bin/Debug/netcoreapp1.0addressbook.sqlite ファイルが作成されます。

sqlite3コマンドでデータベースファイルの中身を確認してみましょう。

$ sqlite3 bin/Debug/netcoreapp1.0/addressbook.sqlite 
sqlite> .schema
CREATE TABLE "__EFMigrationsHistory" (
    "MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
    "ProductVersion" TEXT NOT NULL
);
CREATE TABLE "People" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_People" PRIMARY KEY AUTOINCREMENT,
    "Email" TEXT NOT NULL,
    "FirstName" TEXT NOT NULL,
    "LastName" TEXT NOT NULL
);

Person クラスのテーブルとして People テーブルが作成されています。 また、マイグレーションのバージョンを管理するためのテーブル __EFMigrationsHistory テーブルも作成されます。

ControllerにDbContextをインジェクションする

ApplicationDbContext をコントローラーから扱うには、DependencyInjectionに登録されたオブジェクトをコンストラクタで受け取るようにします。 単にコントローラークラスのメソッド内で ApplicationDbContext クラスのインスタンスを作成してもデータベース接続が設定されていない状態のものになってしまいます。

フォーム処理

コントローラーでフォームの値を受け取って処理するようにしていきます。

New アクションのフォームから値を受け取り、アドレス帳に新規追加する Create アクションを HomeController のメソッドとして実装してみます。

フォームの内容はメソッドの引数として受け取ります。 引数に Bind アトリビュートを追加してどの値を受け取るのか指定します。 例えば、 New アクションから受け取るフォームの内容は以下のようにして Person クラスのインスタンスに割り当てます。

        [HttpPost]
        public IActionResult Create([Bind("FirstName,LastName,Email")]Models.Person person)
        {
            ...
        }

POST メソッド以外でフォームの値を受け取らないように HttpPost アトリビュートをメソッド自体に設定しています。 引数の personPerson クラスのインスタンスです。 Bind アトリビュートを使って FirstName, LastName, Email の値を person の各プロパティに割り当てています。

また、 Person クラスのプロパティにはそれぞれ Required アトリビュートや EmailAddress アトリビュートを追加しています。 これらのプロパティにしたがって、フォームの入力値が正しい形式になっているのか確認するには ModelStateIsValid プロパティを利用します。

        [HttpPost]
        public IActionResult Create([Bind("FirstName,LastName,Email")]Models.Person person)
        {
            if (!ModelState.IsValid) {
                return View("New", person);
            }
        }

IsValidfalse となる場合は、再びフォームを表示するため New ビューを呼び出して処理を中断するようにします。

モデルを永続化する処理

受け取った person オブジェクトをデータベースに保存するには DbContextAdd メソッドで追加した後に SaveChanges メソッドでデータベースに反映します。

        [HttpPost]
        public IActionResult Create([Bind("FirstName,LastName,Email")]Models.Person person)
        {
            if (!ModelState.IsValid) {
                return View("New", person);
            }
            DbContext.Add(person);
            DbContext.SaveChanges();
            return RedirectToAction("Index");
        }

IsValid の確認後に DbContext.AddDbContext.SaveChanges を呼び出します。 保存処理が終わったら、 RedirectToAction メソッドで Index アクションにリダイレクトして一覧を表示させます。

残りのCRUDアクションを実装する

新規追加のアクションができあがったので残りのアクションも EntityFramework Core を使った処理で実装していきます。

一覧表示をする IndexDbContextPeople プロパティを使って、保存されている Person オブジェクトの配列を取得するように変更します。

using System.Linq;

...

        [HttpGet]
        public IActionResult Index()
        {
            var people = DbContext.People.ToArray();
            return View(people);
        }

PersonIEnumerable を実装しているため、 System.Linq 名前空間を追加しておくと ToArray メソッドで配列を取得できるようになります。

Update アクションは Create と同様に Person クラスのオブジェクトでフォームの値を受け取るようにします。 Create アクションとは異なり、 URLパターンの中に更新対象を示す id パラメータがあります。 この値も person のプロパティとして受け取るために Bind アトリビュートで Id を追加します。


        [HttpPost]
        public IActionResult Update(int id, [Bind("Id,FirstName,LastName,Email")]Models.Person data)
        {
            if (!ModelState.IsValid)
            {
                return View("Edit", data);
            }
            var person = DbContext.People.FirstOrDefault(p => p.Id == id);
            if (person == null)
            {
                return NotFound();
            }
            person.FirstName = data.FirstName;
            person.LastName = data.LastName;
            person.Email = data.Email;
            DbContext.SaveChanges();
            return RedirectToAction("Index");            
        }

Update 内では更新対象を FirstOrDefault メソッドで取得します。 存在しない場合は NotFound メソッドでステータスコードが 404 のレスポンスを返して処理を終了します。

取得したオブジェクトのプロパティに、フォームの値を代入したら、 DbContextSaveChanges メソッドでデータベースに反映します。 DbContext から取得したオブジェクトは DbContext が状態を追跡しているため、 Add メソッドを呼び出す必要はありません。

処理が完了したら Create アクションと同様に Index アクションにリダイレクトして一覧を表示します。

最後に Delete アクションです。

        [HttpPost]
        public IActionResult Delete(int id)
        {
            var person = DbContext.People.FirstOrDefault(p => p.Id == id);
            if (person == null)
            {
                return NotFound();
            }
            DbContext.Remove(person);
            DbContext.SaveChanges();
            return RedirectToAction("Index");                        
        }

モデルをデータベースから削除するには DbContextRemove メソッドを利用します。 まずは id で対象のモデルを取得します。 取得したモデルを引数に Remove メソッドを呼び出します。 SaveChanges メソッドを呼ぶと、実際にデータベースから削除されます。 処理が完了したら Create アクションと同様に Index アクションにリダイレクトして一覧を表示します。

これで、 HomeController に Create, Retivie, Update Delete の CRUDに対応したアクションがすべてそろいました。

Viewをまとめる

これまで View には動作に必要な最低限の内容だけを書いてあります。 また、TagHelperの利用など、アプリケーション内で共通な設定がそれぞれのViewに記述されています。

すべてのViewで共通するレイアウトを作成してみましょう。

<!doctype html>
<html>
    <head>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
    </head>
    <body>
        <nav class="navbar navbar-default">
            <div class="container-fluid">
                <div class="navbar-header">
                    <a href="/" class="navbar-brand">Addressbook</a>
                </div>
            </div>
        </nav>
        <div class="container-fluid">
            @RenderBody()
        </div>
    </body>
</html>

twitter-bootstrap のスタイルを適用するレイアウトです。

レイアウトの中では RenderBody() メソッドを呼び出して、実際のViewの内容を表示します。

レイアウトは _Layout.cshtml ファイルに作成します。 コントローラーやアクション専用のViewは Views/[コントローラー]/[アクション].cshtml に配置されています。 _Layout.cshtmlViews/Shared 以下の汎用のディレクトリに配置します。

実際にレイアウトを適用するには、 それぞれの View の中で Layout 変数に利用するレイアウトのビュー名を設定します。

@{
    Layout = "_Layout"
}

この処理をすべてのViewの中で書かずとも _ViewStart.cshtml に書いておくと、自動ですべてのViewで実行されるようになります。 上記のレイアウト指定の内容で Views/_ViewStart.cshtml を作成しましょう。

また、タグヘルパーなど複数の View で利用するライブラリがあります。 これらの import_ViewStart.cshtml に書いても効果はありません。 _ViewImports.cshtml に共通に利用するライブラリなどの設定をまとめましょう。

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

_ViewImports.chtml を作成したら、それぞれの View に書いていた addTagHelper の設定を削除しておきましょう。

まとめ