WebSurfer's Home

Filter by APML

ASP.NET Core Minimal API

by WebSurfer 6. July 2025 15:56

今さらながらですが、ASP.NET Core に Minimal API という軽量・高速 HTTP API を構築する簡単な手法があるということを知ったので、少しだけですが試してみました。以下に備忘録として試したことを書いておきます。

Minimal API からデータ取得

Minimal API というのは、Microsoft のドキュメント「Minimal API の概要」に書いてありますように、Controller を使わないで最小のコードと構成で REST API を構築するもので、Controller のアクションメソッドに代えて Program.cs のミドルウェアで要求を処理して応答を返します。

基本的な作り方は Microsoft のチュートリアル「チュートリアル: ASP.NET Core を使って最小 API を作成する」にありましたので、それを参考に「API コードを追加する」のセクションのところまで作ってみました。

作り方については、プロジェクトの作成からサンプル REST API を構築するための詳しい方法までチュートリアルに述べられていますのでそちらを見てください。手抜きでスミマセンが、チュートリアルには十分な情報が提供されており、追加で書くことなどもありませんので。

チュートリアルの「API コードを追加する」のセクションまで進めば、GET, POST, PUT, DELETE 要求を受けて JSON 文字列を返す REST API が完成します。

上の画像は、検証のため別の ASP.NET Core MVC アプリに作成した REST API を呼び出すページを追加して、ボタンクリックで JavaScript の fetch を使って要求を出し、JSON 文字列として返される応答を表示したものです。そのコードはこの記事の下の方に参考に載せておきます。

また、検証に使用した呼び出し側のアプリはクロスオリジンになるので Minimal API では CORS の機能を有効にする必要がありますが、その方法を以下に書いておきます。

(1) CORS の機能を実装

先の記事「Web API に CORS 実装 (CORE)」に書いた Controller を使う普通の ASP.NET Core Web API と同様に、Program.cs に AddCors と UseCors を追加すれば CORS の機能は働くようになります。具体例は以下の通りです。

namespace MinimalAPI
{
    public class Program
    {
        // CORS の機能を追加
        const string MyAllowAnyOrigins = "_myAllowAnyOrigins";

        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // CORS の機能を追加
            builder.Services.AddCors(options =>
            {
                options.AddPolicy(name: MyAllowAnyOrigins,
                                  policy =>
                                  {
                                      policy.AllowAnyOrigin()
                                            .AllowAnyHeader()
                                            .AllowAnyMethod();
                                  });
            });

            // ・・・中略・・・

            var app = builder.Build();

            // CORS の機能を追加
            app.UseCors(MyAllowAnyOrigins);

            // ・・・中略・・・            
        }
    }

Microsoft のドキュメント「CORS(異なるオリジン間でのリソース共有) 」にも説明がありますが、そのドキュメントには書いてない AllowAnyHeader() と AllowAnyMethod() は Preflight リクエストに必要なので注意してください。

また、そのドキュメントには "CORS は、[EnableCors] 属性により、または RequireCors メソッドを使用して宣言できます" と書いてありますが、自分が試した限りではそれらは不要でした。

(2) 呼び出し側の View のコード

検証のため、別の ASP.NET Core MVC アプリに作成した View のコードです。ボタンクリックで JavaScript の fetch を使って要求を出し、JSON 文字列として返される応答を表示します。

@{
    ViewData["Title"] = "MinimalApiCors";
}

<h1>MinimalApiCors</h1>

<input type="button" value="READ ALL" onclick="minimalApiGet();" />
<input type="button" value="READ 1" onclick="minimalApiGet1();" />
<input type="button" value="READ COMPLETE" onclick="minimalApiGetCompleted();" />
<input type="button" value="UPDATE 1" onclick="minimalApiPut1();" />
<input type="button" value="DELETE 1" onclick="minimalApiDelete1();" />
<input type="button" value="CREATE" onclick="minimalApiPost();" />

<ul id="heroes"></ul>

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[

        const url = "https://localhost:44374/todoitems"; // IIS Express
        const elem = document.querySelector("#heroes");

        const minimalApiGet = async () => {
            const response = await fetch(url);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                for (let i = 0; i < data.length; i++) {
                    elem.insertAdjacentHTML("beforeend",
                        `<li>${data[i].id}: ${data[i].name}, ${data[i].isComplete}</li>`);
                }
            } else {
                elem.innerHTML = "失敗";
            }
        };

        const minimalApiGet1 = async () => {
            const response = await fetch(url + "/1");
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                elem.insertAdjacentHTML("beforeend",
                    `<li>${data.id}: ${data.name}, ${data.isComplete}</li>`);
            } else {
                elem.innerHTML = `失敗 (${response.status})`;
            }
        };

        const minimalApiGetCompleted = async () => {
            const response = await fetch(url + "/complete");
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                for (let i = 0; i < data.length; i++) {
                    elem.insertAdjacentHTML("beforeend",
                        `<li>${data[i].id}: ${data[i].name}, ${data[i].isComplete}</li>`);
                }
            } else {
                elem.innerHTML = "失敗";
            }
        };

        const minimalApiPut1 = async () => {
            const params = {
                method: "PUT",
                body: '{"Id":1,"Name":"Updated animal","isComplete":true}',
                headers: { 'Content-Type': 'application/json' }
            }
            const response = await fetch(url + "/1", params);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                elem.insertAdjacentHTML("beforeend",
                    `<li>${data.id}: ${data.name}, ${data.isComplete} - Updated</li>`);
            } else {
                elem.innerHTML = `失敗 (${response.status})`;
            }
        };

        const minimalApiDelete1 = async () => {
            const params = {
                method: "DELETE"
            }
            const response = await fetch(url + "/1", params);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                elem.insertAdjacentHTML("beforeend",
                    `<li>${data.id}: ${data.name}, ${data.isComplete} - Deleted</li>`);
            } else {
                elem.innerHTML = `失敗 (${response.status})`;
            }
        };

        const minimalApiPost = async () => {
            const params = {
                method: "POST",
                body: '{"name":"Posted animal","isComplete":true}',
                headers: { 'Content-Type': 'application/json' }
            }
            const response = await fetch(url, params);
            if (response.ok) {
                const data = await response.json();
                elem.innerHTML = "";
                elem.insertAdjacentHTML("beforeend",
                    `<li>${data.id}: ${data.name}, ${data.isComplete} - Inserted</li>`);
            } else {
                elem.innerHTML = "失敗";
            }
        };

        //]]>
    </script>
}

Tags: , , ,

Web API

React アプリから ASP.NET Core Web API を呼び出し

by WebSurfer 2. July 2024 15:36

React アプリから、トークン (JWT) ベースの認証が必要な ASP.NET Core Web API にクロスドメインでアクセスしてデータを取得するサンプルを作ってみました。

結果の表示

先の記事「Blazor WASM から ASP.NET Core Web API を呼び出し」の Blazor WASM アプリを React アプリに代えただけです。

Web API アプリの作り方は先の記事に詳しく書きましたのでそちらを見てください。

React アプリは、Visual Studio 2022 v17.10.3 のテンプレートを使って「新しいプロジェクトの作成」ダイアログで「React and ASP.NET Core」のテンプレートを選び、ターゲットフレームワーク .NET 8.0 で作成したものです。

その中の .client プロジェクトの src\App.jsx ファイルの populateWeatherData メソッドに以下のように手を加えて、先の記事の Web API を呼び出すようにしています。その結果が上の画像です。

import { useEffect, useState } from 'react';
import './App.css';

function App() {
    const [forecasts, setForecasts] = useState();

    useEffect(() => {
        populateWeatherData();
    }, []);

    const contents = forecasts === undefined
        ? <p><em>Loading... Please refresh once the ASP.NET backend has started.</em></p>
        : <table className="table table-striped" aria-labelledby="tabelLabel">
            <thead>
                <tr>
                    <th>Date</th>
                    <th>Temp. (C)</th>
                    <th>Temp. (F)</th>
                    <th>Summary</th>
                </tr>
            </thead>
            <tbody>
                {forecasts.map(forecast =>
                    <tr key={forecast.date}>
                        <td>{forecast.date}</td>
                        <td>{forecast.temperatureC}</td>
                        <td>{forecast.temperatureF}</td>
                        <td>{forecast.summary}</td>
                    </tr>
                )}
            </tbody>
        </table>;

    return (
        <div>
            <h1 id="tabelLabel">Weather forecast</h1>
            <p>This component demonstrates fetching data from the server.</p>
            {contents}
        </div>
    );
    
    async function populateWeatherData() {
        // 自動生成されたコード
        //const response = await fetch('weatherforecast');
        //const data = await response.json();
        //setForecasts(data);

        // ・・・を、以下のように書き換える

        const tokenUrl = "https://localhost:44366/api/token";
        const forecastUrl = "https://localhost:44366/WeatherForecast";

        // 送信する ID とパスワード
        const credentials = {
            Username: "oz@mail.example.com",
            Password: "myPassword"
        };

        const params = {
            method: "POST",
            body: JSON.stringify(credentials),
            headers: { 'Content-Type': 'application/json' }
        };

        // ID とパスワードを POST 送信してトークンを取得
        const responseToken = await fetch(tokenUrl, params);
        if (responseToken.ok) {
            const data = await responseToken.json();
            let token = data.token;

            // 取得したトークンを Authorization ヘッダに含めて
            // 天気予報データを GET 要求し、応答を表示
            const responseForecast = await fetch(forecastUrl,
                { headers: { 'Authorization': `Bearer ${token}` } });
            if (responseForecast.ok) {
                const data = await responseForecast.json();
                setForecasts(data);
            }
        }
    }
}

export default App;

不可解なのが、上のコードでは Web API が 2 回呼ばれることです。ネットの記事を見ると、上のコード例のように useState の第 2 引数に空の配列 [] を設定した場合は初回レンダリングの際の 1 回だけしか第 1 引数に設定した populateWeatherData() は実行されないそうですが、Fiddler で要求・応答をキャプチャしてみると 2 回呼び出しが行われていました。

ネットの記事をいろいろ探したところ、React の useEffect のドキュメント(これが公式?)の「外部システムへの接続」のセクションに以下の記述を見つけました。

"バグを見つけ出すために、開発中には React はセットアップとクリーンアップを、セットアップの前に 1 回余分に実行します。これは、エフェクトのロジックが正しく実装されていることを確認するストレステストです。"

この記事以外に 2 回呼ばれる理由と結びつくことが書かれた記事は見つけられませんでした。本番環境で検証するとかすれば分かるのかもしれませんが、今はその気力がありません。(汗) 調べる気力が出てきて、何か分かったら追記します。

ちなみに、React には「クラスコンポーネント」と「関数コンポーネント」というものがあるそうで、上のサンプルコードは「関数コンポーネント」を使っています。それゆえ React フックと呼ばれる useEffect を使わざるを得ないようです。

一方、古い Visual Studio のテンプレートで作った React アプリは「クラスコンポーネント」を使っており、componentDidMount で Web API の呼び出しを行っています。その場合は 2 回 Web API が呼ばれることは無いです。

Tags: , , ,

React

CORS 非対応の場合のエラーメッセージ(その2)

by WebSurfer 4. June 2024 12:47

先の記事「CORS 非対応の場合のエラーメッセージ」の続きで、fetch メソッドで指定した URL が間違っていた場合どうなるかを調べたので、備忘録として書いておきます。

URL が間違っているとサーバーからは HTTP 404 Not Found 応答が返ってきますが、それがどのように表示されるかが書いておきたかったことです。

ちなみに、404 応答というのは、(1) URL のホスト名は正しく名前解決されブラウザからの要求は Web サーバーに届いた、(2) 要求を受け取った Web サーバーは URL に指定されたリソースを探したが見つからなかった、(3) Web サーバーは見つからなかったという応答を返した・・・ということです。

検証に使ったのは、先の記事「Web API に CORS 実装 (CORE)」に書いた ASP.NET Core Web API アプリで、Web API の CORS を無効にして、ドメインが異なる MVC アプリから fetch API を使って Web API に要求を出します。その際、正しい URL は https://localhost:44371/api/values とすべきところを、values ⇒ valuesx としました。

実は、CORS を無効にしたら 404 応答は返ってこないと思い違いをしていたのですが、そうではなかったです。バックエンドが CORS 非対応でもフロントエンドから要求は出て、URL が間違っているため指定されたリソースが見つからない場合は 404 応答が返ってきます。

以下に、「CORS 非対応 URL 誤」「CORS 非対応 URL 正」「CORS 有効 URL 誤」の 3 つのケースで、Chrome のディベロッパーツールの Console に表示されるエラーメッセージと、Fiddler で要求・応答をキャプチャした画像を貼っておきます。

(1) CORS 非対応 URL 誤

CORS 非対応でもブラウザから要求は出ます。URL が間違っているので Web サーバーは 404 応答を返します。それを受けたブラウザでは、まず応答ヘッダに Access-Control-Allow-Origin が無いということで CORS 関係のエラーメッセージを出しています。次に、404 応答なのでそのエラーメッセージ、最後に Failed to fetch (fetch 失敗) というエラーメッセージを出しています。

CORS 非対応 URL 誤

Fiddler の画像です。404 応答が返ってきています。バックエンドが CORS 非対応なので応答ヘッダには Access-Control-Allow-Origin は含まれていません。

CORS 非対応 URL 誤


(2) CORS 非対応 URL 正

上の (1) のケースから URL を正しいものにした場合の結果です。URL は正しいので当然 404 応答ではなく 200 応答が返ってきます。JSON 文字列も正しく応答コンテンツに含まれて返ってきます。しかし、応答ヘッダに Access-Control-Allow-Origin が無いので fetch メソッドでのデータの取得に失敗しています。

CORS 非対応 URL 正

Fiddler の画像です。URL は正しいので 200 応答が返ってきています。応答の TextView (コンテンツ) を見ると期待通り JSON 文字列が返ってきています。しかし、バックエンドが CORS 非対応なので応答ヘッダには Access-Control-Allow-Origin は含まれていません。

CORS 非対応 URL 正


(3) CORS 有効 URL 誤

上の (1) のケースからバックエンドの CORS 対応を有効にした結果です。URL は間違ったままなので 404 応答が返ってきています。上の (1), (2) と違って fetch 失敗とはみなされないようです。

CORS 有効 URL 誤

CORS を有効にしたので応答ヘッダには Access-Control-Allow-Origin が含まれています。

CORS 有効 URL 誤

Chrome のディベロッパーツールの Source タブの画面です。上の (1), (2) のケースでは fetch(url) の実行で例外がスローされて処理が終わってしまいますが、(3) のケースでは if(response.ok) 以降に処理が進みます。

CORS 有効 URL 誤

Tags: , , , ,

JavaScript

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  December 2025  >>
MoTuWeThFrSaSu
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar