WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

Web API と要求ヘッダの application/xml

by WebSurfer 7. June 2020 14:12

.NET Framework 版の ASP.NET Web API を要求する際、要求ヘッダの Accept に application/xml が含まれると応答は JSON ではなく XML になります。

ASP.NET Web API の応答

知ってました? 実は自分は知らなかったです。(汗) ちなみに、MVC の Json メソッド、および Core 3.1 Web API と MVC の Json メソッドは、要求ヘッダの Accept に application/xml が含まれていても JSON が返ってきます。

Web API の応答をチェックするには Postman などのツールを使うのが一般的なようです。でも、GET 要求ならブラウザのアドレスバーに直接 URL を入力して応答を見ることでも十分だったので自分は IE11 を使ってそうしてました。それで期待した通り JSON 文字列が返ってきてました。

ところが、Chrome, Edge, Firefox では応答が JSON ではなく XML になってしまいます。理由は要求ヘッダの Accept に application/xml が含まれるからのようです。ハマったのは上の画像のようなサーバーエラーとなってしまったことです(HTTP 500 応答が返ってきます。上の画像は形式は XML ですが中身はエラーメッセージです)。

エラーメッセージ "The 'ObjectContent`1' type failed to serialize the response body for content type 'application/xml; charset=utf-8'." によるとシリアライズに失敗したということです。

IE11 や jQuery ajax を使った検証ではシリアライズには問題はないことは確認しており、ブラウザに Chrome などを使ったからと言ってそれがシリアライズの部分に影響があるとは考えられなかったです。何故なのでしょう?

循環参照の問題でした。

上の画像の InnerException のエラーメッセージにある Blog_E36679EB... D5AD0 という文字列が、先の記事「JSON シリアライズの際の循環参照エラー」にもあったのを思い出して気が付きました。

.NET Framework 版の ASP.NET Web API の JSON シリアライザは Newtonsoft の Json.NET のもので、JsonIgnoreAttribute クラスという属性が利用できます。なので、問題のプロパティに [JsonIgnore] を付与してシリアライズの際の循環参照エラーは回避していました。

でも [JsonIgnore] が有効なのは JSON にシリアライズするときのみで、XML にシリアライズするときは循環参照の問題は回避できません。

Chrome, Edge などを使った時は、ASP.NET Web API は要求ヘッダの Accept に application/xml が含まれるのを見て、XML にシリアライズしようとして循環参照の問題に陥ったということのようです。

Tags: , , ,

Web API

ASP.NET Core 3.1 Web API

by WebSurfer 15. May 2020 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 というように小文字になります。

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

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

Tags: ,

CORE

JSON シリアライズの際の循環参照エラー

by WebSurfer 8. March 2020 15:51

.NET Framework の ASP.NET MVC アプリや Web API アプリでオブジェクトを JSON 文字列にシリアライズするときの循環参照エラーの問題とその回避方法を書きます。

循環参照エラー

例えば、Entity Framework 6 の Code First 機能を利用して、以下のコード(Microsoft のチュートリアル「新しいデータベースへの Code First」のサンプルコードです)からデーターベースを生成し、Linq to Entities で取得したデータを JSON にシリアライズするケースを考えます。

public class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; }

    public virtual List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogId { get; set; }
    public virtual Blog Blog { get; set; }
}

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

以下のコードのように、Linq to Enitities を使ってデータベースからデータを取得し、ASP.NET MVC5 の Controller.Json メソッドを使って JSON 文字列にシリアライズしてみます。

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Mvc5App.DAL;
using System.Data.Entity;

namespace Mvc5App.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Json()
        {
            var db = new BloggingContext();
            var list = db.Blogs.Include(b => b.Posts);
            return Json(list, JsonRequestBehavior.AllowGet);
        }
    }
}

上のアクションメソッド Json を呼び出すと、この記事の上の画像の通り、循環参照エラーになります。

(注: ちなみに、上のコードを var list = db.Blogs; に変えるとシリアライズの際に遅延ローディングが起こって "この Command に関連付けられている DataReader が既に開かれています。このコマンドを最初に閉じる必要があります" というエラーになります。var list = db.Blogs.ToList(); とすると、シリアライズの際に遅延ローディングが起こるのは同じですが、その時は DataReader が閉じているので循環参照のところまで進んで、上の画像と同じエラーになります)

ASP.NET Web API でも同様で、return db.Blogs.Include(b => b.Posts); とするとシリアライズする際に循環参照が生じて JSON へのシリアライズに失敗します。

原因は Post クラスにナビゲーションプロパティ Blog が定義されているためで、それをシリアライズしようとすると循環参照が発生し InvalidOperationException がスローされるからだそうです。ググってヒットした記事「ASP.NET Web API で循環参照なモデルの公開を解決する」を参考にさせていただきました。

その記事に書いてありますが、.NET Framework の ASP.NET Web API の JSON シリアライザは Newtonsoft の Json.NET のもので、JsonIgnoreAttribute Class という属性が利用できます。なので、上の Post クラスの Blog プロパティに [JsonIgnore] を付与すればシリアライズの際の循環参照エラーは回避できます。

しかしながら、MVC5 アプリで利用する Controller.Json メソッドは内部で JavaScriptSerializer クラスを使っており、JsonIgnore 属性は効果がありません。

なので、Microsoft のドキュメント「Create Data Transfer Objects (DTOs)」を参考にして以下のコードのようにしてみました。

public class BlogDto
{
    public int BlogId { get; set; }
    public string Name { get; set; }
    public List<PostDto> Posts { get; set; }
}

public class PostDto
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public int BlogId { get; set; }
}

public ActionResult Json()
{
    var db = new BloggingContext();

    var list = db.Blogs.Include(b => b.Posts).
               Select(b => new BlogDto
               {
                   BlogId = b.BlogId,
                   Name = b.Name,
                   Posts = b.Posts.Select(p => new PostDto
                   {
                       PostId = p.PostId,
                       Title = p.Title,
                       Content = p.Content,
                       BlogId = p.BlogId
                   }).ToList()
               });

    return Json(list, JsonRequestBehavior.AllowGet);
}

ナビゲーションプロパティを除いた別の入れ物(上の BlogDto クラス、PostDto クラス)に入れてシリアライズしているわけですから、当然ながら循環参照の問題はなくなって JSON 文字列にシリアライズできます。

循環参照を避ける以外に、必要なデータだけを望む形で JSON 文字列にシリアライズするということも出来るわけですから、積極的に DTO を使うのが正解だと思います。

なお、.NET Core 3.0 以降には JsonIgnoreAttribute クラスが利用できるので、上の .NET Framework のケースとは話は違ってきて、MVC, Web API どちらの場合も循環参照になるプロパティに [JsonIgnore] を付与すればシリアライズできます。

(Microsoft のドキュメント Add Newtonsoft.Json-based JSON format support を見ると ASP.NET Core 2.x 以前は Newtonsoft.Json パッケージが、ASP.NET Core 3.x 以降は System.Text.Json が使われているそうです)

Tags: , , ,

MVC

About this blog

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

Calendar

<<  July 2020  >>
MoTuWeThFrSaSu
293012345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar