MSBOOKS

プログラミングとか深層学習とか日常の出来事とか

【勉強メモ】ASP.NET CoreでWeb APIを作成するチュートリアルを試してみた その2

はじめに

.net core勉強のその2です。前回の続きのGET メソッドの確認から勉強していきます。
docs.microsoft.com

GET メソッドの確認

このAPIの動作を確認する。

  • GET /api/TodoItems
  • GET /api/TodoItems/{id}

postmanの実際の記述は以下のようになるっぽい。

これによってGetTodoItemsが呼び出される。
{id}は取り出す番号っぽい、idは入れた順番に登録されているようだ。
試しにwalk catをpostしてみたらidが2で登録されていた。
f:id:msteacher:20210805214924p:plain

逆に{id}を指定しない場合は全部取得できるようだ。
f:id:msteacher:20210805215039p:plain

なんとなくのHTTPメソッドのGETの挙動を理解できた気がする。

ルーティングとURLパス

・TodoItemsController.cs

[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
{

[controller] をコントローラーの名前に置き換えられるそうで、 今回はTodoItems Controllerなので、コントローラー名は"TodoItems"(Controllerが除かれる)らしい。
つまりapi/TodoItemsになって、さっきのGETの文と同じになるんですね。
さっきのGetTodoItemが呼び出されると、そのidにURLの"{id}"の値が指定されるっぽい。結構便利。

戻り値

戻り値の型は、ActionResult型だそうで、JSON形式で返ってくる。↓みたいな感じなやつですね。うまく作ればJavascirptでいい感じに処理できそう(語彙力)。

    {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
    }

"この戻り値の型の応答コードは 200 で、ハンドルされない例外がないものと想定します。ハンドルされない例外は 5xx エラーに変換されます。"だそうで、たしかにpostmanの結果を見ると、"200 OK"と書いてあって、存在しないidを指定すると確かに"404 Not Found"になった。
f:id:msteacher:20210805220350p:plain

PutTodoItemメソッド

  • PUT /api/TodoItems/{id}

の話で、

のようにHTTP PUTを指定するっぽい。
"応答は 204 (No Content) となります。 HTTP 仕様に従って、PUT 要求では、変更だけでなく、更新されたエンティティ全体を送信するようクライアントに求めます"だそうで、JSONのid1つの全体を送る必要があるらしい。

"Id = 1 の To Do アイテムを更新し、その名前を "feed fish" に設定します。"といわれたのでそのまま実行してみた。
f:id:msteacher:20210805220839p:plain

となって、GETを呼ぶと、
f:id:msteacher:20210805220939p:plain

となり、ちゃんと内容が変更されていることがわかった。

DeleteTodoItem メソッド

ここまでくると次がなんなのかなんとなくわかってきた。

  • DELETE /api/TodoItems/{id}

idで指定したものを削除するんでしょう。
DeleteTodoItemメソッドを使用する場合はメソッドをDELETEにしてURIを以下のように指定する。

今更だがURLとURIって何が違うんやって思い調べたら、"「URI」のほうが広い概念で、「URL」はURIの部分集合です。"だそうです。URIは名前または場所を識別する書き方のルールの総称で、URLは場所を示す書き方のルールで、ページや画像などを取得したりするための主要な場所とアクセス方法を指定するものだそうだ。
DELETEで1を指定して削除してGETしてみたところ、以下のような結果となった。
f:id:msteacher:20210805222206p:plain

2番目にいれたwalk catはidは2のままになっていて、とくに前につめられたりはしないようだ。でもデータベースの管理としては当たり前か…。

過剰な投稿を防止する

セキュリティのために入力されるデータや返されるデータを制限するのが通常環境のアプリだそう。"モデルのサブセットは、通常、データ転送オブジェクト (DTO)、入力モデル、またはビュー モデルと呼ばれます。 この記事では DTO を使用しています。"だそうで、DTOを利用すると、過剰な投稿を防止したり、クライアントに表示させたくないものを非表示にしたりできるみたい。そのためにまずDTOを実現するためにシークレットフィールドを作るそう。
・TodoItem.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TodoApi.Models
{
    public class TodoItem
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }
        public string Secret { get; set; }
    }
}

管理アプリの場合は公開することを選択でき、シークレット フィールドを投稿および取得できるようになっている。たしかにこのままだと何もついてないからアクセスできそう。
次にDTOモデルを作成。

・TodoItemDTO.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TodoApi.Models
{
    public class TodoItemDTO
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }
    }
}

なるほど、データ転送時はDTOを使うことで見えなくするってことか。
TodoItemDTO を使用するように TodoItemsController.csを更新して出来上がり。

・ TodoItemsController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
    {
        private readonly TodoContext _context;

        public TodoItemsController(TodoContext context)
        {
            _context = context;
        }

        [HttpGet]
        public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
        {
            return await _context.TodoItems
                .Select(x => ItemToDTO(x))
                .ToListAsync();
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);

            if (todoItem == null)
            {
                return NotFound();
            }

            return ItemToDTO(todoItem);
        }

        [HttpPut("{id}")]
        public async Task<IActionResult> UpdateTodoItem(long id, TodoItemDTO todoItemDTO)
        {
            if (id != todoItemDTO.Id)
            {
                return BadRequest();
            }

            var todoItem = await _context.TodoItems.FindAsync(id);
            if (todoItem == null)
            {
                return NotFound();
            }

            todoItem.Name = todoItemDTO.Name;
            todoItem.IsComplete = todoItemDTO.IsComplete;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
            {
                return NotFound();
            }

            return NoContent();
        }

        [HttpPost]
        public async Task<ActionResult<TodoItemDTO>> CreateTodoItem(TodoItemDTO todoItemDTO)
        {
            var todoItem = new TodoItem
            {
                IsComplete = todoItemDTO.IsComplete,
                Name = todoItemDTO.Name
            };

            _context.TodoItems.Add(todoItem);
            await _context.SaveChangesAsync();

            return CreatedAtAction(
                nameof(GetTodoItem),
                new { id = todoItem.Id },
                ItemToDTO(todoItem));
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);

            if (todoItem == null)
            {
                return NotFound();
            }

            _context.TodoItems.Remove(todoItem);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        private bool TodoItemExists(long id) =>
             _context.TodoItems.Any(e => e.Id == id);

        private static TodoItemDTO ItemToDTO(TodoItem todoItem) =>
            new TodoItemDTO
            {
                Id = todoItem.Id,
                Name = todoItem.Name,
                IsComplete = todoItem.IsComplete
            };
    }
}

ちょっと急に長くなって差分がよくわかんなかったのでGETだけ比較してみた。
・変更前

        [HttpGet]
        public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
        {
            return await _context.TodoItems.ToListAsync();
        }

        // GET: api/TodoItems/5
        [HttpGet("{id}")]
        public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);

            if (todoItem == null)
            {
                return NotFound();
            }

            return todoItem;
        }

・変更後

        [HttpGet]
        public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
        {
            return await _context.TodoItems
                .Select(x => ItemToDTO(x))
                .ToListAsync();
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);

            if (todoItem == null)
            {
                return NotFound();
            }

            return ItemToDTO(todoItem);
        }

これを見ると.Select(x => ItemToDTO(x))でシークレットが指定され、引数で受け取る部分と帰り値がTodoItemDTOに変更されていて、シークレットにアクセスできなくなっていることがわかりました。
POSTで追加してみた。

f:id:msteacher:20210805224737p:plain

なんか追加できているようにも見えるけど、JSONだから多分Secretのところは読み飛ばされて、入っていないのでしょう。
GETして取得してみた。

f:id:msteacher:20210805224833p:plain

Secretが表示されていませんでした。ユーザには見せたくない内部的な処理を書くときには良さそうですね。

おわりに

.net coreについてなんとなーく雰囲気がつかめた気がします。いろいろやってみてさらに理解が深めていけたらと思います。
今回はPostmanを使いましたが、JavaScript を使用した Web API の呼び出しもチュートリアルがあるそうなので、こっちだとわりとwebアプリっぽそうなので、次はそれも記事にしてみたいと思います。
それではまた。
docs.microsoft.com