WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ASP.NET Core 3.1 Web API の .NET 版との違い

by WebSurfer 2020年5月15日 12:45

ASP.NET Web API の Core 3.1 版のアプリが .NET Framework 版のアプリと異なる点を備忘録としてまとめておきます。あくまで自分的に大きな違いがあると思っている点のみです。

  1. Controller は ControllerBase クラスを継承する。(.NET Framework の ApiController ではなく)
  2. Controller に ApiControllerAttribute 属性を付与する。
  3. ルーティングは Controller に RouteAttibute 属性を付与して設定する。(.NET Framework 版プロジェクトで生成される "api/{controller}/{id}" というようなルーティング設定はないようです)
  4. アクションメソッドに [HttpGet], [HttpGet("{id}")], [HttpPost] 等の属性を付与する。(付与しないと HTTP 動詞でどれを呼ぶかの判別ができないようです)
  5. アクションメソッドの引数、例えば [FromBody] string name の name に文字列をバインドする場合、.NET Framework 版では「=文字列」という文字列をコンテンツとして送信すれば可能でしたが、Core 版ではそれができない。
  6. .NET のクラスのプロパティ名の最初の文字は大文字にするのが普通ですが、大文字にしておいてもシリアライズされた結果の応答の JSON 文字列のキー名の最初の文字が小文字になる。

詳しくは Microsoft のドキュメント「ASP.NET Core を使って Web API を作成する」に書いてありますのでそちらを見てください。

コード例を示すと以下のようになります。先の記事「ASP.NET Web API のバインディング」に .NET Framework 版のコードがありますので、それと比較してみてください。

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using WebAPI.Models;

namespace WebAPI.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private List<Hero> heroes = new List<Hero> {
              new Hero {Id = 1, Name = "スーパーマン"},
              new Hero {Id = 2, Name = "バットマン"},
              new Hero {Id = 3, Name = "ウェブマトリクスマン"},
              new Hero {Id = 4, Name = "チャッカマン"},
              new Hero {Id = 5, Name = "スライムマン"}
          };

        // GET api/values (Read...すべてのレコードを取得)
        [HttpGet]
        public List<Hero> Get()
        {
            return heroes;
        }

        // GET api/values/5 (Read...id 指定のレコード取得)
        [HttpGet("{id}")]
        public Hero Get(int id)
        {
            return heroes[id - 1];
        }

        // POST api/values  (Create...レコード追加)
        [HttpPost]
        public List<Hero> Post(Hero postedHero)
        {
            heroes.Add(postedHero);
            return heroes;
        }

        // PUT api/values/5 (Update...id 指定のレコード更新)
        [HttpPut("{id}")]
        public List<Hero> Put(int id, Hero postedHero)
        {
            heroes[id - 1].Name = postedHero.Name;
            return heroes;
        }

        // DELETE api/values/5 (Delete...id 指定のレコード削除)
        [HttpDelete("{id}")]
        public List<Hero> Delete(int id)
        {
            heroes.RemoveAt(id - 1);
            return heroes;
        }
    }
}

上に列挙した 5 番目の項目に書いた件を説明します。上のコード例の Put の場合を見てください。.NET Framework 版の記事ではアクションメソッドの引数を Put(int id, [FromBody] string name) とし、それを呼び出す jQuery ajax のコードでは data: encodeURI("=ガッチャマン") として無理やり(?)引数 name にバインドできていました。Core 版ではそれはできないようで、以下のコードのようにしないとダメでした。

// .NET Framework ベースとは異なり、以下のようにしないとダメ
function apiHeroesPut5() {
    var j = { Id: 5, Name: "ガッチャマン" };
    var jsonString = JSON.stringify(j);
    $.ajax({
        type: "PUT",
        url: "/values/5",
        data: jsonString,
        contentType: "application/json; charset=utf-8",
        success: function (data) {
            $('#heroes').empty();
            $.each(data, function (key, val) {
                var str = val.id + ': ' + val.name;
                $('<li/>', { html: str }).appendTo($('#heroes'));
            });
        },
        error: function (jqXHR, textStatus, errorThrown) {
            $('#heroes').empty();
            $('#heroes').text('textStatus: ' + textStatus +
                ', errorThrown: ' + errorThrown);
        }
    });
}

理由は不明です。バインディング ソース パラメーター推論を読むと分かるかも。手抜きですみません。

最後に、上に列挙した 6 番目の項目に書いた、シリアライズされた結果の応答の JSON 文字列のキー名の最初の文字が小文字になる件を説明します。

上の 1 番目のコード例を見てください。Hero クラスのプロパティ名 Id, Name の最初の文字は大文字にしています。しかし、Web API が Hero オブジェクトを JSON 文字列にシリアライズすると何故かキー名は id, name というように小文字になります。

これは Camel Casing と言うそうです。Core 3.0 以降の MVC でも同様で、MVC や Web API のフレームワークでデフォルトでそのようにする設定がされているようです(コンソールアプリで試したときは Camel Casing にはなりませんでしたので、JsonSerializer クラス自体の設定ではなさそうです)。Camel Casing になるのを回避する方法はあります。詳しくは別の記事「JsonSerializer の Camel Casing」を見てください。

2 番目のコード例の success: function (data) の data には応答の JSON 文字列をデシリアライズした JavaScript オブジェクトが渡されますが、キー名から値を取得する際、最初の文字を小文字にしないと値を取得できず undefined になってしまいます。(なので上のコードでは val.id, val.name としています)

何を隠そう自分はここにハマって 1 ~ 2 時間悩みました。(笑)

Tags: ,

CORE

ASP.NET Identity で MySQL 利用 (CORE)

by WebSurfer 2020年5月14日 17:33

Core 3.1 ベース(.NET Framework ではありません)の ASP.NET MVC アプリで認証に ASP.NET Core Identity を用い、ユーザー情報のストアに MySQL を利用するにはどうするかということを書きます。

Visual Studio 2022 + .NET 6.0 の ASP.NET Core Identity の場合は「.NET 6.0 ASP.NET Identity に MySQL 使用 (CORE)」を見てください。

.NET Framework 版の ASP.NET Identity の場合は「ASP.NET Identity で MySQL 利用 (.NET 版)」を見てください。

ネットの情報ではサードパーティ製の Pomelo.EntityFrameworkCore.MySql を使ったという記事を目にしますが、ここでは NuGet でインストールできる Oracle 製の MySql.Data.EntityFrameworkCore v8.0.20 を使ってみました。

Oracle 製の MySql.Data.EntityFrameworkCore は Core に対応してないので下記ステップ「(6) プロジェクトのビルド」でエラーになったという話を聞きましたが、最新版ではそのあたりは対応されているようです。

ただし、何も問題がなかったわけではなく、最後に Update-Database でデータベース / テーブルを生成するところで "Specified key was too long; max key length is 3072 bytes" というエラーが出ます・・・が、解決は可能でした。詳しくは下のステップ「(8) Update-Database の実行」を見てください。

(1) プロジェクトの作成

Visual Studio 2019 のテンプレートを利用して ASP.NET Core 3.1 MVC アプリを作成します。認証は「認証なし」のままとしておきます。

新しい ASP.NET Core Web アプリケーションの作成

認証に「個別のユーザーアカウント」を選んでプロジェクト作成時点から ASP.NET Identity を実装しても、それを MySQL を利用するように変更できると思います。しかし、そうすると Login, Register 他の認証関係の機能は Razor Class Library (RCL) として提供され、ソースコードはプロジェクトには含まれません。

認証関係のソースコードを見たい / 修正したいことがあるでしょうから(少なくとも登録、ログインページは書き換えたくなるはず)、まず「認証なし」で作って下のステップ (2) に述べるようにスキャフォールディング機能を利用して ASP.NET Identity を実装するのがよさそうです。(「個別のユーザーアカウント」で作って、特定のページをオーバーライドすることもできるそうですが)

(2) ASP.NET Core Identity の実装

Microsoft のドキュメント Scaffold identity into an MVC project without existing authorization を参考に、スキャフォールディング機能を利用して ASP.NET Identity を実装します。

ドキュメントの通り、ソリューションエクスプローラーでプロジェクトを右クリック⇒[追加(D)]⇒[新しいスキャフォールディングアイテム(F)...]で下の画像のダイアログが開きます。

新規スキャフォールディングアイテムの追加

上のダイアログで[インストール済み]の項目から[ID]を選択し、[追加]をクリックすると以下の画像の「ID の追加」ダイアログが表示されます。

ID の追加

レイアウトページ欄にはステップ (1) で生成したプロジェクトのレイアウトページ ~/Views/Shared/_Layout.cshtml を設定します。

ソースコードが必要なファイルに[オーバーライドするファイルの選択]でチェックを入れます。(上の画像では[すべてのファイルをオーバーライド]にチェックを入れて全てのファイルを選択しています。どれを選んだらよいか分からなかったので)

[データコンテキストクラス(D)]には、+ をクリックすると表示されるデフォルトをそのまま設定しました。必要なら任意の名前に変更できます。デフォルトでは xxxxxContext の xxxxx がプロジェクト名になり、プロジェクトルート下に Areas/Identity/Data というフォルダが作られ、その中にコンテキストクラスを含むファイル xxxxxContext.cs が生成されます。

[ユーザークラス(U)]にも同様に、+ をクリックすると表示されるデフォルトをそのまま設定しました。これも任意の名前に変更できます。デフォルトでは IdentityUser を継承した xxxxxUser クラスが生成され、xxxxx がプロジェクト名になります。

[ユーザークラス(U)]に何も設定せず空白のままにしておくと IdentityUser がそのまま使われます。基本的な機能はそれで問題ないですが、もし、将来プロファイル情報を追加するようなことがあると困ることになりますので、設定しておいた方がよさそうです。

その他、NuGet での Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore の追加、Razor ページを有効にするための Startup.cs へのコードの追加を忘れないようにしてください。詳しくは上に紹介した Microsoft のドキュメントを見てください。

(3) MySql.Data.EntityFrameworkCore

NuGet で MySql.Data.EntityFrameworkCore をインストールします。この記事を書いた時点では v8.0.20 しか見つからなかったのでそれをインストールしました。

2021/11/26 追記: MySql.Data.EntityFrameworkCore はいつの間にか非推奨になってました。 代替えパッケージが MySql.EntityFrameworkCore とのことです。それを使って同様なことができるかは未検証・未確認です。

MySql.Data.EntityFrameworkCore

関連するパッケージの更新・インストールが自動的に行われます。その内容は以下の画像を見てください。

変更のプレビュー

(4) 接続文字列の変更

テンプレートで自動生成された接続文字列は appsetteins.json にありますが、それは LocalDB を利用するように設定されていますので MySQL に接続するように変更します。以下のような感じです。

接続文字列の変更

例えば、上の画像の database=coreidentity というようにデータベース名を指定すると、Entity Framework Code First の機能を使って coreidentity という名前のデータベースを新たに生成し、そこに必要なテーブルを生成してくれます。(データベース名は任意に設定できます。この記事で coreidentity としたのは単なる例です)

(5) IdentityHostingStartup.cs の修正

自動生成されれた Areas/Identity/IdentityHostingStartup.cs ファイルで、サービス登録のコードが SQL Server を使うように設定されていますが、これを MySQL を使うように変更します。以下のような感じです。

サービス登録の変更

なお、IdentityHostingStartup.cs ファイルは、上のステップ (2) のようにスキャフォールディング機能を利用して ASP.NET Identity を実装した場合に生成されるものです。プロジェクト作成の時点で認証を「個別のユーザーアカウント」とした場合は、サービス登録のコードは Startup.cs ファイルに含まれますので注意してください。

(6) プロジェクトのビルド

ここで一旦プロジェクトをビルドしてみました。以下の通り成功します。

プロジェクトのビルド

参考にさせていただいた記事「ASP.NET CoreでMySQLを利用する」ではここでエラーとなったそうです。

その記事では Core のバージョンは 2.0、MySql.Data.EntityFrameworkCore のバージョンは 8.0.6 だったそうです。この記事では Core は 3.1、MySql.Data.EntityFrameworkCore は 8.0.20 です。その差があるのでしょうか。

(7) Add-Migration の実行

パッケージマネージャーコンソールから Add-Migration CreateIdentitySchema を実行します(CreateIdentitySchema という名前は任意です。また、Core では Enable-Migration は不要になったそうです)。

Add-Migration を実行

Migrations フォルダには CreateIdentitySchema.cs ファイルが生成されます。ざっと見たところ内容は正しそうです。(ツールのバージョンが 3.1.2 でランタイムのバージョン 3.1.4 より古いという警告は出ていますが Migration には影響ないようです)

(8) Update-Database の実行

次に Update-Database を実行し、Entity Framework Code First の機能を利用して MySQL にデータベース / テーブルの生成を試みます・・・が、以下の画像の通りダメでした。

Update-Database 実行結果

メッセージを見ると AspNetUserRoles というテーブルを生成する際に "Specified key was too long; max key length is 3072 bytes" という制約のためエラーになったということのようです。

以下、少々話が長くなりますが、なぜそういうエラーになるかと、その解決方法を書きます。

データベース / テーブルは CreateIdentitySchema.cs ファイルのコードに基づいて生成されるのですが、主キーは全て、

UserId = table.Column<string>(nullable: false)

というというような maxLength が指定されない設定となります。その場合、上の画像にある通り、テーブルを生成する create 句では主キーは varchar(767) に設定されます。

varchar(767) の 767 という数字は MySQL の主キーに許容される最大バイト数で、それゆえ主キーに maxLength が指定されないと varchar(767) になるのだと思われます。

767 バイトという制限は、「InnoDBインデックスの最大キー長について」という記事によると、ある条件で 3,072 バイトまで拡張できるそうで、MySQL 5.7 以降がその条件に当てはまるそうです。この記事では MySQL 8.x を使っているのでエラーメッセージにある通り 3,072 バイトが上限になっているのだと思います。

何故 nvarchar ではなくて varchar になるのか不思議でしたが、MySQL のドキュメント 10.3.7 The National Character Set によると、"MySQL uses utf8 as this predefined character set" なのでどちらでも同じなのだそうです。ちなみに create 句で nvarchar と指定しても、生成されるのは varchar になるそうです。

この記事で使用している MySQL 8.0.19 で使用されている文字コードを確認してみましたが、以下のようになっていました。

使用されている文字コード

utf8mb4 というのは、10.1.10.7 utf8mb4 文字セット (4 バイトの UTF-8 Unicode エンコーディング) によると、"utf8 という名前の文字セットは、文字あたり最大 3 バイトを使用し、BMP 文字だけを含みます。utf8mb4 文字セットは、文字ごとに最大 4 バイトを使用し、補助文字をサポートします" ということだそうです。

utf8mb4 を使用していますので、主キーに設定された varchar(767) は 767 x 4 = 3,068 バイトになります。制限の 3,072 より小さいので単独の主キーなら varchar(767) で問題ないはずです。

しかし、ASP.NET Core Identity が使うテーブルには連結主キーが設定されるものがあり、それが最大 3,072 バイトの制約を超えるため "Specified key was too long; max key length is 3072 bytes" というエラーになったということのようです。

連結主キーが設定されるテーブルは AspNetUserRoles と AspNetUserTokens です。

それらのテーブルの主キーが varchar(767) とならないよう、CreateIdentitySchema.cs ファイルの当該コードに制限 maxLength: 128 を設定してやります。以下の画像の赤枠部分を見てください。

maxLength: 128 を設定

Id フィールドには d85de5ae-a926-45cd-831a-4835034f6b79 というように書式指定された Guid の文字列が設定されますので、varchar(128) で十分なはずです(少なくとも現時点では)。

上の画像のように maxLength: 128 を設定後 Update-Database を実行すれば MySQL に ASP.NET Core Identity に必要なデータベースとテーブルが生成されます。

その後で Visual Studio から MVC アプリを動かしてユーザー登録できます。登録したユーザーはデーターベースに反映されます。もちろん登録した ID とパスワードでアプリにログインできるようになります。

Tags: , , ,

CORE

OData で ODataQueryOptions 使用

by WebSurfer 2020年4月12日 17:11

Open Data Protocol (OData) を利用した ASP.NET Web API アプリで、ODataQueryOptions<TEntity> を引数に持つアクションメソッドを作る話を備忘録として書いておきます。

Open Data Protocol (OData)

OData を利用した ASP.NET Web API アプリの作り方は、.NET Framework ベースの場合は Microsoft のドキュメント Create an OData v4 Endpoint Using ASP.NET Web API に、Core ベースの場合は Experimenting with OData in ASP.NET Core 3.1 にあります。

(ちなみに、OData を利用するメリットですが、Creating an OData v4 API with ASP.NET Core 2.0 に書いてありますように、"the payload size and the querying of the data from the database" ということだそうです。確かにその通りだと思いました。OData に興味があれば読んでみることをお勧めします)

上に紹介した記事には書かれていませんが、Microsoft のドキュメント Invoking Query Options Directly によると、Advanced Scenarios(多分、より細かなコントロールが可能ということだと思います)として、ODataQueryOptions<TEntity> を引数に持つアクションメソッドを利用するオプションがあるそうです。

(注:記事に書いてある [Queryable] 属性は OData v3 の場合で、OData v4 または Core の場合は [EnableQuery] 属性になるようです)

ハマったのは、上の記事のコード results as IQueryable<Product> が null になってしまう場合があるということです。例えばこの記事では $select=name とした場合がそれです。

普通にアクションメソッドに [EnableQuery] 属性を付与する場合は以下のようにします。それだけで OData が有効になり、クエリ文字列に $select とか $top などを指定すればそれに従って JSON 文字列が返ってきます。

using System.Linq;
using Microsoft.AspNetCore.Mvc;
using ODataCore3API.DAL;
using ODataCore3API.Models;

using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Query;

namespace ODataCore3API.Controllers
{
    [Produces("application/json")]
    public class ProductController : ODataController
    {
        private readonly ProductContext _context;

        public ProductController(ProductContext context)
        {
            _context = context;
        }

        [HttpGet]
        [EnableQuery]
        public IQueryable<Product> GetProduct()
        {
            return _context.Product.AsQueryable();
        }
    }
}

クエリ文字列を $select=name としても、期待通りの JSON 文字列が、この記事の上にある画像の通り返ってきます。アクションメソッドの戻り値を IQueryable<Product> としていますが、返ってくる JSON 文字列は IQueryable<Product> をデシリアライズしたものではないことに注目してください。OData がどこかでその違いを吸収しているようです。

[EnableQuery] 属性を削除して ODataQueryOptions<TEntity> を引数に持つアクションメソッドを使う場合、かつ $select=name でも対応できるようにするには以下のようにします。コメントに注目してください。

// ODataQueryOptions<Product> を引数に持つバージョン
[HttpGet]
public IActionResult GetProduct(
                ODataQueryOptions<Product> opts)
{
    IQueryable results = 
        opts.ApplyTo(_context.Product.AsQueryable());

    // 以下はダメ。$select=name としたとき null になる
    // return results as IQueryable<Product>;

    // アクションメソッドの戻り値を IActionResult とし
    // 以下のようにすれば $select=name でも OK
    return Ok(results);
}

上のコードは OData Query Options という記事を参考にしました。$select=name とした場合でも、この記事の上にある画像のとおり期待した JSON 文字列が返ってきます。

results as IQueryable<Product> が null になるのは考えてみれば当たり前だったのですが、そこに気が付かず半日ぐらいハマってしまった次第です。(笑)

Tags: , , ,

Web API

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar