ASP.NET Core で Riot.js その２


前回は Riot.js を使って Hello World してみました。
今回は Web API からデータを取得して一覧を表示するところをやってみます。

* [ASP.NET Core で Riot.js その１](http://qiita.com/t-koyama/items/a2b1b6bfbf401a8dd868)
* ASP.NET Core で Riot.js その２

# 開発環境
Windows 10 Pro
Visual Studio Code

# Server-side
Server-side に Web API を実装します。

今回作成する HTTP メソッドは以下となります。

| API                                          | 概要                                              | Request body | Response body |
| :------------------------------ | :----------------------------------| :-------------- | :--------------- |
| GET /api/v1/person               | Person の一覧を取得する。         | なし                 | Personの一覧  | 
| GET /api/v1/person/{id}        | id に紐付く Person を取得する。| なし                | Person              | 
| POST /api/v1/person             | Person を作成する。                   | Person            | Person              |
| PUT /api/v1/person/{id}        | id に紐付く Person を更新する。| Person            | なし                  | 
| DELETE /api/v1/person/{id} | id に紐付く Person を削除する。 | なし                | なし                  | 

今回は一覧を表示するだけなので一番上の GET メソッドだけあればよいのですが、 CRUD 操作に必要なメソッドを一通り作成しておきます。まずは HTTPメソッドでやり取りするために必要な Model となるクラスを作成していきます。

## Model
Model を作成していきます。今回は [Building Your First Web API with ASP.NET Core MVC and Visual Studio](https://docs.asp.net/en/latest/tutorials/first-web-api.html) を参考に Repository  パターンを使ってやってみたいと思います。

ここでは以下のことをおこないます。

* Model クラスの作成
* Repository クラスの作成
* 作成したRepository クラスの登録 (DI)

まずは Models フォルダに必要なファイルを作成します。Visual Studio Code のターミナルから以下のコマンドを入力してください。

```bash
mkdir Models & cd Models
yo aspnet:Class Person
yo aspnet:Interface IPersonRepository
yo aspnet:Class PersonRepository
cd ..
```

### Model クラスの作成
Models フォルダに作成した Person.cs のコードを以下のように変更します。

```csharp:Person.cs
namespace AspRiotApp.Models
{
    public class Person
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }
    }
}
```

### Repository クラスの作成
Repository を作成します。

まずは `IPersonRepository` に Repository クラスで実装するデータアクセスロジックを定義します。Models フォルダに作成した IPersonRepository.cs のコードを以下のように変更します。

```csharp:IPersonRepository.cs
using System.Collections.Generic;

namespace AspRiotApp.Models
{
    public interface IPersonRepository
    {
        void Add(Person person);
        IEnumerable<Person> GetAll();
        Person Find(string id);
        void Remove(string id);
        void Update(Person person);
    }
}
```

続いて作成したインターフェイスに定義されているデータアクセスロジックを `PersonRepository` に実装します。 Models フォルダに作成した PersonRepository.cs のコードを以下のように変更します。

```csharp:PersonRepository.cs
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;

namespace AspRiotApp.Models
{
    public class PersonRepository : IPersonRepository
    {
         private static ConcurrentDictionary<string, Person> _people = 
              new ConcurrentDictionary<string, Person>();

        public PersonRepository()
        {
            Add(new Person { Name = "Eiji", Age = 35 });
            Add(new Person { Name = "Akemi", Age = 35 });
            Add(new Person { Name = "Satoru", Age = 7 });
            Add(new Person { Name = "Madoka", Age = 7 });
        }

        public IEnumerable<Person> GetAll() => _people.Values;

        public void Add(Person person)
        {
            person.Id = Guid.NewGuid().ToString();
            _people[person.Id] = person;
        }

        public Person Find(string id)
        {
            var person = default(Person);
            _people.TryGetValue(id, out person);
            return person;
        }

        public void Remove(string id)
        {
            var person = default(Person);
            _people.TryRemove(id, out person);
        }

        public void Update(Person person) => _people[person.Id] = person;
    }
}
```

 実際は Database からデータを取得してマッピングしたりなどすると思いますが、今回はモックとして private な 静的メンバとして `_people` コレクションを用意して、適当なデータを突っ込んでいます。
 
```csharp
private static ConcurrentDictionary<string, Person> _people = new ConcurrentDictionary<string, Person>();
```
 
System.Collections.Concurrent.ConcurrentDictionary は値のペアのコレクションです。値のペアからなるコレクションということで System.Collections.Generic.Dictionary と似ていますが ConcurrentDictionary は同時に複数のスレッドからアクセスすることができる（スレッド セーフ）という特徴を持っています。
 
### 作成したRepository クラスの登録
作成したRepository クラスをサービスとして登録します。ASP.NET Core プロジェクトで新しくサービスを登録する場合は Startup.cs に定義されている `ConfigureServices()` メソッドに処理を追加していきます。 

```csharp:Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc();
    // リポジトリを登録
    services.AddSingleton<IPersonRepository, PersonRepository>();
}
```

`AddSingleton()` メソッドを呼んで作成した Repository クラスをひとつだけインスタンス化するよう DI コンテナに追加しています。ここで登録したサービスは Controller などのコンストラクタで受取ることで利用できます。

これで Model の作成は完了です。つぎは Controller を作成してWeb API として今回提供する HTTP メソッドを実装します。

## Controller
ターミナルから以下のコマンドを入力して Controllers フォルダに PersonController.cs を作成します。

```bash
cd Controllers
yo aspnet:WebApiController PersonController
cd ..
```

作成したらコードを以下のように変更します。

```csharp:PersonController.cs
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using AspRiotApp.Models;

namespace AspRiotApp.Controllers
{
    [Route("api/v1/[controller]")]
    public class PersonController : Controller
    {
        private IPersonRepository People { get; set; }

        public PersonController(IPersonRepository people)
        {
            People = people;
        }

        // GET api/v1/person
        [HttpGet]
        public IEnumerable<Person> Get() => People.GetAll();

        // GET api/v1/person/{id}
        [HttpGet("{id}", Name="GetPerson")]
        public IActionResult Get(string id)
        {
            var person = People.Find(id);

            if(person == null)
                return NotFound();

            return new ObjectResult(person);
        }

        // POST api/v1/person/
        [HttpPost]
        public IActionResult Post([FromBody]Person person)
        {
            if (person == null)
                return BadRequest();

            People.Add(person);

            return new CreatedResult("GetPerson", person);
        }

        // PUT api/v1/person/{id}
        [HttpPut("{id}")]
        public IActionResult Put(string id, [FromBody]Person person)
        {
            if (person == null || person.Id != id)
                return BadRequest();

            if (People.Find(id) == null)
                return NotFound();

            People.Update(person);

            return new NoContentResult();
        }

        // DELETE api/v1/person/{id}
        [HttpDelete("{id}")]
        public void Delete(string id) => People.Remove(id);
    }
}
```

### Injection

コントラクタで先ほど登録した Repository サービスをインジェクションして受取ります。

```csharp
private IPersonRepository People { get; set; }

public PersonController(IPersonRepository people)
{
    People = people;
}
```

### Attribute

クラスに Attribute を付加してルーティングします。(api/v1/person)

```csharp
[Route("api/v1/[controller]")]
public class PersonController : Controller
{
    ...
}
```

GET/POST/PUT/DELETE など Web API として今回提供する HTTP メソッドを定義するときは対応する Attribute をメソッドに付加します。

```csharp
// GET api/v1/person
[HttpGet]
public IEnumerable<Person> Get() => People.GetAll();
```

付加できる Attribute は以下となります

* HttpDelete
* HttpGet
* HttpHead
* HttpOptions
* HttpPatch
* HttpPost
* HttpPut

URLにパラメータを指定する場合は Attribute の引数に指定します。

```csharp
// DELETE api/v1/person/{id}
[HttpDelete("{id}")]
public void Delete(string id) => People.Remove(id);
```

アクション名を指定する場合は Attribute の `Name` に指定します。

```csharp
// GET api/v1/person/{id}
[HttpGet("{id}", Name="GetPerson")]
public IActionResult Get(string id)
{
    ...
}
```

### 戻り値

メソッドの戻り値には `IEnumerable<Person>` など任意の型を返すことができます。ステータスコードは 200 (OK) となります。 

```csharp
[HttpGet]
public IEnumerable<Person> Get() => People.GetAll();
```

また、 戻り値がないメソッドも扱えます。 この場合のステータスコードは  204 (No Content) となります。

```csharp
[HttpDelete("{id}")]
public void Delete(string id) => People.Remove(id);
```

IActionResult は処理結果に応じてステータスコードを返す必要がある場合に使います。例として `Put()` メソッドでは条件によって `BadRequest()` や `NotFound()` などそれぞれのステータスコードに対応するメソッドを呼び出しています。

```csharp
 [HttpPut("{id}")]
 public IActionResult Put(string id, [FromBody]Person person)
 {
     if (person == null || person.Id != id)
         return BadRequest();

     if (People.Find(id) == null)
         return NotFound();

     People.Update(person);

     return new NoContentResult();
 }
```

`Put()` メソッドの条件とステータスコードは以下となります。

| ステータスコード     | 条件                                                                                                                | 
| :-------------------- | :----------------------------------------------------------------------------- |
| 400 (Bad Request)  |  引数で与えられてる値に不正がある場合                                                        |
| 404 (Not Found)      |  引数の `id` からデータを検索して一致するデータが見つからなかった場合 |
| 204 (No Content)     |  更新に成功した場合                                                                                      |

---
最後にプロジェクト作成時にデフォルトで作成されている ValuesController.cs は使わないので削除すれば Server-side の実装は終わりです。続いて Client-side の実装をおこないます。

# Client-side
Client-side では index.html と app.tag の修正をおこないます。

## html
index.html は以下となります。

```html:index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <title>ASP.NET Core で Riot.js</title>
    <!-- jQuery -->
    <script src="https://code.jquery.com/jquery-3.0.0.min.js"></script>
    <!-- app.tag の読込み -->
    <script src="app.tag" type="riot/tag"></script>
    <!-- riot+compiler.min.js の読込み -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/riot/2.5.0/riot+compiler.min.js"></script>
</head>

<body>
    <!-- app タグ -->
    <app></app>

    <!-- 作成したタグをマウントする -->
    <script>
        riot.mount('*');
    </script>
</body>

</html>
```

Web API とのやり取りを jQuery を使って Ajax 通信でおこないたいので jQuery を読込むようにします。

```html
<script src="https://code.jquery.com/jquery-3.0.0.min.js"></script>
```

あと前回 `app` タグの `title` 属性に値を入れていましたが今回は使わないので取りました。

```html
<app></app>
```

## Tag
app.tag に Ajax 通信で GET してレスポンス結果を一覧で表示する処理を実装します。

```html:app.tag
<app>
    <h1>People</h1>
    <ul>
        <li each="{ person in people }" title="{ person.name }">
            <a href="#item/{person.id}">{ person.name } ({ person.age })</a>
        </li>
    </ul>

    <style scoped>
        :scope { display: block }
        ul {
            list-style: none;
            padding: 0;
            margin: 0;
        }
        li {
            margin: 0.8em 0;
        }
        a {
            background: #eee;
            border-radius:0.3em;
            color: inherit;
            display: block;
            padding: 1.2em;
            text-decoration: none;
        }
        a:hover {
            background: #7ACBE2
        }
    </style>

    // 一覧
    this.people = [];
 
    var self = this;

    // GET
    $.ajax({
        url  : '/api/v1/person',
        type : 'GET'
    })
    .then(
        function (result) {
            // 取得結果を一覧に設定
            self.people = result;
            // 更新
            self.update();
        },
        function () {
            console.log('失敗');
        }
    );
</app>
```

### Ajax

`$.ajax()` で GET リクエストして `people` にレスポンス結果を設定しています。

```javascript
// GET
$.ajax({
    url  : '/api/v1/person',
    type : 'GET'
})
.then(
    function (result) {
        // 取得結果を一覧に設定
        self.people = result;
        // 更新
        self.update();
    },
    function () {
        console.log('失敗');
    }
);
```

今回は `$.ajax()` のオプションに `url` と `type` だけ指定してますが、本番では `timeout` を指定したりする必要があると思います。また、Web API の内容を変更した場合など再度 GETリクエストしたときに IE などで取得結果がうまく反映されないことがあると思います。そのときは `cache : false` を指定してあげます。

### each

`ul` タグ内では取得した結果を一覧で表示するために `each="{ person in people }"` で `people` コレクションをループして要素を `person` として取り出して内容を `li` タグにバインドしています。

```html
<li each="{ person in people }" title="{ person.name }">
    <a href="#item/{person.id}">{ person.name } ({ person.age })</a>
</li>
```

---

これで Client-side の実装は終わりです。`dotnet run` してブラウザから localhost に接続すると一覧が表示されます。

# 次回
次回は Server-side に実装した HTTP メソッドを利用して、Client-side でデータの追加や変更などができるようにします。