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 を使ってデータベースを作らないといけないかも。