WebSurfer's Home

Filter by APML

ASP.NET Core Web API と Swagger(その1)

by WebSurfer 22. October 2024 18:00

Visual Studio 2022 のテンプレートを使ってターゲットフレームワーク .NET 8.0 で ASP.NET Core Web API のプロジェクトを作成し、Visual Studio から実行すると下の画像のようにブラウザ上に swagger/index.html というページが表示され、そこから Web API のアクションメソッドを要求して応答を調べることができます。(追記: 2024/11/25 時点で、ターゲットフレームワーク .NET 9.0 で作成した Web API プロジェクトには Swagger は含まれません)

Swagger

Swagger を使って (1) ファイルをアップロードする方法、及び (2) ベアラトークンを要求ヘッダに含めて送信する方法を調べましたので、備忘録として残しておくことにしました。

(1) と (2) を一つの記事に書くと長くなりすぎるので、この記事では (1) を書いて、(2) は別の記事に「その2」として書くことにします。

(1) ファイルアップロード

アクションメソッドの引数に IFormFile 型の変数を含めれば、Swagger が自動的にそれを検出してアップロードするファイルの選択が可能になり、Swagger から Web API にファイルをアップロードできるようになります。

Swagger にファイル選択のための画面を表示するには、まず、[Try it out]ボタンをクリックします。

[Try it out]ボタンをクリック

[Try it out]ボタンをクリックすると、下のように、ブラウザが html の <input type="file" > を表示した時と同様なファイル選択を行うための画面が表示されます。

アップロードするファイルの選択

その画面でアップロードするファイルを選択したら、[Execute]ボタンをクリックすれば選択したファイルは multipart/form-data 形式でサーバーに送信され、アクションメソッドの引数に渡されます。

下の Fiddler による要求のキャプチャ画像を見てください。

Fiddler による要求のキャプチャ画像

アクションメソッドへの引数に IFormFile 型の変数を含める方法ですが、下のコード例の SampleA メソッドのように直接含めても、SampleB メソッドのようにモデル経由で含めても、Swagger が自動的にそれを検出してくれます。上の画像は SampleA のものですが、SampleB でも同様になります。

using Microsoft.AspNetCore.Mvc;

namespace WebApi2.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class UploadController : ControllerBase
    {
        [HttpPost("SampleA")]
        public IActionResult SampleA(IFormFile? postedFile, 
                                     [FromForm] string? customField)
        {
            if (postedFile == null || postedFile.Length == 0)
            {
                return Content("ファイルを受信できませんでした");
            }

            if (customField == null)
            {
                return Content("customField を受信できませんでした");
            }

            return Content($"ファイル: {postedFile.FileName}, " +
                $"customField: {customField} 受信");
        }

        [HttpPost("SampleB")]
        public IActionResult SampleB([FromForm] UploadModels model)
        {
            if (model.PostedFile == null || model.PostedFile.Length == 0)
            {
                return Content("ファイルを受信できませんでした");
            }

            if (model.CustomField == null)
            {
                return Content("customField を受信できませんでした");
            }

            return Content($"ファイル: {model.PostedFile.FileName}, " +
                $"customField: {model.CustomField} 受信");
        }
    }

    public class UploadModels
    {
        public string? CustomField { get; set; }
        public IFormFile? PostedFile { get; set; }
    }
}

この記事の本題とは関係ない話ですが、上のサンプルコードで、SampleA の引数の型、SampleB が使う UploadModels のプロパティの型が null 許容型となっているのには理由がありますのでそれも書いておきます。

ユーザーがファイルを選択しないまま / customFiled が空白のまま[Execute]ボタンをクリックすると、null をアクションメソッドの引数にバインドしようとします。なので、引数を null 許容型にしておかないと、バインド時にエラーとなって HTTP 400 Bad Request が応答として返され、アクションメソッドは実行されません。

上のコード例で言うと、if 文以下は実行されないので期待した応答("customField を受信できませんでした" とか "ファイルを受信できませんでした")は返ってこないということになります。

Tags: , , , ,

DevelopmentTools

Controller で作成された匿名型は View でアクセス不可

by WebSurfer 11. October 2024 15:20

.NET Framework 版の ASP.NET MVC アプリでは、Controller で作成されて View に渡された匿名型のオブジェクトには View 内部ではアクセスできず、 アクセスしようとすると以下の画像のように RuntimeBinderException がスローされるということを書きます。

