ASP.NET MVC Core 2.0で作ったAPIでXMLなどの形式を返したい
Created at:
ASP.NET MVC Coreはアクションの戻り値が IActionResult
ではない場合、デフォルトでJSONをシリアライズしたものを返します。しかし場合によってはJSONだけでなくXMLで返したいという場合もあるかと思います。
そのようなニーズにこたえるべくフォーマッターを登録することで様々な形式でレスポンスを返すことができる仕組みがあります。ここではXMLで返す方法を説明します。
まずXML用のフォーマッターはNuGetから Microsoft.AspNetCore.Mvc.Formatters.Xml
パッケージをインストールできるのでインストールします。
次にConfigureService
メソッドのASP.NET MVC Coreの設定でXMLのフォーマッターを登録します。
services.AddMvc(options =>
{
// XMLを返すフォーマッターとそのMIME Typeと拡張子のマッピングを登録
// XmlDataContractSerializerOutputFormatterはASP.NET Web API相当
options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
// options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
options.FormatterMappings.SetMediaTypeMappingForFormat("xml", "application/xml");
});
この状態で Accept
ヘッダーフィールドに application/xml
を指定してリクエストするとXMLでレスポンスが返ってくるようになります。簡単ですね。
XMLシリアライザを使うときの注意
XmlDataContractSerializerOutputFormatter
でシリアライズするクラスは引数なしのコンストラクタやプロパティにsetterが必要だったりするのでご注意ください。
この辺りを忘れていると406 Not Acceptableが返ってきて悩むことになります。
URLの .json/.xml のような拡張子で出し分けたい
通常は Accept
によるコンテントネゴシエーションで返すフォーマットを決定すればよいのですが、URLの一部でフォーマットを指定したいというということもあります。例えば、/People/Alice.json
とアクセスした場合はJSONで、/People/Alice.xml
であればXMLを返すといった形です。
これを行うには FormatFilter
フィルター(FormatFilterAttribute
クラス)をコントローラーにつけ、ルーティングに{format}
を足します。例えば以下のようになります。
[FormatFilter]
[Route("[controller]/{id}.{format?}")]
public class PeopleController : Controller
{
[HttpGet]
public Person Get(string id)
{
return /* Person のインスタンスを取得 */;
}
}
これでフォーマットが指定された場合はそれにマッチするフォーマッターが、指定されていなければデフォルトのフォーマッターが使用されます。
$ curl http://localhost:4743/People/Alice.json
{"name":"Alice","age":17}
$ curl http://localhost:4743/People/Alice.xml
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/WebApplication19.Controllers"><Age>17</Age><Name>Alice</Name></Person>
デフォルトのフォーマッターを変更する
Acceptやフォーマット名(拡張子)によってフォーマッターを決定できない場合、フォーマッターの登録順で見て行って一番最初に利用できるもの(=必ずしも一番最初ではない)がデフォルトのフォーマッターとなります。
つまり標準では JsonOutputFormatter
が選択されてJSONが返されるわけですが、デフォルトをXMLにしたいという場合もあるかもしれません。そのような場合は一度 JsonOutputFormatter
を抜いて、XMLのフォーマッターを登録してから再度 JsonOutputFormatter
を登録します。
var jsonOutputFormatter = options.OutputFormatters.OfType<JsonOutputFormatter>().First();
options.OutputFormatters.RemoveType<JsonOutputFormatter>();
options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
options.OutputFormatters.Add(jsonOutputFormatter);
MessagePack-CSharpを使ってみる
折角なのでレスポンスでMessagePackを返すのも試してみましょう。まあWeb APIでMessagePackを受けたり返したりしたいシーンがどのぐらいあるのかは謎といえば謎なのですが。
C#界最速のシリアライザであるところのMessagePack-CSharpにはASP.NET MVC Core用のフォーマッターが用意されているので使うのは簡単です。
まずNuGetからMessagePack
とMessagePack.AspNetCoreMvcFormatter
をインストールします。
インストールできたらXMLの時と同様に登録します。MessagePackのMIME Typeはapplication/x-msgpack
なので拡張子をマッピングを登録する場合やリクエストのAcceptにはそれを指定してください。折角なのでInputFormatterも登録しておきます。
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.FormatterMappings.SetMediaTypeMappingForFormat("msgpack", "application/x-msgpack");
options.OutputFormatters.Add(new MessagePackOutputFormatter());
options.InputFormatters.Add(new MessagePackInputFormatter());
});
}
あとは適当なコントローラーとデータを作ります。ここでは /People/Alice
といった形でアクセスできるようなものを作ってみます。
レスポンスに使う Person
クラスはMessagePackでシリアライズできるように MessagePackObject
としてマークしておきます。これはMessagePack-CSharpのお約束です。
[FormatFilter]
[Route("[controller]/{id}.{format?}")]
public class PeopleController : Controller
{
[HttpGet]
public Person Get(string id)
{
switch (id)
{
case "Alice": return new Person("Alice", 17);
case "Karen": return new Person("Karen", 17);
default: return null;
}
}
}
[MessagePackObject]
public class Person
{
[Key(0)]
public string Name { get; }
[Key(1)]
public int Age { get; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
あとは /People/Alice.msgpack
または Accept: application/x-msgpack
付きででリクエストを投げることでMessagePackで返ってくるはずです。
$ curl -H 'Accept: application/json' http://localhost:4743/People/Alice 2>/dev/null | od -c
0000000 { " n a m e " : " A l i c e " ,
0000020 " a g e " : 1 7 }
0000031
$ curl -H 'Accept: application/x-msgpack' http://localhost:4743/People/Alice 2>/dev/null | od -c
0000000 222 245 A l i c e 021
0000010
ところでMessagePackObject
とかつけるの面倒だなーMapモードでいいんだけどなーということもあるかもしれません。そんな時はContractlessStandardResolver
を使用するとよいでしょう。
services.AddMvc(options =>
{
options.FormatterMappings.SetMediaTypeMappingForFormat("msgpack", "application/x-msgpack");
options.OutputFormatters.Add(new MessagePackOutputFormatter(ContractlessStandardResolver.Instance));
options.InputFormatters.Add(new MessagePackInputFormatter(ContractlessStandardResolver.Instance));
});
public class Person
{
public string Name { get; }
public int Age { get; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
$ curl -H 'Accept: application/x-msgpack' http://localhost:4743/People/Alice 2>/dev/null | od -c
0000000 202 244 N a m e 245 A l i c e 243 A g e
0000020 021
0000021
このように簡単にMessagePackで返せるようになるので使えそうなときは積極的に使っていきたいですね。
余談: MvcOptions.RespectBrowserAcceptHeader とは何か
ASP.NET MVC CoreのMvcOptions
にはフォーマッターに関連したRespectBrowserAcceptHeader
プロパティという設定が存在します。この設定は何をするものかというと「ブラウザのようにAcceptに*/*
といったワイルドカードのメディアタイプを乗せて来た場合、デフォルトとするかどうか」です。
例えばそれぞれの設定時には以下のようになります。
- falseの場合: Acceptに
text/html, application/xml, */*
ならデフォルトのフォーマッター(JSON)、application/xml
ならXMLのフォーマッター - trueの場合: Acceptに
text/html, application/xml, */*
ならXMLのフォーマッター、application/xml
ならXMLのフォーマッター
つまりfalseの場合には*/*
やtext/*
のようなものを見つけると、Acceptで指定されたものを全て無視するのと同じになります。基本的にワイルドカードを指定してくるのはブラウザぐらいなのでブラウザのAcceptに敬意を払うかどうかということです。
このプロパティはデフォルトは false
となっていますので、もしブラウザのようなAcceptも受け入れてその通りに判断してほしいというときは true
を設定してください。