社内の新人研修の課題として後輩にやってもらった内容となりますが、参考になる部分も多かったので手順をまとめています。
今回は Tour of Heroes を対象としていますが同様の構成のアプリを一から作る際にも参考になるかと思います。
はじめに
Tour of Heroes においては Web API との通信部分を In-memory Web API を用いてシミュレートしています。今回は ASP.NET Core で Web API を実装し、 Tour of Heroes と実装した Web API とを通信するように修正します。In-memory Web API についての Tutorial 内における言及は 6. サーバーからデータの取得 にあります。
また Tour of Heroes は完成済みとします。本記事を書くにあたってはこちらにある完成品をダウンロードしています。
最終的には 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 の「インストールメディア」章のリンクから任意のバージョンのインストーラーをダウンロードします。インストーラーを起動したら下図のように「メディアのダウンロード」を選択し次の画面で「localDB」にチェックを入れた状態でダウンロードを開始します。
データベースプロジェクトの作成とテーブルの作成から公開
Visual Studio から SQL Server データベースプロジェクトを作成します。本記事では作成したデータベースを localDB に公開します。
Heroes テーブルを追加し Id, Name 列を設定します。列に関しては Tour of Heroes ソースコードから Hero インターフェースを参考とします。
export interface Hero {
id: number;
name: string;
}
Hero テーブルを追加した際、Id のプロパティ欄 「Null の許容」 が False
になっていることと「IENTITY の指定」が True
になっていることを確認します。
Visual Studio からデータベースプロジェクトを localDB に公開します。
localDB への公開のウィンドウの出し方
プロジェクト右クリック → 公開... によって表示できます。
データベースの公開を確認します。Heroes テーブルにはまだデータが入っていません。ここでは SQL Server (mssql) を Visual Studio Code にインストールして localDB へ接続しています。
Web API
チュートリアル: ASP.NET Core で Web API を作成する を参考とします。本章においてはこちらの参考資料のセクションに対応するようにそれぞれのセクションでの手順や注意点などをまとめます(省略するセクションもあります)。参考資料と併せてご覧いただけると良いと思います。
主な変更点は次のようなものになります。
- 「Todo データ」を扱う Web API から 「Hero データ」を扱う Web API に変更をします
- データベースプロバイダを In-Memory から SQL Server に変更します
Web プロジェクトの作成
本資料ではこの手順において作成するプロジェクトの名前を「TourOfHeroes.API」としています。
launchUrl を更新する
設定する URI を "api/heroes"
に変更しましょう。
"launchUrl": "api/heroes"
モデルクラスの追加
チュートリアルでは TodoItem
クラスを実装していますが代わりに下記のような Hero
クラスを Models フォルダ下に作成します。
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 のリリースと計画の対象表を参考に .NET のバージョンに対応した EF Core バージョンを追加しましょう。
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
必ず、Microsoft から出荷されたすべての EF Core パッケージの同じバージョンをインストールしてください。 たとえば、Microsoft.EntityFrameworkCore.SqlServer のバージョン 5.0.3 をインストールする場合は、他の Microsoft.EntityFrameworkCore.* パッケージもすべて 5.0.3 にする必要があります。
TodoContext データベースコンテキストの追加
この手順では「TodoContext」というクラスの代わりに「HeroContext」クラスを追加します。
この時追加する HeroContext
クラスの実装は下記のようにこれまでの手順で作成した HeroContext
と Hero
クラスを参照するようにチュートリアルの実装から変更を加えてください。
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!;
}
}
上記で言う Heroes
プロパティについては前の手順で作成したデータベースのテーブル名と一致させます。
データベース コンテキストの登録
Program.cs
で必要になる変更は下記のとおりとなります。
- ①データベースプロバイダーの変更(In-Memory > SQL Server)に対応するための変更
- ②データベースの接続文字列を
appsettings.json
から読み込むための変更
まず appsettings.json
に localDB への接続情報を設定します。ConnectionStrings
の部分が追加する箇所となります。
このとき前の手順で作成した localDB インスタンス名とデータベース名がそれぞれ Data Source
部分と Initial Catalog
部分で指定している値と一致させます。
{
"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
の実装を変更します。変更箇所についてはそれぞれ番号付きでコメントを付けています。
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
にアクセスすると下記のように空のデータが返ってきます。
アクセス先となる localhost に続くポート番号に関してはプロジェクトごとに異なりますので自身のプロジェクトの設定を確認したうえで読み替えてください。
[]
前の手順ですでに Hero データの登録(Post)のテストを行っている場合はその時に登録したデータが取得されていることを確認しましょう。
searchHeroes に対応する Controller の実装
こちらは参考資料に対応するセクションはありません。
Tour of Heroes では searchHeroes メソッドにより、ヒーロー名 (Hero.name) をパラメータとしてヒーローのマッチング結果を返してもらうように Web API にリクエストしています。
ここまでの手順で Web API にはそうした実装が含まれていませんのでこちらを実装します。
/* 検索語を含むヒーローを取得する */
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
のようにクエリ文字列として検索語となる文字列を受け取れるようにします。クエリ文字列の受け取りについては こちら を参考にしました。
// 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();
}
string.Contains
メソッドで検索語を部分文字列として持つ名前のヒーローを抽出するようにします。
クエリ文字列として受け取る name は全件検索のために省略可能とし空文字に置き換えます。
Tour of Heroes
ここまでで Web API とデータベースが接続されたので最後に Tour of Heroes から Web API を接続するように修正していきます。本章ではいくつかある修正ポイントを一つずつ追ってみていきたいと思います。
In-memory Web API モジュールのインポートの削除
元から記述されているコメントの通り In-memory Web API モジュールをインポートしている箇所をコメントアウトしています。
README - Import the in-memory web api module の Notes の最終項目(In Production, ...)に実行環境によってモジュールのインポートを削除する方法も書いてありますが、今回は当てはまりません。
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
となっていることが確認できます。
Tour of Heroes のダッシュボード上に表示されるメッセージやブラウザの Dev Tools のコンソールなどから確認できます。
HeroService: getHeroes failed: Http failure response for http://localhost:4200/api/heroes: 404 Not Found
ここでは Angular アプリケーション内部で Web API としての機能を提供していた In-memory Web API を削除したため 404
エラーが発生しています。
リクエスト先 の URL を実装した Web API のものに修正
実際に Web API にリクエストを送るメソッドは hero.service.ts
で実装されています。heroesUrl
の値を下記のように前の手順で実装した Web API のものに変更します。
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.
作成した Web API にリクエストが送られていること(404 が解消されること)と CORS policy に関わるエラーが発生していることを確認します。
プロキシの設定
前の手順で Web API の URL を変更する方法ではうまくいかないことがわかったので バックエンドサーバーへのプロキシ を参考に Web API にリクエストを送ります。
前の手順で変更した heroesUrl の値は元に戻しておきます。
private heroesUrl = 'api/heroes'; // Web APIのURL
proxy.conf.json
の設定で注意が必要なことは target のプロトコルとポート番号が実装した Web API のそれと一致していることです。今回の手順であれば https
になっていることに注意してください。
{
"/api": {
"target": "https://localhost:44389",
"secure": false
}
}
angular.json
に関しては参考資料のように options > proxyConfig
で設定するかもしくは development > proxyConfig
で設定すると上手くいきます。
"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",
}
},
"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 を受け取るように実装されています。
// 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
の値を修正します。修正後再度ヒーロー情報の更新操作を実行して操作が成功することを確認してください。
/** 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 とデータベースの実装を行いました。
ローカル環境での実行で今回実装したそれぞれのアプリ、データベースが接続されていることを確認しました。