RuntimeBinderException

理由は、Microsoft のドキュメント「匿名型」に書いてあるように、「匿名型のアクセシビリティ レベルは internal であるため」です。internal 型またはメンバは、同じアセンブリのファイル内でのみしかアクセスできません。

.NET Framework 版の MVC アプリでは、Controller など拡張子が cs のファイルは Visual Studio で単一アセンブリにコンパイルされ、bin フォルダに配置されます。

一方、View (.cshtml) は、デフォルトではランタイムコンパイルとなり、アプリをデプロイした後サーバーで動的にアセンブリにコンパイルされ、サーバーの Temporary ASP.NET Files フォルダに保存されます。

という訳で、Controller と View とは違うアセンブリになるため、Controller で作成された匿名クラスのプロパティは View では見えず、アクセスしようとすると上の画像のように RuntimeBinderException がスローされます。

ただし、ASP.NET Core アプリの場合は、Controller と View はデフォルトで単一アセンブリにコンパイルされるので、上に書いたような問題は起きません。(ASP.NET Core のコンパイルについて、詳しくは Microsoft のドキュメント「ASP.NET Core での Razor ファイルのコンパイル」を見てください)

上の画像を表示した MVC アプリの Controler と View のコードを以下に載せておきます。Visual Studio 2022 のテンプレートを使って作成した .NET Framework 4.8 の MVC5 アプリです。

Controller

using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Net;
using System.Web.Mvc;
using Mvc5App2.Models;

namespace Mvc5App2.Controllers
{
    public class ProductsController : Controller
    {
        private NORTHWINDEntities db = new NORTHWINDEntities();


        public async Task<ActionResult> Test()
        {
            var products = db.Products
                .Select(p => new
                {
                    Id = p.ProductID,
                    Name = p.ProductName,
                    Price = p.UnitPrice
                });

            ViewBag.List = await products.ToListAsync();

            return View();
        }
    }
}

View

@{
    ViewBag.Title = "Test";
}

<h2>Test</h2>

<br />
<table  class="table">
    <tr>
        <th>Id</th>
        <th>Name</th>
        <th>Price</th>
    </tr>
    @foreach (var item in ViewBag.List)
    {
        <tr>
            <td>@item.Id</td>
            <td>@item.Name</td>
            <td>@item.Price</td>
        </tr>
    }
</table>

解決策は、匿名型を使うのは止めて、以下のようなカスタムクラスを定義し、

public class DTO
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal? Price { get; set; }
}

以下のように List<DTO> 型のデータを生成して View に渡すことです。

public async Task<ActionResult> Test()
{
    var products = db.Products
        .Select(p => new DTO  // List<DTO> を生成
        {
            Id = p.ProductID,
            Name = p.ProductName,
            Price = p.UnitPrice
        });

    ViewBag.List = await products.ToListAsync();

    return View();
}

なお、上にも書きましたように、ASP.NET Core アプリの場合は、Controller と View はデフォルトで同じアセンブリにコンパイルされるので、上に書いた問題は起きません。

なので、匿名型を使っても以下の画像の通り期待した結果が得られます。

ASP.NET Core での結果

Tags: , ,

MVC

FriendlyUrls を有効にしておくとページの静的メソッドが呼び出せない

by WebSurfer 27. September 2024 13:50

Visual Studio 2019 / 2022 のテンプレートを使って作成する Web Forms アプリのプロジェクトでは、Microsoft.AspNet.FriendlyUrls という NuGet パッケージがインストールされ、デフォルトで有効になるように設定されていますが、デフォルトの設定のままではクライアントスクリプトでページの静的メソッドが呼び出せないという話を書きます。

FriendlyUrls NuGet パッケージ

いろいろ説明すると長くなるのでまず最初に解決策を書いておきます。

自動生成されて App_Start フォルダに配置されている RouteConfig.cs で、以下のコードの通りリダイレクトモードが Permanent に設定されていますが、解決策はそれを Off に変更することです。

using Microsoft.AspNet.FriendlyUrls;
using System.Web.Routing;

namespace WebForms3
{
    public static class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            var settings = new FriendlyUrlSettings();

            // デフォルトは Permanent
            // settings.AutoRedirectMode = RedirectMode.Permanent;

            // Off に変更
            settings.AutoRedirectMode = RedirectMode.Off;

            routes.EnableFriendlyUrls(settings);
        }
    }
}

Off にするのは SEO 的に好ましくないかもしれません。しかし、FriendlyUrls を使って拡張子 .aspx なしの url で呼び出せるようにしたい、かつクライアントスクリプトでページの静的メソッドを呼び出せるようにしたいのであれば、他に方法はなさそうです。


以下にどういうメカニズムになっているかなどの説明を書いておきます。興味があれば読んでください。

(1) FriendlyUrls とは何か

ASP.NET Web Forms アプリの場合、ブラウザからページを呼び出す際、url に https://.../default.aspx というように目的のページのファイル名を拡張子を付けて指定する必要があります。

FriendlyUrls を利用すると、一番最初にブラウザから拡張子 .aspx 付きで要求した場合は、デフォルトのリダイレクトモードの設定 Permanent では 301 Moved Permanently 応答が返ってきて、拡張子 .aspx 無しの url にリダイレクトされます。

下の Fiddler の画像の #9 を見てください。青枠で示したように Default.aspx を要求すると、赤枠で示したように 301 Moved Permanently 応答が返ってきて Default にリダイレクト指示が出ています。

FriendlyUrls NuGet パッケージ

ブラウザはリダイレクト指示を受けて Default を要求します。上の Fiddler の画像の #14 がそれです。Default を要求されたサーバー側では、FriendlyUrls がそれを Default.aspx にルーティングしてくれるので、Default.aspx が応答として返されます。

リダイレクトの際の HTTP 応答は 301 Moved Permanently なので、ブラウザは Default.aspx ⇒ Default に恒久的に移ったという情報をキャッシュします。なので2 回目以降は、たとえブラウザのアドレスバーに Default.aspx と入力して要求をかけても、ブラウザからは Default という url で要求が出ます。

(2) ページの静的メソッドとは何か

ページの静的メソッドとは、ASP.NET Web Forms のページ (.aspx.cs) に WebMethodAttribute 属性を付与して配置した public static メソッドで、AJAX を利用してクライアントスクリプトから呼び出すことができるものです。

jQuery ajax や fetch 等を使ったクライアントスクリプトで JSON 形式のデータを送信して JSON 形式の応答を受けるという Web API 的な使い方ができるので、ASP.NET Web Forms アプリでは利用価値は高いと思います。

詳しくは、先の記事「ASP.NET AJAX でページの静的メソッド呼び出し」を見てください。

(3) FriendlyUrls のリダイレクトモード

リダイレクトモードは Permanent, Temporary, Off のいずれかに設定でき、設定によって以下のように動きが異なります。

  • Permanent の場合、ブラウザから Default.aspx というように拡張子 .aspx 付きで要求を受けたときは 301 Moved Permanently 応答が返され、Default にリダイレクトされるようになっています。
  • Temporary の場合は 302 Found 応答でリダイレクトされる以外は Permanent と同じになります。
  • Off の場合はリダイレクトされません。Default.aspx で要求を受けるとそのまま Default.aspx が返されます。Default で要求を受けると、FriendlyUrls によって Default.aspx にルーティングされ、Default.aspx が返されます。

(4) リダイレクトモードが Permanent / Temporary の時の動作

リダイレクトモードが Permanent で、以下のように jQuery ajax を使って WebForm1.aspx ページにある静的メソッド MyWebMethod を呼び出したとします。

function CallWebMthod(productId) {
    $.ajax({
        type: "POST",
        url: "/WebForm1.aspx/MyWebMethod",
        contentType: "application/json; charset=utf-8",
        data: `{ "id": ${productId} }`
    }).done(response => {
        
        // ・・・中略・・・

    });
}

その応答を Fiddler で応答を見ると以下の通りとなっていました。これはリダイレクトモードが Temporary の場合も同じです。

Fiddler で見た応答

要求が WebForm1.aspx と拡張子 .aspx が付いているのでリダイレクト応答を返す動きになるはずですが、要求を受けてリダイレクト応答を返すまでのプロセスのどこかで 401 Unauthorized となり (赤枠部分)、認証プロセスに入ろうとしたが失敗したのでその旨応答を返した (青枠部分) ということのように見えます。プロセスのどこでどういう理由でそうなるかなど詳しいことは分かりません。

ちなみに、上の画像の青枠部分の JSON 文字列は、jQuery ajax のコードの変数 response に、JavaScript オブジェクトにデシリアライズした形で受け取ることができます。

(5) リダイレクトモードが Off の時の動作

FriendlyUrls が有効なまま静的メソッド MyWebMethod を呼び出せるようにするには、上に書いたように RoutConfig.cs のコードのリダイレクトモードを Off に変更してやります。

そうすれば上の jQuery のコードの /WebForm1.aspx/MyWebMethod のように拡張子 .aspx が付いた url の要求を受けてもリダイレクト応答を返すためのプロセスが動くことはなく、即 WebForm1.aspx ページの静的メソッド MyWebMethod が呼ばれて期待通り MyWebMethod の応答が返ってきます。

注: url: "/WebForm1/MyWebMethod" とすると (.aspx 拡張子を削除すると)、リダイレクトモード Permanent, Temporary, Off いずれの設定でも WebForm1.aspx ページ本体が応答として返ってきます。それは FriendlyUrls により WebForm1 は WebForm1.aspx にルーティングされ、MyWebMethod はサーバーに渡すパラメータと解釈され、WebForm1.aspx ページ本体が返すべきリソースと判断されるからだと思います。

(6) 検証に使ったサンプルコード

この記事を書くにあたって検証用に作成した WebForm1.aspx.cs、WebForm1.aspx のコードを載せておきます。以下の画像が実行結果で、LinkButton クリックで jQuery ajax を使って静的メソッド MyWebMethod を呼び出し、その応答を GridView の下に表示しています。

WebMethod の呼び出し

WebForm1.aspx.cs

using System;
using System.Web.Services;

namespace WebForms3
{
    public partial class WebForm1 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        [WebMethod]
        public static string MyWebMethod(int id)
        {
            return $"WebMethod called with id={id}";
        }

        protected string SetOnClientClick(int id)
        {
            return $"CallWebMthod({id}); return false;";
        }
    }
}

WebForm1.aspx

<%@ Page Language="C#" AutoEventWireup="true"
    CodeBehind="WebForm1.aspx.cs"
    Inherits="WebForms3.WebForm1" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script src="Scripts/jquery-3.7.0.js"></script>
    <script type="text/javascript">
        function CallWebMthod(productId) {
            $.ajax({
                type: "POST",
                url: "/WebForm1.aspx/MyWebMethod",
                contentType: "application/json; charset=utf-8",
                data: `{ "id": ${productId} }`
            }).done(response => {
                $("#result").empty;
                // .NET 3.5 で追加された d パラメータの処置。
                if (response.hasOwnProperty('d')) {
                    $("#result").text(response.d);
                }

                // リダイレクトモードが Parmanent の時、上の url
                // を要求するとサーバー内で 401 エラーとなって下
                // の応答が返ってくる:
                // {"Message":"認証に失敗しました。" ...}
                if (response.hasOwnProperty('Message')) {
                    $("#result").text(response.Message);
                }
            });
        }
    </script>
</head>
<body>
    <form id="form1" runat="server">
        <asp:SqlDataSource ID="SqlDataSource1"
            runat="server"
            ConnectionString="<%$ ConnectionStrings:NORTHWINDConnectionString %>"
            SelectCommand="SELECT TOP(10) [ProductID], [ProductName] FROM [Products]">
        </asp:SqlDataSource>

        <asp:GridView ID="GridView1"
            runat="server"
            AutoGenerateColumns="False"
            DataKeyNames="ProductID"
            DataSourceID="SqlDataSource1">
            <Columns>
                <asp:BoundField DataField="ProductID"
                    HeaderText="ProductID"
                    InsertVisible="False"
                    ReadOnly="True"
                    SortExpression="ProductID" />
                <asp:BoundField DataField="ProductName"
                    HeaderText="ProductName"
                    SortExpression="ProductName" />
                <asp:TemplateField HeaderText="Click to call WebMethod">
                    <ItemTemplate>
                        <asp:LinkButton ID="LinkButton1"
                            runat="server"
                            OnClientClick='<%# SetOnClientClick((int)Eval("ProductID")) %>'>
                    LinkButton
                        </asp:LinkButton>
                    </ItemTemplate>
                </asp:TemplateField>
            </Columns>
        </asp:GridView>

        <div id="result"></div>
    </form>
</body>
</html>

Tags: , , , ,

ASP.NET

About this blog

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

Calendar

<<  April 2025  >>
MoTuWeThFrSaSu
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar