WebSurfer's Home

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

JsonSerializer の Camel Casing (CORE)

by WebSurfer 2021年1月18日 21:18

.NET Core v3.x 以降の MVC や Web API アプリで .NET オブジェクトを JSON 文字列にシリアライズすると、以下の画像のように {"name":"value"} の "name" の先頭の文字が小文字になってしまうこと、さらにそれによりにデシリアライズに問題が出ること、その対処方法について書きます。

JSON 文字列

.NET Core MVC や Web API では、Core v3.x 以降 System.Text.Json 名前空間JsonSerializer クラスが JSON のシリアライズ/デシリアライズに用いられるようになったそうです。Core v2.x 以前は Newtonsoft.Json が使われていたという違いがあります。

上の画像の例ではシリアライズ対象のオブジェクトのクラス定義は以下のようになっており、プロパティ名の先頭は大文字ですが、JsonSerializer クラスを使ってシリアライズした結果の JSON 文字列では上の画像の通り "name" が "firstName", "lastName" と先頭の文字が小文字になっている点に注目してください。

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

(上の画像で "value" が "\u592A\u90CE", "\u65E5\u672C" となっているのは、それぞれ "太郎", "日本" の Unicode が Unicode Escape Sequence という形にエスケープされた結果です。詳しくは先の記事「ASP.NET Core MVC の JSON シリアライズ」を見てください)

先頭文字が小文字になるのは Camel Casing と言って、変数などを camelCasing というように記述するものだそうです。(先頭が大文字、即ち CamelCasing というように書く場合もあるはずですがそこはちょっと置いときます)

なお、コンソールアプリで JsonSerializer クラスを使って以下のようにした場合は "name" は "FirstName", "LastName" となり、先頭文字が小文字になることはありません。ということは、MVC や Web API のフレームワークで Camel Casing にする設定がされているようです。

namespace ConsoleAppJson
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person
            {
                FirstName = "太郎",
                LastName = "日本"
            };

            string json = JsonSerializer.Serialize(person);
            Console.WriteLine(json);
        }
    }
}

"name" の先頭文字が小文字になると、例えば firstName となると困るのは、JSON 文字列を JavaScript オブジェクトにデシリアライズした後 "太郎" という値を取得するには <javascript object>.firstName というようにしなければならず、それを忘れて FirstName を使うと結果は undefined になってしまうことです。

さらに困るのは、 JSON 文字列を元の Person クラス(プロパティの先頭文字が大文字)にデシリアライズする場合です。JsonSerializer クラスのデシリアライザはデフォルトでは大文字小文字の区別をするのでデシリアライズできないという結果になります。(エラーは出ません。Person クラスを例に取ると FirstName, LastName プロパティが null になります)

シリアライズの際 Camel Casing を避ける("name" の先頭文字が小文字にならないようにする)方法ですが、Startup.cs で JsonSerializerOptions の PropertyNamingPolicy プロパティを null に設定してやることで可能です。

具体的には、Visual Studio のテンプレートで ASP.NET Core MVC / Web API プロジェクトを作成すると自動生成される Startup.cs のコードの中に services.AddControllersWithViews(); が含まれているはずなので、それに以下のように .AddJsonOptions(opttions => ... 以下のコードを追加します。

services.AddControllersWithViews().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.PropertyNamingPolicy = null;
});

これで以下のような MVC アクションメソッドの Controller.Json メソッドによる JSON シリアライズでも、

public IActionResult Json()
{
    var person = new Person
    {
        FirstName = "太郎",
        LastName = "日本"
    };

    return Json(person);
}

以下のような Web API のアクションメソッドによる JSON シリアライズでも、

[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [HttpGet]
    public Person GetPerson()
    {
        var person = new Person
        {
            FirstName = "太郎",
            LastName = "日本"
        };

        return person;
    }
}

シリアライズされた結果の JSON 文字列 {"name":"value"} の "name" は、元の Person クラスのプロパティ名通り "FirstName", "LastName" となります。

(アクションメソッド個別に対応する場合は、MVC の場合は Controller.Json メソッドの第 2 引数に JsonSerializerOptions を設定することで可能です。Web API の場合は JsonSerializer.Deserialize メソッドを使って第 2 引数に JsonSerializerOptions を設定することで可能です)

さらに、"value" が Unicode Escape Sequence という形になるのを避けたいのであれば、options の設定に以下を追加すれば OK です。(セキュリティ上それが良いのかどうかの話は置いといてですが)

options.JsonSerializerOptions.Encoder = 
              JavaScriptEncoder.Create(UnicodeRanges.All);

最後にもう一つ、上に書いたデシリアライズする際の大文字小文字の区別の問題を回避する方法を書いておきます。これも JsonSerializerOptions の設定で可能で、以下のようにすれば大文字小文字の違いは無視します。(Newtonsoft.Jason と同じ結果になります)

Person person = JsonSerializer.Deserialize<Person>(json, 
                new JsonSerializerOptions 
                {
                    PropertyNameCaseInsensitive = true
                });

Tags: , , ,

CORE

Web API でファイルアップ/ダウンロード (CORE)

by WebSurfer 2021年1月17日 17:41

ファイルをアップロード/ダウンロードする相手が ASP.NET Core 3.1 Web API の場合はどのようにすれば良いかについて書きます。

Web API へファイルアップロード

コードを書いてみましたが、先の記事「ASP.NET Core 3.1 Web API」に書いた、(1) Controller は ControllerBase クラスを継承、(2) ApiControllerAttribute 属性を付与、(3) ルーティングは RouteAttibute 属性を付与して設定、アクションメソッドに [HttpGet], [HttpPost] 属性を付与する以外は MVC の場合とほ��んど変わりませんでした。

(MVC の場合は、先の記事「ASP.NET Core MVC でファイルアップロード」と「ASP.NET Core MVC でファイルダウンロード」を見てください)

それでこの記事の話は終わってしまうのですが、それではちょっとブログの記事としては寂しいし、今後の参考になるかもしれないので検証に使ったコードを下にアップしておきます。

Web API コントローラー/アクションメソッド

using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using System.IO;

namespace WebAPI.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class FileUpDownloadController : ControllerBase
    {
        // 物理パスの取得用
        private readonly IWebHostEnvironment _hostingEnvironment;

        public FileUpDownloadController(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }

        [HttpPost]
        public async Task<IActionResult> PostFile(IFormFile postedFile)
        {
            string result = "";
            if (postedFile != null && postedFile.Length > 0)
            {
                // アップロードされたファイル名を取得。ブラウザが IE 
                // の場合 postedFile.FileName はクライアント側でのフ
                // ルパスになることがあるので Path.GetFileName を使う
                string filename = Path.GetFileName(postedFile.FileName);

                // アプリケーションルートの物理パスを取得
                // wwwroot の物理パスは WebRootPath プロパティを使う
                string contentRootPath = _hostingEnvironment.ContentRootPath;
                string filePath = contentRootPath + "\\" + 
                                  "UploadedFiles\\" + filename;

                using (var stream = new FileStream(filePath, FileMode.Create))
                {
                    await postedFile.CopyToAsync(stream);
                }

                result = filename + " (" + postedFile.ContentType +
                         ") - " + postedFile.Length +
                         " bytes アップロード完了";
            }
            else
            {
                result = "ファイルアップロードに失敗しました";
            }

            return Content(result);
        }

        [HttpGet]
        [ResponseCache(Duration = 0, 
            Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult GetFile(string filename = "sample1.jpg")
        {
            if (string.IsNullOrEmpty(filename))
            {
                return NotFound("引数が null または空");
            }

            // アプリケーションルートの物理パスを取得
            string contentRootPath = _hostingEnvironment.ContentRootPath;

            // ダウンロードするファイルの物理パス
            string physicalPath = contentRootPath + "\\" + 
                                  "Files\\" + filename;

            if (!System.IO.File.Exists(physicalPath))
            {
                return NotFound("指定されたパスにファイルが無い");
            }

            // Content-Disposition ヘッダを設定(RFC 6266 対応してない)
            Response.Headers.Append("Content-Disposition",
                "attachment;filename="+filename);
            
            return new PhysicalFileResult(physicalPath, "image/jpeg");
        }
    }
}

アップロードの検証に使った View

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

<h1>Upload</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post" enctype="multipart/form-data">
            <div>
                <div>
                    <p>Upload file using this form:</p>
                    @* name 属性はモデルのクラスのプロパティ名と同じ
                       にしないとサーバー側でモデルバインディングさ
                       れないので注意。大文字小文字は区別しない。*@
                    <input type="file" name="postedfile" />
                </div>
            </div>

        </form>
        <div>
            <div>
                <input type="button" id="ajaxUpload" value="Upload" />
                <div id="result"></div>
            </div>
        </div>
    </div>
</div>

<script src="~/Scripts/jquery.js"></script>
<script type="text/javascript">
    //<![CDATA[
    $(function () {
        $('#ajaxUpload').on('click', function (e) {
            // FormData オブジェクトの利用
            var fd = new FormData(document.querySelector("form"));

            $.ajax({
                url: '/FileUpDownload',
                method: 'post',
                data: fd,
                processData: false, // jQuery にデータを処理させない
                contentType: false  // contentType を設定させない
            }).done(function(response) {
                $("#result").empty;
                $("#result").text(response);
            }).fail(function( jqXHR, textStatus, errorThrown ) {
                $("#result").empty;
                $("#result").text('textStatus: ' + textStatus +
                    ', errorThrown: ' + errorThrown);
            });
        });
    });
    //]]>
</script>

ダウンロードの検証はブラウザのアドレスバーにコントローラーの URL を入力して FileUpDownload を GET 要求すれば可能です。ファイル名はデフォルトで "sample1.jpg" となっていますが、クエリ文字列で別のファイルを指定できます。

Tags: , , ,

Upload Download

ASP.NET Core MVC の非同期プログラミング

by WebSurfer 2021年1月2日 17:40

先の記事「ASP.NET MVC の非同期プログラミング」は .NET Framework 版の MVC5 アプリの話ですが、ASP.NET Core 3.1 MVC アプリでも同様なことを検証しましたのでその結果を書きます。

ASP.NET Core 3.1 MVC の非同期プログラミング

以下に (1) async / await を利用した非同期プログラミングで使用されるスレッドがどのようになるか、(2) Task.Result などを使った同期コードと非同期コードを混在させるとデッドロックは起きるのか、(3) Task.ConfigureAwait(Boolean) メソッドを付与するとどう変わるかついて書きます。

意外だったのは、.NET Framework 版と違って、Task.Result を使ってもデッドロックならないということででした。

なお、Kestrel の場合はどうなるかですが、Visual Studio を使って IIS Express (インプロセス ホスティング) と Kestrel を切り替えて両方の動作を確認しました。どちらも同じ結果となりました。

(1) 使用されるスレッド

ASP.NET で非同期プログラミングを行う目的は、スレッドプールにある限られた数のスレッドを有効利用し、スループットを向上するためです。なので非同期メソッドのチェーンの一番深いところにある await 前後でスレッドが切り替わるはずです。

それを確認するために以下のコードで試してみた結果が上の画像です。期待通り、TimeCosumingMethod メソッドの await 前後で ManagedThreadId が 27 から 25 に変わっています。

public async Task<ActionResult> AsyncTest()
{
    ViewBag.Id1 = "開始時, ID=" +
                  Thread.CurrentThread.ManagedThreadId;

    ViewBag.Id2 = await TimeCosumingMethod();

    // .NET Framework 版の MVC5 アプリでは上に代えて以下のようにすると
    // デッドロックになったが Core 3.1 版ではデッドロックにはならない
    //ViewBag.Id2 = TimeCosumingMethod().Result;

    ViewBag.Id3 = "終了時, ID=" +
                  Thread.CurrentThread.ManagedThreadId;

    return View();
}

private async Task<string> TimeCosumingMethod()
{
    int id = Thread.CurrentThread.ManagedThreadId;

    await Task.Delay(3000);

    // ConfigureAwait(false) の付与は結果に関係なし。
    // デッドロックになる時は ConfigureAwait(false) が勝手に付与され、
    // デッドロックにならない時は付与しても無視されるような感じ
    //await Task.Delay(3000).
    //    ConfigureAwait(continueOnCapturedContext: false);

    return "TimeCosumingMethod の戻り値, ID(IN)=" + id +
           " / ID(OUT)=" + Thread.CurrentThread.ManagedThreadId;
}

(2) Task.Result の使用

先の記事「await と Task.Result によるデッドロック」で書いたように Task.Result を使った同期コードと非同期コードを混在させるとデッドロックは起きるのかということの確認です。

結果は下の画像の通り処理は無事完了し、デッドロックは起きなかったです。.NET Framework 版の MVC5 アプリで TimeCosumingMethod の await Task.Delay(3000); に .ConfigureAwait(false) を付与した場合と同じ結果です。すなわち ID(OUT) のみ ManagedThreadId が異なり他は同じになっています。

Task.Result の使用

Task.Result をどのように使ったかは上のコードの AsyncTest アクションメソッドのコメントを見てください。.NET Framework 版での検証と全く同じやり方ですです。

.NET Framework 版でデッドロックなる理由は: まず、TimeCosumingMethod().Result で 1 つの同期ブロックが待機中となる。呼び出された TimeCosumingMethod メソッドの await で待機する際にキャプチャされた「現在のコンテキスト」で await 完了後の同期処理を実行しようとする。しかし「現在のコンテキスト」では一度に実行するコードは 1 つのチャンクに限定されている。なので、Result プロパティでの待機が終わるまで await 完了後の同期処理は実行できない。結果デッドロックになる・・・ということで、Core 3.1 でも同じになると信じていたんですが、一体どうなっているのでしょう?

(3) ConfigureAwait の付与

AsyncTest アクションメソッドで Result を使うか否かで結果は上の画像のように変わりますが、上のコードの TimeCosumingMethod のコメントに書きましたように、ConfigureAwait(false) の付与はその結果に関係なかったです。

Core では AsyncTest アクションメソッドで Result を使ってデッドロックになる時は ConfigureAwait(false) が勝手に付与され、正しく await してデッドロックにならない時は ConfigureAwait(false) を付与しても無視されているような感じです。

.NET Framework 版とは話が大きく変ってきてしまうのですが、一体どうなっているのでしょう。どうも今までの知識は Core には役に立たないようで、また勉強しなければならないようです。でも、今はその気力がないです。(笑)

Tags: , , , , ,

CORE

About this blog

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

Calendar

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

View posts in large calendar