WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

MVC に JSON をバインドするには [FromBody] が必要 (CORE)

by WebSurfer 12. April 2021 15:14

ASP.NET Core MVC のアクションメソッドに JSON 文字列をボディに含めて POST 送信する場合、アクションメソッドの引数に [FromBody] 属性を付与しないと、なぜかモデルバインディングに失敗するという話を書きます。(Core 3.1 と 5.0 で確認。1.x, 2.x. 6.0 は未検証・未確認です)

モデルバインディングに失敗

上の画像がその例で、アクションメソッドの引数には [FromBody] を付与していません。RequestJson は生成されて引数の requestJson に渡されますが、各プロパティには送信されてきた JSON 文字列の値が代入されていません。(各プロパティの型の既定値のまま)

引数の RequestJson クラスの定義は以下の通りです。

namespace MvcCore5App2.Models
{
    public class RequestJson
    {
        public string Method { get; set; }
        public bool MuteHttpExceptions { get; set; }
        public string ContentType { get; set; }
        public string Payload { get; set; }
    }
}

JSON 文字列の送信は以下の画像のように Fiddler の Composer を使いました。(注: RequestJson クラスの各プロパティ名と JSON の {"name";"value"} の "name" の大文字小文字が違いますがそこは関係ないです。先の記事「JsonSerializer の Camel Casing (CORE)」に書いたように、Core v3.x 以降で使われる System.Text.Json 名前空間の JsonSerializer クラスは、デフォルトではデシリアライズする際の大文字小文字を区別するのですが、アプリ内で区別しない設定にしているようです)

Fiddler の Composer で送信

.NET Framework 版の MVC5 では [FromBody] を付与しなくてもモデルバインディングされたのですが・・・

何故か ASP.NET Core MVC では [FromBody] を付与しないと、送信されてきたクエリ文字列、フォームボディ、ルートパラメータ、クッキー、要求ヘッダ、ファイルなどのどれから取得するかが分からず、モデルバインディング (JSON 文字列から値を取得して各プロパティに代入) できないということのようです。(想像です)

以下の画像のように、アクションメソッドの引数に [FromBody] を付与すればモデルバインディングに成功します。送信した JSON 文字列は上の Fiddler の Composer 画像のものと同じですが、JSON の value が RequestJson クラスの各プロパティに代入されてから引数の requestJson に渡されています。

モデルバインディングに成功

上にも書きましたが、.NET Framework 版の MVC では [FromBody] 無しでも問題なくモデルバインディングされます。Core 版 MVC でも同じだろうと思っているとハマります。何を隠そう自分もハマって 2 時間ぐらい悩みました。

さらに、Core 版でも Web API の場合は [FromBody] 無しでも問題なくモデルバインディングされます。以下の画像を見てください。

Web API の場合

Web API では、コンプレックス型(この記事の例では RequestJson クラス)の場合、デフォルトではボディからパラメータを取得するということになるそうで、それゆえ [FromBody] 無しでも問題ないということかもしれません(想像です)。

Web API にせよ MVC5 にせよ、JSON 文字列をボディに含めて POST 送信するなら、それを受けるアクションメソッドの引数には [FromBody] を付与しておくというのが正解と思いました。

Tags: , , , ,

CORE

WinForms で構成情報とコンテキストの DI (CORE)

by WebSurfer 30. March 2021 15:00

.NET Core 3.1 の Windows Forms アプリで、構成情報を取得する方法、さらに DI 機能を追加して、取得した構成情報と EF Core で利用するコンテキストクラスを DI する方法を書きます。

DataGridView に結果を表示

ASP.NET Core アプリのプロジェクトを Visual Studio 2019 のテンプレートを使って作成すると、appsettings.json などの構成ファイルが自動的に生成されてプロジェクトに含まれます。さらに、構成ファイルから情報を読み込んで IConfiguration オブジェクトが生成され、構成情報を取得できるようになります。(詳しくは Microsoft のドキュメント「ASP.NET Core の構成」参照)

また、作成したプロジェクトには DI 機能も自動的に組み込まれ、生成された IConfiguration オブジェクトを DI コンテナに登録し、必要に応じて Controller や Page のコンストラクタ経由で DI できる機能が実装されます。

DI コンテナには ILogger, UserManager, EF Core で使用するコンテキストクラスなども登録でき、これらも必要に応じて Controller や Page のコンストラクタ経由で DI できます。

Windows Forms やコンソールアプリの場合はそれらの機能は Visual Studio 2019 のテンプレートを使っても実装されず、自力でコードを書いて実装する必要があります。

.NET Core 3.1 の Windows Forms アプリで appsettings.json ファイルから IConfiguration オブジェクトを作って構成情報を取得する方法、DI機能を実装して IConfiguration オブジェクトとコンテキストクラスを DI する方法を以下に書きます。

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

Visual Studio 2019 のテンプレートを利用して、ターゲットフレームワーク .NET Core 3.1 で Windows Forms アプリのプロジェクトを作成します。

VS2019 のテンプレート

ターゲットフレームワークを、この記事を書いた時点での最新 .NET 5.0 ではなく .NET Core 3.1 としたのは、3.1 が Long Term Support (LTS) 版であること、5.0 でサポートされた新機能を使用しなくても可能なことを確認するためです。

(2) NuGet パッケージのインストール

Visual Studio 2019 の[ツール(T)]⇒[NuGet パッケージマネージャ(N)]⇒[ソリューションの NuGet パッケージの管理(N)...] を開いて以下のパッケージをインストールします。

  1. Microsoft.EntityFrameworkCore.SqlServer
  2. Microsoft.EntityFrameworkCore.Tools
  3. Microsoft.Extensions.Configuration.Json
  4. Microsoft.Extensions.DependencyInjection

下の画像がインストールした結果です。各パッケージのバージョンはこの記事を書いた時点での最新です。プロジェクトのターゲットフレームワークは .NET Core 3.1 なのですが、それに合わせる必要はなかったです。

NuGet パッケージのインストール

ちなみに、上のリストの 1 は EF Core を利用し��� SQL Server にアクセスして処理を行うための機能、2 は Visual Studio の NuGet Pakage Manager Console で Add-Migration, Scaffold-DbContext などのコマンドを利用できるようにするための機能、3 は appsettings.json などの構成ファイルから情報を取得するための機能、4 は DI 機能の実装のために必要です。

(3) appsettings.json の作成

ソリューションエクスプローラーを操作してプロジェクトに json ファイルを追加します。ファイル名は任意ですが、この記事では ASP.NET アプリに合わせて "appsettings.json" にしました。内容はこの記事では以下の通り接続文字列のみとしましたが、他に任意の情報を含めることができます。

{
  "ConnectionStrings": {
    "NorthwindConnection": "Data Source=(local)\\sqlexpress; ..."
  }
}

作成したらそのプロパティの中の「出力ディレクトリにコピー」を「常にコピーする」または「新しい場合はコピーする」に設定するのを忘れないようにしてください。接続文字列のバックスラッシュ \ は \\ にエスケープする必要があるので注意してください。

appsettings.json のプロパティ設定

(4) 構成情報が取得できることを確認

上記ステップ (1) で作成した Windows Forms アプリの Form1 のコンストラクタに以下のコードを追加して、変数 connString に appsettings.json に設定した接続文字列が取得できることを確認します。

using System.Windows.Forms;
using System.IO;
using WinFormsCore3App1.Contexts;

// NuGet packages:
// Microsoft.Extensions.Configuration.Json
// Microsoft.Extensions.DependencyInjection
// Microsoft.EntityFrameworkCore.SqlServer
// Microsoft.EntityFrameworkCore.Tools

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;

namespace WinFormsCore3App1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            // Configuration の生成
            // appsettings.json に接続文字列が含まれている
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false);
            IConfiguration config = builder.Build();

            // appsettings.json から取得した接続文字列
            string connString = 
                config.GetConnectionString("NorthwindConnection");
        }
    }
}

(5) DI 機能の実装

DI 機能の実装と IConfiguration オブジェクトの DI コンテナへの登録は以下の 2 行を上のステップ (4) の Form1 のコンストラクタに追加することで可能です。

// DI コンテナ
IServiceCollection services = new ServiceCollection();

// Configuration を DI コンテナに登録
services.AddSingleton(config);

Configuration を DI できるようにするにはさらなるコードの追加が必要ですが、それについては以下にコンテキストクラスの DI 方法と共に書きます。

(6) コンテキストクラスの生成

この記事では Microsoft のサンプルデータベース Northwind の Products, Categories, Suppliers テーブルからリバースエンジニアリングでコンテキストクラスとエンティティクラスを生成して使います。

詳しくは先の記事「スキャフォールディング機能 (CORE)」のステップ (1) を見てください。

上の appsettings.json と違ってバックスラッシュ \ はエスケープする必要はないところに注意してください。エスケープして \\ としたりするとエラーになります。

成功するとコンテキストクラス NorthwindContext.cs と各テーブルのエンティティクラス Product.cs, Category.cs, Supplier.cs が指定したフォルダに生成されます。

NorthwindContext.cs ファイルの NorthwindContext クラスの引数を持たないコンストラクタと OnConfiguring メソッドはコメントアウトしてください。

(7) ProductService クラスの作成

SQL Server から EF Core を利用してデータを取得するクラスを作成します。ソリューションエクスプローラーでクラスファイルを ProductService.cs という名前で追加し、以下のコードを実装します。

コードの説明はコメントに書きましたのでそれを見てください。

using Microsoft.Extensions.Configuration;
using WinFormsCore3App1.Contexts;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;

namespace WinFormsCore3App1
{
    public class ProductService
    {
        private readonly IConfiguration _configuration;
        private readonly NorthwindContext _context;

        // コンストラクタの引数経由で Configuration と
        // NorthwindContext のインスタンスを DI する
        public ProductService(IConfiguration configuration,
                              NorthwindContext context)
        {
            this._configuration = configuration;
            this._context = context;
        }

        // DI された Configuration から接続文字列が取得できる
        // ことを確認するための検証用メソッド
        public string GetConnectionString()
        {
            return _configuration
                   .GetConnectionString("NorthwindConnection");
        }

        // DI されたコンテキスト NorthwindContext を使ってSQL
        // Server の Northwind データベースからデータを取得し
        // List<ProductItem> 型のオブジェクトとして返す。
        // それを DataGridView に表示
        public async Task<List<ProductItem>> GetListAsync()
        {
            var list = from p in _context.Products
                       join s in _context.Suppliers
                       on p.SupplierId equals s.SupplierId
                       join c in _context.Categories
                       on p.CategoryId equals c.CategoryId
                       select new ProductItem 
                       { 
                           ProductId = p.ProductId,
                           ProductName = p.ProductName,
                           Supplier = s.CompanyName,
                           Category = c.CategoryName,
                           UnitPrice = p.UnitPrice.Value
                       };

            return await list.ToListAsync();
        }
    }

    // DataGridView に渡すデータを格納する Data Transfer
    // Object クラスの定義
    public class ProductItem
    {
        public int ProductId { get; set; }

        public string ProductName { get; set; }

        public string Supplier { get; set; }

        public string Category { get; set; }

        public decimal UnitPrice { get; set; }

    }
}

(8) Form1 クラスの完成

Form1 のコンストラクタで DI コンテナに Configuration, NorthwindContext, ProductService を登録し、ServiceProvider から ProductService のインスタンスを生成する際 Configuration と NorthwindContext のインスタンスはコンストラクタ経由 DI されるように設定します。

上記ステップ (4) のコードを含めた完全なコードは以下の通りです。実行した結果がこの記事の一番上にある画像です。

using System.Windows.Forms;
using System.IO;

// NuGet packages:
// Microsoft.Extensions.Configuration.Json
// Microsoft.Extensions.DependencyInjection
// Microsoft.EntityFrameworkCore.SqlServer
// Microsoft.EntityFrameworkCore.Tools

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WinFormsCore3App1.Contexts;
using Microsoft.EntityFrameworkCore;

namespace WinFormsCore3App1
{
    public partial class Form1 : Form
    {
        // ProductService は SQL Server から EF Core を利用
        // してデータを取得するクラス
        private readonly ProductService productService;

        public Form1()
        {
            InitializeComponent();

            // Configuration の生成
            // appsettings.json に接続文字列が含まれている
            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false);
            IConfiguration config = builder.Build();

            // appsettings.json から取得した接続文字列
            string connString = 
                config.GetConnectionString("NorthwindConnection");

            // DI コンテナ
            IServiceCollection services = new ServiceCollection();

            // Configuration を DI コンテナに登録
            services.AddSingleton(config);

            // NorthwindContext はリバースエンジニアリングで生成
            // したコンテキストクラス。それを DI コンテナに登録
            services.AddDbContext<NorthwindContext>(options =>
                options.UseSqlServer(connString));

            // ProductService を DI コンテナに登録
            services.AddSingleton<ProductService>();

            // ServiceProvider を生成
            var provider = services.BuildServiceProvider();

            // ServiceProvider から ProductService のインスタンス
            // を生成。その際、Configuration と NorthwindContext
            // のインスタンスはコンストラクタ DI される
            productService = 
                provider.GetRequiredService<ProductService>();

            // DataGridView と BindingSource はデザイン画面で
            // ツールボックスから Form にドラッグ&ドロップ
            dataGridView1.DataSource = bindingSource1;
        }

        private async void Form1_Load(object sender, System.EventArgs e)
        {
            // ProductService に DI した Configuration から接続
            // 文字列を取得できることの確認用
            var northwind = productService.GetConnectionString();

            // EF Core を使って Northwind の Products テーブルか
            // ら List<T> 型のデータを取得し DataGridView に表示 
            var list = await productService.GetListAsync();
            bindingSource1.DataSource = list;
        }
    }
}

Tags: , , ,

CORE

EF Core で PK / Unique 制約違反例外をキャッチ

by WebSurfer 25. March 2021 10:26

EF Core 5.0 を利用して SQL Server のテーブルの CRUD 操作を行う ASP.NET Core 5.0 MVC アプリで、新規レコードの Create の際に PK / Unique 制約違反例外をキャッチし、エラーメッセージを表示する方法を書きます。

PK / Unique 制約の検証

先の記事「EF6 で PK / Unique 制約違反例外をキャッチ」の Core 版です。ほとんど EF6 の記事と同じですが、いくつか違う点があるので以下まとめて書いておきます。

  1. EF Code First でのデータ注釈を使用したインデックスとユニーク制約の付与は EF Core 5.0 で導入されたそうです。詳しくは Microsoft のドキュメント Indexes を見てください。(この記事でアプリのプラットフォームを .NET 5.0 としたのはそれが理由です。ちなみに、EF Core 3.x 以前は NuGet パッケージ Toolbelt.EntityFrameworkCore.IndexAttribute を利用できるらしいです)
  2. DbUpdateException から SqlException を取得する方法が EF6 とは異なります。EF6 では DbUpdateException の InnerException のさらに下の InnerException から SqlException を取得していましたが、EF Core では DbUpdateException 直下の InnerException で取得することができます。(EF6 も EF Core もコードを書いて試した結果の話です。必ずそうなると明記した Microsoft のドキュメントは見つからないので将来変わるかもしれないという不安要素はあります)
  3. DbUpdateException の InnerException から取得できる SqlException は System.Data.SqlClient 名前空間でなく Microsoft.Data.SqlClient 名前空間に属するものになります。(EF6 の場合は System.Data.SqlClient 名前空間)

先の EF6 の記事と同様に PK / Unique 制約違反例外をキャッチし、エラーメッセージを表示する機能を実装して���ました。それが以下のコードです。上の画像が実行結果で SQL Server のテーブルの ProductId 列に付与した PK 制約違反を補足してエラーメッセージを表示しています。

エンティティクラス (Model)

上にも書きましたが、IndexAttribute が使用できるのは EF Core 5.0 以降です。プロパティには付与できなくてクラスに付与する必要がある点が EF6 とは異なります。

Migration の際 decimal 型のプロパティには "This will cause values to be silently truncated ..." という警告が出るので No Type Was Specified for the Decimal Column を参考に ColumnAttribute を付与してみました。その必要はないと思いますが。

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

// IndexAttribute の属する名前空間
using Microsoft.EntityFrameworkCore;

namespace MvcCore5App2.Models
{
    [Index(nameof(ProductName), IsUnique = true)]
    public class PkUnique
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int ProductId { get; set; }

        [Required]
        [StringLength(128)]
        public string ProductName { get; set; }

        [Required]
        [Column(TypeName = "decimal(18,4)")]
        public decimal UnitPrice { get; set; }
    }
}

コンテキストクラス

using Microsoft.EntityFrameworkCore;
using MvcCore5App.Models;

namespace MvcCore5App.DAL
{
    public class PkUniqueContext : DbContext
    {
        public PkUniqueContext(DbContextOptions<PkUniqueContext> options)
            : base(options)
        {

        }

        public DbSet<PkUnique> PkUnique { set; get; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // DB に生成されるテーブルの名前を Product にした
            modelBuilder.Entity<PkUnique>().ToTable("Product");
        }
    }
}

Startup.cs に追加

上のコンテキストクラス PkUniqueContext のインスタンスを Controller に DI できるよう Startup.cs の ConfigureServices メソッドに以下のコードを追加します。

コード内の "DefaultConnection" はテンプレートで生成されたプロジェクトの appsettings.json に含まれる ASP.NET Identity 用の接続文字列です。なので、ASP.NET Identity 用のデータベースの中に Product というテーブルが追加で生成されます。

services.AddDbContext<PkUniqueContext>(options =>
  options.UseSqlServer(
    Configuration.GetConnectionString("DefaultConnection")));

上のエンティティクラスとコンテキストクラスから、EF Code First の機能を利用して生成された SQL Server のテーブルの構造は以下の通りです。上のエンティティクラスの UnitPrice プロパティに [Column(TypeName = "decimal(18,4)")] 属性を付与したのでデータ型が decimal(18,4) になっています。ちなみに、属性を設定しないと decimal(18,2) になります。

生成されたテーブルの構造

Controller / Action Method

スキャフォールディング機能を利用して Controller と View を生成します。以下のコードは、その Controller の Create アクションメソッドに、上に紹介した記事の PK / Unique 制約違反例外を補足するコードを実装したものです。

上に書きましたが、DbUpdateException から SqlException を取得する方法、取得した SqlException は Microsoft.Data.SqlClient 名前空間に属することに注意してください。

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MvcCore5App2.Data;
using MvcCore5App2.Models;

// SqlException が属する名前空間
using Microsoft.Data.SqlClient;

namespace MvcCore5App2.Controllers
{
    public class PkUniqueController : Controller
    {
        private readonly PkUniqueContext _context;

        public PkUniqueController(PkUniqueContext context)
        {
            _context = context;
        }

        // ・・・中略・・・

        // GET: PkUnique/Create
        public IActionResult Create()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(
            [Bind("ProductId,ProductName,UnitPrice")] PkUnique pkUnique)
        {
            if (ModelState.IsValid)
            {
                _context.Add(pkUnique);

                try
                {
                    await _context.SaveChangesAsync();
                }
                catch (DbUpdateException e)
                when (e.InnerException is SqlException sqlEx &&
                      (sqlEx.Number == 2601 || sqlEx.Number == 2627))
                { 
                    if (sqlEx.Number == 2627)
                    {
                        ModelState.AddModelError("ProductId", 
                                                 "PK 制約違反");
                    }

                    if (sqlEx.Number == 2601)
                    {
                        ModelState.AddModelError("ProductName", 
                                                 "Unique 制約違反");
                    }

                    return View(pkUnique);
                }
                return RedirectToAction(nameof(Index));
            }
            return View(pkUnique);
        }

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

View

スキャフォールディング機能を利用して生成した create.cshtml のコードそのままです。

@model MvcCore5App2.Models.PkUnique

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

<h1>Create</h1>

<h4>PkUnique</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="ProductId" class="control-label"></label>
                <input asp-for="ProductId" class="form-control" />
                <span asp-validation-for="ProductId" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ProductName" class="control-label"></label>
                <input asp-for="ProductName" class="form-control" />
                <span asp-validation-for="ProductName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="UnitPrice" class="control-label"></label>
                <input asp-for="UnitPrice" class="form-control" />
                <span asp-validation-for="UnitPrice" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

EF6 と同様に PK / Unique 両方に制約違反がある場合は PK 制約違反だけしか補足できません。両方補足して両方のエラーメッセージを表示する方法は分かりません。分かったらこの記事に追記します。

Tags: , , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  April 2021  >>
MoTuWeThFrSaSu
2930311234
567891011
12131415161718
19202122232425
262728293012
3456789

View posts in large calendar