ASP.NET Core の Web API は標準で JSON 形式を扱うようになっているので、XML 形式を扱おうとすると苦労します…というか、苦労したのでメモ書き。
送受信の形式
Web API を POST で送信する場合 Body に何の形式を使うのか、というのと、受信に何の形式を使うのか、で組み合わせがある。
送信側
– フォーム形式 application/x-www-form-urlencoded
– JSON 形式 application/json
– XML 形式 application/xml あるいは text/xml
受信側
– JSON 形式 application/json
– XML 形式 application/xml あるいは text/xml
で、最近はブラウザ経由で JSON 形式で送受信することが多いので、そっちの情報は比較的多いのだが、XML 形式の情報がない。というか、WCF がそれを担っていたのだけど、WCF 自体が廃盤になっている。
なので、試験的に
– WPF アプリで XML 形式で送信
– ASP.NET Core Web API で XML 形式で返す
ということが考える。
テストプロジェクトを作る
ASP.NET Core Web Applicaiton(.NET Core) を使う.

Web API 側の設定
project.json に
"Microsoft.AspNetCore.Mvc.Formatters.Xml": "1.0.0"
を加える。
Setup.cs の Setup.ConfigureServices に XmlSerializerOutputFormatter と XmlSerializerInputFormatter を追加する。
services.AddMvc();
// add XML output formatter
services.Configure<Microsoft.AspNetCore.Mvc.MvcOptions>(
options => {
options.OutputFormatters.Add(
new Microsoft.AspNetCore.Mvc.Formatters.XmlSerializerOutputFormatter());
options.InputFormatters.Add(
new Microsoft.AspNetCore.Mvc.Formatters.XmlSerializerInputFormatter());
});
OutputFormatters がアウトプット用で、InputFormattersがインプット用なので、フォーム形式で受けてXML形式で返す場合には、OutputFormattersだけでよい。
XML形式には、XmlDataContractSerializerOutputFormatter もあるの。これはクライアントと形式を揃える必要がある。じゃないとデシリアライズができない。
これで ASP.NET Core 側の XML 形式で送受信する設定は完了。
Web API の PeopleController クラスを作ってみる
Modelクラスである Person クラスを作っておく。
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
#if false
// これでは XMLデシリアライズできない
public class People : List<Person> { }
#endif
public class People
{
// プロパティにして List 化すると通る
public List<Person> Items { get; set; }
}
実は、List<Person> を使いたいのだが、XML形式でシリアライズするときに ArrayOfPerson のように変換されてデシリアライズでうまくいかない。仕方がないので、People クラスのように、中身に List を含んだラップクラスを作る。
この Person クラスからコードファーストでデータベースを作った後、PeopleController クラスを作る。
[Produces("application/xml")]
[Route("api/People")]
public class PeopleController : Controller
{
private readonly ApplicationDbContext _context;
public PeopleController(ApplicationDbContext context)
{
_context = context;
}
// GET: api/People
[HttpGet]
#if false
public IEnumerable<Person> GetPerson()
{
return _context.Person;
}
#else
public async Task<People> GetPerson()
{
var people = new People();
people.Items = new List<Person>();
await _context.Person.ForEachAsync(p => people.Items.Add(p));
return people;
}
#endif
// GET: api/People/5
[HttpGet("{id}")]
public async Task<IActionResult> GetPerson([FromRoute] int id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
Person person = await _context.Person.SingleOrDefaultAsync(m => m.Id == id);
if (person == null)
{
return NotFound();
}
return Ok(person);
}
// PUT: api/People/5
[HttpPut("{id}")]
public async Task<IActionResult> PutPerson([FromRoute] int id, [FromBody] Person person)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != person.Id)
{
return BadRequest();
}
_context.Entry(person).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!PersonExists(id))
{
return NotFound();
}
else
{
throw;
}
}
// return NoContent();
// return CreatedAtAction("GetPerson", new { id = person.Id }, person);
return await GetPerson(person.Id);
}
// POST: api/People
[HttpPost]
public async Task<IActionResult> PostPerson([FromBody] Person person)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_context.Person.Add(person);
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateException)
{
if (PersonExists(person.Id))
{
return new StatusCodeResult(StatusCodes.Status409Conflict);
}
else
{
throw;
}
}
return CreatedAtAction("GetPerson", new { id = person.Id }, person);
}
// DELETE: api/People/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeletePerson([FromRoute] int id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
Person person = await _context.Person.SingleOrDefaultAsync(m => m.Id == id);
if (person == null)
{
return NotFound();
}
_context.Person.Remove(person);
await _context.SaveChangesAsync();
return Ok(person);
}
private bool PersonExists(int id)
{
return _context.Person.Any(e => e.Id == id);
}
/// <summary>
/// 受け口を POST に変換する
/// </summary>
[HttpPost("{id}")]
[Route("Edit/{id}")]
public async Task<IActionResult> Edit([FromRoute] int id, [FromBody] Person person)
{
return await PutPerson(id, person);
}
[HttpPost]
[Route("Create")]
public async Task<IActionResult> Create([FromBody] Person person)
{
return await PostPerson(person);
}
}
Entity Framework から取得するように修正してあるので、ややこしくなっているけど、ASP.NET Core MVC のスキャフォールディングと合わせるように、Create や Update で通るようにしてある。
フォーム形式の場合は、Bind 属性で値を取るが、JSONやXML形式の場合は FromBody 属性でバインドする。このあたり、ちょっと混乱しているような気がする。
PeopleController クラスの Produces 属性は、デフォルトで返す Content-type を指定するらしい。
WPF クライアントを作る
本来ならば、Person, People クラスをアセンブリで共有するほうがいいのだが、ASP.NET Core は .NET Core のライブラリで、WPF クライアントは .NET Framework のライブラリなので共有できない。が、XML形式やJSON形式でシリアライズしてやり取りするだけなので、クラス名やプロパティ名だけ合わせておけばよい。
namespace ClientXml
{
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private async void clickGet(object sender, RoutedEventArgs e)
{
var hc = new HttpClient();
hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
var res = await hc.GetAsync("http://localhost:5000/api/people");
var str = await res.Content.ReadAsStringAsync();
textXml.Text = str;
#if false
// ArrayOfPerson は取れない
var xs = new System.Xml.Serialization.XmlSerializer(typeof(IEnumerable<Person>));
var items = xs.Deserialize(new System.IO.StringReader(str)) as IEnumerable<Person>;
textPerson.Text = "";
foreach ( var item in items )
{
textPerson.Text += $"{item.Id} {item.Name} {item.Age} n";
}
#endif
var xs = new System.Xml.Serialization.XmlSerializer(typeof(People));
var people = xs.Deserialize(new System.IO.StringReader(str)) as People;
textPerson.Text = "";
foreach (var item in people.Items)
{
textPerson.Text += $"{item.Id} {item.Name} {item.Age} n";
}
}
private async void clickGetById(object sender, RoutedEventArgs e)
{
int id = 2;
var hc = new HttpClient();
hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
var res = await hc.GetAsync($"http://localhost:5000/api/people/{id}");
var str = await res.Content.ReadAsStringAsync();
textXml.Text = str;
var xs = new System.Xml.Serialization.XmlSerializer(typeof(Person));
var item = xs.Deserialize(new System.IO.StringReader(str)) as Person;
textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
}
private async void clickPutById(object sender, RoutedEventArgs e)
{
var person = new Person() { Id = 2, Name = "update person", Age = 99 };
var xs = new System.Xml.Serialization.XmlSerializer(typeof(Person));
var hc = new HttpClient();
hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
var sw = new System.IO.StringWriter();
// 先頭の <?xml ... をカットする
var settings = new System.Xml.XmlWriterSettings() { OmitXmlDeclaration = true, Encoding = Encoding.UTF8 };
var xw = System.Xml.XmlWriter.Create(sw, settings);
xs.Serialize(xw, person);
var xml = sw.ToString();
var cont = new StringContent(xml, Encoding.UTF8, "application/xml");
var res = await hc.PutAsync($"http://localhost:5000/api/people/{person.Id}", cont);
var str = await res.Content.ReadAsStringAsync();
textXml.Text = str;
var item = xs.Deserialize(new System.IO.StringReader(str)) as Person;
textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
}
private async void clickPost(object sender, RoutedEventArgs e)
{
var person = new Person() { Id = 0, Name = "new person", Age = 88 };
var xs = new System.Xml.Serialization.XmlSerializer(typeof(Person));
var hc = new HttpClient();
hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
var sw = new System.IO.StringWriter();
var settings = new System.Xml.XmlWriterSettings() { OmitXmlDeclaration = true, Encoding = Encoding.UTF8 };
var xw = System.Xml.XmlWriter.Create(sw, settings);
xs.Serialize(xw, person);
var xml = sw.ToString();
var cont = new StringContent(xml, Encoding.UTF8, "application/xml");
var res = await hc.PostAsync($"http://localhost:5000/api/people", cont);
var str = await res.Content.ReadAsStringAsync();
textXml.Text = str;
var item = xs.Deserialize(new System.IO.StringReader(str)) as Person;
textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
}
private void clickDeleteById(object sender, RoutedEventArgs e)
{
}
private async void clickCreate(object sender, RoutedEventArgs e)
{
var person = new Person() { Id = 0, Name = "new person", Age = 88 };
var xs = new System.Xml.Serialization.XmlSerializer(typeof(Person));
var hc = new HttpClient();
hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
var sw = new System.IO.StringWriter();
var settings = new System.Xml.XmlWriterSettings() { OmitXmlDeclaration = true, Encoding = Encoding.UTF8 };
var xw = System.Xml.XmlWriter.Create(sw, settings);
xs.Serialize(xw, person);
var xml = sw.ToString();
var cont = new StringContent(xml, Encoding.UTF8, "application/xml");
var res = await hc.PostAsync($"http://localhost:5000/api/people/Create", cont);
var str = await res.Content.ReadAsStringAsync();
textXml.Text = str;
var item = xs.Deserialize(new System.IO.StringReader(str)) as Person;
textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
}
private async void clickEdit(object sender, RoutedEventArgs e)
{
var person = new Person() { Id = 2, Name = "update person", Age = 99 };
var xs = new System.Xml.Serialization.XmlSerializer(typeof(Person));
var hc = new HttpClient();
hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
var sw = new System.IO.StringWriter();
// 先頭の <?xml ... をカットする
var settings = new System.Xml.XmlWriterSettings() { OmitXmlDeclaration = true, Encoding = Encoding.UTF8 };
var xw = System.Xml.XmlWriter.Create(sw, settings);
xs.Serialize(xw, person);
var xml = sw.ToString();
var cont = new StringContent(xml, Encoding.UTF8, "application/xml");
var res = await hc.PostAsync($"http://localhost:5000/api/people/Edit/{person.Id}", cont);
var str = await res.Content.ReadAsStringAsync();
textXml.Text = str;
var item = xs.Deserialize(new System.IO.StringReader(str)) as Person;
textPerson.Text = $"{item.Id} {item.Name} {item.Age}";
}
}
}
namespace SampleWebApiXml.Models
{
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
public class People
{
public List<Person> Items { get; set; }
}
}
試行錯誤した結果を載せているので、これが一番良いというわけではない。
いくつか、ポイントがあるのでざっと解説をしておくと、
– XML 形式で受信するために hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(“application/xml”)); を付ける。HTTP プロトコルの Accept ヘッダに受信する形式を指定しておくと、Web API が Accept にあわせて送ってくれる。
– POST や PUT で送信するときに、StringContent(xml, Encoding.UTF8, “application/xml”); のように Content-type を指定する。これを指定しないと、Web API 側で XML データとして認識してくれない。
– XmlSerializer は、何故か UTF16 でシリアライズするので、先頭の <?xml … を取り除くために、System.Xml.XmlWriterSettings で設定をする。ASCII 文字でしかテストしていないが、日本語を通す場合はきちんと string 型から UTF8 エンコードをしたほうがいいかもしれない.
うまく実行できると、こんな風にサーバ側を dotnet run で動かして、WPF アプリから送受信できるようになる。
きちっとした説明は別の機会にでも…
サンプルコード
動作できるサンプルコードはこちら
https://1drv.ms/u/s!AmXmBbuizQkXgfsQuu9Ly1NjTHa6nQ
最初に dotnet ef database update を使ってデータベースを作らないといけないかも。

