Angular Tutorial (Tour of Heroes) の Web API を ASP.NET Core で実装して通信する

社内の新人研修の課題として後輩にやってもらった内容となりますが、参考になる部分も多かったので手順をまとめています。
今回は Tour of Heroes を対象としていますが同様の構成のアプリを一から作る際にも参考になるかと思います。

# はじめに

[Tour of Heroes](https://angular.jp/tutorial) においては Web API との通信部分を [In-memory Web API](https://github.com/angular/in-memory-web-api) を用いてシミュレートしています。今回は ASP.NET Core で Web API を実装し、 Tour of Heroes と実装した Web API とを通信するように修正します。In-memory Web API についての Tutorial 内における言及は [6. サーバーからデータの取得](https://angular.jp/tutorial/toh-pt6) にあります。
また Tour of Heroes は完成済みとします。本記事を書くにあたっては[こちら](https://angular.jp/tutorial#%E3%83%84%E3%82%A2%E3%83%BC%E3%82%AA%E3%83%96%E3%83%92%E3%83%BC%E3%83%AD%E3%83%BC%E3%82%BA-%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%A8%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB)にある完成品をダウンロードしています。

最終的には Tour of Heroes, Web API, データベース をそれぞれ Azure PaaS 上にデプロイして動かすことを目標としますが本記事ではいずれもローカル環境で動かすまでについてをまとめます。

# 環境など

+ Angular CLI : `15.0.5`
+ Angular : `15.0.5`
+ ASP.NET Core : `6.0`
+ SQL Server 2019 LocalDB
+ Visual Studio 2022
+ Visual Studio Code

# データベース
## localDB のインストール

[SQL Server Express LocalDB](https://learn.microsoft.com/ja-jp/sql/database-engine/configure-windows/sql-server-express-localdb?view=sql-server-ver16#installation-media) の「インストールメディア」章のリンクから任意のバージョンのインストーラーをダウンロードします。インストーラーを起動したら下図のように「メディアのダウンロード」を選択し次の画面で「localDB」にチェックを入れた状態でダウンロードを開始します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/38574819-8659-99dd-1ae8-c10bee4e4163.png)

## データベースプロジェクトの作成とテーブルの作成から公開
Visual Studio から SQL Server データベースプロジェクトを作成します。本記事では作成したデータベースを localDB に公開します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/28744cfb-94ff-6c08-80e1-9a339d36cb3d.png)

Heroes テーブルを追加し Id, Name 列を設定します。列に関しては Tour of Heroes ソースコードから Hero インターフェースを参考とします。

```ts:hero.ts
export interface Hero {
  id: number;
  name: string;
}
```

:::note info
Hero テーブルを追加した際、Id のプロパティ欄 「Null の許容」 が ```False``` になっていることと「IENTITY の指定」が ```True``` になっていることを確認します。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/d1b3e3c7-96d7-2d94-76a5-c75e95a4174d.png)

Visual Studio からデータベースプロジェクトを localDB に公開します。

:::note info
localDB への公開のウィンドウの出し方
**プロジェクト右クリック → 公開...** によって表示できます。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/504eb4d4-b8f4-672e-1a0f-3f6c28ba07a9.png)

データベースの公開を確認します。Heroes テーブルにはまだデータが入っていません。ここでは [SQL Server (mssql)](https://marketplace.visualstudio.com/items?itemName=ms-mssql.mssql) を Visual Studio Code にインストールして localDB へ接続しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/bfb7871f-60cf-3a7e-b6cf-ac9e1f606518.png)

# Web API

[チュートリアル: ASP.NET Core で Web API を作成する](https://learn.microsoft.com/ja-jp/aspnet/core/tutorials/first-web-api?view=aspnetcore-6.0&tabs=visual-studio) を参考とします。本章においてはこちらの参考資料のセクションに対応するようにそれぞれのセクションでの手順や注意点などをまとめます(省略するセクションもあります)。参考資料と併せてご覧いただけると良いと思います。

主な変更点は次のようなものになります。

- 「**Todo** データ」を扱う Web API から 「**Hero** データ」を扱う Web API に変更をします
- データベースプロバイダを **In-Memory** から **SQL Server** に変更します 

## Web プロジェクトの作成

本資料ではこの手順において作成するプロジェクトの名前を「TourOfHeroes.API」としています。

### launchUrl を更新する
設定する URI を ```"api/heroes"``` に変更しましょう。
```json
"launchUrl": "api/heroes"
```

## モデルクラスの追加

チュートリアルでは ```TodoItem``` クラスを実装していますが代わりに下記のような ```Hero``` クラスを Models フォルダ下に作成します。

```c#:Models/Hero.cs
namespace TourOfHeroes.API.Models
{
    public class Hero
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

```

## データベース コンテキストの追加

### NuGet パッケージを追加する

今回は前の手順で作成した localDB 上のテーブルに接続しますので `Microsoft.EntityFrameworkCore.InMemory` についてはインストールしません。
代わりに下記の NuGet パッケージを追加します。[EF Core のリリースと計画](https://learn.microsoft.com/ja-jp/ef/core/what-is-new/)の対象表を参考に .NET のバージョンに対応した EF Core バージョンを追加しましょう。

- `Microsoft.EntityFrameworkCore`
- `Microsoft.EntityFrameworkCore.SqlServer`

:::note info
[EF Core の NuGet パッケージ](https://learn.microsoft.com/ja-jp/ef/core/what-is-new/nuget-packages)から抜粋。
> 必ず、Microsoft から出荷されたすべての EF Core パッケージの同じバージョンをインストールしてください。 たとえば、Microsoft.EntityFrameworkCore.SqlServer のバージョン 5.0.3 をインストールする場合は、他の Microsoft.EntityFrameworkCore.* パッケージもすべて 5.0.3 にする必要があります。
:::

## TodoContext データベースコンテキストの追加

この手順では「TodoContext」というクラスの代わりに「HeroContext」クラスを追加します。
この時追加する ```HeroContext``` クラスの実装は下記のようにこれまでの手順で作成した ```HeroContext``` と ```Hero``` クラスを参照するようにチュートリアルの実装から変更を加えてください。

```c#:Models/HeroContext.cs
using Microsoft.EntityFrameworkCore;

namespace TourOfHeroes.API.Models
{
    public class HeroContext : DbContext
    {
        public HeroContext(DbContextOptions<HeroContext> options)
            : base(options)
        {
        }

        public DbSet<Hero> Heroes { get; set; } = null!;
    }
}
```

:::note info
上記で言う `Heroes` プロパティについては前の手順で作成したデータベースのテーブル名と一致させます。
:::

## データベース コンテキストの登録
`Program.cs` で必要になる変更は下記のとおりとなります。

- ①データベースプロバイダーの変更（In-Memory > SQL Server）に対応するための変更
- ②データベースの接続文字列を `appsettings.json` から読み込むための変更

まず `appsettings.json` に localDB への接続情報を設定します。`ConnectionStrings` の部分が追加する箇所となります。

:::note info
このとき前の手順で作成した localDB インスタンス名とデータベース名がそれぞれ `Data Source` 部分と `Initial Catalog` 部分で指定している値と一致させます。
:::

```json:appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=TourOfHeroes.DB;Integrated Security=True;"
  },
  "AllowedHosts": "*"
}

```

次に `Program.cs` の実装を変更します。変更箇所についてはそれぞれ番号付きでコメントを付けています。

```c#:Program.cs
using Microsoft.EntityFrameworkCore;
using TourOfHeroes.API.Models;

var builder = WebApplication.CreateBuilder(args);

// ②こちらを追加
IConfiguration config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddEnvironmentVariables()
    .Build();

// Add services to the container.

builder.Services.AddControllers();
builder.Services.AddDbContext<HeroContext>(opt =>
    opt.UseSqlServer(config.GetConnectionString("DefaultConnection"))); // ①及び② ここを変更
//builder.Services.AddSwaggerGen(c =>
//{
//    c.SwaggerDoc("v1", new() { Title = "TodoApi", Version = "v1" });
//});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (builder.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    //app.UseSwagger();
    //app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TodoApi v1"));
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
```

## コントローラーのスキャフォールディング
こちらの章については次の「GET メソッドの確認」まで TodoItem から Hero への変更に注意しながらチュートリアルの手順で進めていきましょう。

## GET メソッドの確認

Heroes テーブルにはデータを登録してませんので API プロジェクトをデバッグ実行し `https://localhost:44389/api/Heroes` にアクセスすると下記のように空のデータが返ってきます。

:::note warn
アクセス先となる localhost に続くポート番号に関してはプロジェクトごとに異なりますので自身のプロジェクトの設定を確認したうえで読み替えてください。
:::

```
[]
```

:::note info
前の手順ですでに Hero データの登録（Post）のテストを行っている場合はその時に登録したデータが取得されていることを確認しましょう。
:::

## searchHeroes に対応する Controller の実装

**こちらは参考資料に対応するセクションはありません。**
Tour of Heroes では searchHeroes メソッドにより、ヒーロー名 (Hero.name) をパラメータとしてヒーローのマッチング結果を返してもらうように Web API にリクエストしています。
ここまでの手順で Web API にはそうした実装が含まれていませんのでこちらを実装します。

```ts:hero.service.ts
  /* 検索語を含むヒーローを取得する */
  searchHeroes(term: string): Observable<Hero[]> {
    if (!term.trim()) {
      // 検索語がない場合、空のヒーロー配列を返す
      return of([]);
    }
    return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
      tap(_ => this.log(`found heroes matching "${term}"`)),
      catchError(this.handleError<Hero[]>('searchHeroes', []))
    );
  }
```

GetHeroes メソッドで `api/Heroes/?name=takashi` のようにクエリ文字列として検索語となる文字列を受け取れるようにします。クエリ文字列の受け取りについては [こちら](https://docs.microsoft.com/ja-jp/aspnet/core/web-api/?view=aspnetcore-3.1#binding-source-parameter-inference) を参考にしました。

```c#:HeroesController.cs
        // GET: api/Heroes
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Hero>>> GetHeroes([FromQuery]string? name)
        {
            if(name == null)
            {
                name = "";
            }
            return await _context.Heroes.Where(hero => hero.Name.Contains(name)).ToListAsync();
        }
```

:::note info
`string.Contains` メソッドで検索語を部分文字列として持つ名前のヒーローを抽出するようにします。
クエリ文字列として受け取る name は全件検索のために省略可能とし空文字に置き換えます。
:::

# Tour of Heroes

ここまでで Web API とデータベースが接続されたので最後に Tour of Heroes から Web API を接続するように修正していきます。本章ではいくつかある修正ポイントを一つずつ追ってみていきたいと思います。

## In-memory Web API モジュールのインポートの削除

元から記述されているコメントの通り In-memory Web API モジュールをインポートしている箇所をコメントアウトしています。

:::note info
[README - Import the in-memory web api module](https://github.com/angular/in-memory-web-api#import-the-in-memory-web-api-module) の Notes の最終項目（In Production, ...）に実行環境によってモジュールのインポートを削除する方法も書いてありますが、今回は当てはまりません。
:::

```ts:app.module.ts
import { NgModule }       from '@angular/core';
import { BrowserModule }  from '@angular/platform-browser';
import { FormsModule }    from '@angular/forms';
import { HttpClientModule }    from '@angular/common/http';

import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService }  from './in-memory-data.service';

import { AppRoutingModule }     from './app-routing.module';

import { AppComponent }         from './app.component';
import { DashboardComponent }   from './dashboard/dashboard.component';
import { HeroDetailComponent }  from './hero-detail/hero-detail.component';
import { HeroesComponent }      from './heroes/heroes.component';
import { HeroSearchComponent }  from './hero-search/hero-search.component';
import { MessagesComponent }    from './messages/messages.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule,
    HttpClientModule,

    // The HttpClientInMemoryWebApiModule module intercepts HTTP requests
    // and returns simulated server responses.
    // Remove it when a real server is ready to receive requests.
    // HttpClientInMemoryWebApiModule.forRoot(
    //   InMemoryDataService, { dataEncapsulation: false }
    // )
  ],
  declarations: [
    AppComponent,
    DashboardComponent,
    HeroesComponent,
    HeroDetailComponent,
    MessagesComponent,
    HeroSearchComponent
  ],
  bootstrap: [ AppComponent ]
})

export class AppModule { }
```
In-memory Web API のインポートを削除した状態で Tour of Heroes を起動してみましょう。
`http://localhost:4200/api/heroes` へのリクエストで `404 Not Found` となっていることが確認できます。

:::note info
Tour of Heroes のダッシュボード上に表示されるメッセージやブラウザの Dev Tools のコンソールなどから確認できます。
:::

```
HeroService: getHeroes failed: Http failure response for http://localhost:4200/api/heroes: 404 Not Found
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/f2bbee42-cea3-a793-9181-2894c2c55517.png)

:::note info
ここでは Angular アプリケーション内部で Web API としての機能を提供していた In-memory Web API を削除したため `404` エラーが発生しています。
:::

## リクエスト先 の URL を実装した Web API のものに修正

実際に Web API にリクエストを送るメソッドは `hero.service.ts` で実装されています。`heroesUrl` の値を下記のように前の手順で実装した Web API のものに変更します。

```ts:hero.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';

import { Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';

import { Hero } from './hero';
import { MessageService } from './message.service';


@Injectable({ providedIn: 'root' })
export class HeroService {

  // private heroesUrl = 'api/heroes';  // Web APIのURL
  private heroesUrl = 'https://localhost:44389/api/heroes';  // Web APIのURL

  ...

  /** サーバーからヒーローを取得する */
  getHeroes (): Observable<Hero[]> {
    return this.http.get<Hero[]>(this.heroesUrl)
      .pipe(
        tap(heroes => this.log('fetched heroes')),
        catchError(this.handleError<Hero[]>('getHeroes', []))
      );
  }

  ...
```

修正後のメッセージを見ると意図した URL に対してリクエストを行うように変化していることが確認できます。
しかし依然としてデータは取得できません。メッセージの内容が下記のとおりになっていることを確認しましょう。

```
HeroService: getHeroes failed: Http failure response for https://localhost:44389/api/heroes: 0 Unknown Error
```

Chrome のデベロッパーツールから Console 上にエラーメッセージが表示されていることを確認します。

```
Access to XMLHttpRequest at 'https://localhost:44389/api/heroes' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
```

:::note info
作成した Web API にリクエストが送られていること（404 が解消されること）と CORS policy に関わるエラーが発生していることを確認します。
:::

## プロキシの設定

前の手順で Web API の URL を変更する方法ではうまくいかないことがわかったので [バックエンドサーバーへのプロキシ](https://angular.jp/guide/build#%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%B8%E3%81%AE%E3%83%97%E3%83%AD%E3%82%AD%E3%82%B7-) を参考に Web API にリクエストを送ります。

前の手順で変更した heroesUrl の値は元に戻しておきます。

```ts:hero.service.ts
private heroesUrl = 'api/heroes';  // Web APIのURL
```

`proxy.conf.json` の設定で注意が必要なことは target のプロトコルとポート番号が実装した Web API のそれと一致していることです。今回の手順であれば `https` になっていることに注意してください。

```json:src/proxy.conf.json
{
  "/api": {
    "target": "https://localhost:44389",
    "secure": false
  }
}
```

`angular.json` に関しては参考資料のように `options > proxyConfig` で設定するかもしくは `development > proxyConfig` で設定すると上手くいきます。

```json:angular.json
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
             "proxyConfig": "src/proxy.conf.json"
          },
          "configurations": {
            "production": {
              "browserTarget": "angular.io-example:build:production"
            },
            "development": {
              "browserTarget": "angular.io-example:build:development",
            }
          },
```

```json:angular.json
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "configurations": {
            "production": {
              "browserTarget": "angular.io-example:build:production"
            },
            "development": {
              "browserTarget": "angular.io-example:build:development",
              "proxyConfig": "src/proxy.conf.json" 
            }
          },
```

ここまでで `getHeroes` (ヒーロー一覧の取得)で Web API に接続できたことを確認します。
Tour of Heroes 画面上に下記のメッセージが表示されます。

```
HeroService: fetched heroes
```

また Chrome デベロッパーツールの　Network タブから heroes へのリクエストが `Status Code: 200` で返ってきていることを確認します。

## updateHero の修正

ここまでの実装ではヒーローの更新処理がうまくいきません。
ヒーローの詳細画面からヒーロー名を更新する操作をすると `405 Method Not Allowed` となります。

```
HeroService: updateHero failed: Http failure response for http://localhost:4200/api/heroes: 405 Method Not Allowed
```

上記メッセージからもわかるように Tour of Heroes の実装では `api/heroes` に対してリクエストをしています。Chrome のデベロッパーツールからも確認できます。
対して Web API の `PutHero` メソッドは `api/Heroes/{id}` としてヒーローの id を受け取るように実装されています。

```c#:HeroesController.cs
        // PUT: api/Heroes/5
        // To protect from overposting attacks, please enable the specific properties you want to bind to, for
        // more details see https://aka.ms/RazorPagesCRUD.
        [HttpPut("{id}")]
        public async Task<IActionResult> PutHero(int id, Hero hero)
        {
            if (id != hero.Id)
            {
                return BadRequest();
            }

            _context.Entry(hero).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!HeroExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }
```

`getHero` メソッドや `deleteHero` メソッドを参考に `url` の値を修正します。修正後再度ヒーロー情報の更新操作を実行して操作が成功することを確認してください。

```ts:hero.service.ts
  /** PUT: サーバー上でヒーローを更新 */
  updateHero (hero: Hero): Observable<any> {
    const url = `${this.heroesUrl}/${hero.id}`;
    return this.http.put(url, hero, this.httpOptions).pipe(
      tap(_ => this.log(`updated hero id=${hero.id}`)),
      catchError(this.handleError<any>('updateHero'))
    );
  }
```

# 確認

Tour of Heroes, Web API をそれぞれ localhost で実行し Tour of Heroes からの各種操作を確認します。
下記は 「操作 / Tour of Heroes メソッド / Web API メソッド」 の対応です。

+ ヒーローの登録 / addHero / PostHero
+ ヒーローの更新 / updateHero / PutHero
+ ヒーローの一覧の取得 / getHeroes / GetHeroes
+ ID によるヒーローの取得 / getHero / GetHero
+ ID によるヒーローの削除 / deleteHero / DeleteHero
+ ヒーローの検索 / searchHeroes / GetHero

一連の操作による画面上でのデータの変化およびデータベースの値の変化を確認します。

# まとめ

Tour of Heroes でモックやダミーとなっていた Web API とデータベースの実装を行いました。
ローカル環境での実行で今回実装したそれぞれのアプリ、データベースが接続されていることを確認しました。

# 参考

+ [angular.jp/tutorial](https://angular.jp/tutorial)
+ [チュートリアル: ASP.NET Core で Web API を作成する](https://learn.microsoft.com/ja-jp/aspnet/core/tutorials/first-web-api?view=aspnetcore-6.0&tabs=visual-studio)
+ [ASP.NET Core を使って Web API を作成する](https://learn.microsoft.com/ja-jp/aspnet/core/web-api/?view=aspnetcore-6.0)
+ [in-memory-web-api](https://github.com/angular/in-memory-web-api)
+  [バックエンドサーバーへのプロキシ](https://angular.jp/guide/build#%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%81%B8%E3%81%AE%E3%83%97%E3%83%AD%E3%82%AD%E3%82%B7-)