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 に
1 | "Microsoft.AspNetCore.Mvc.Formatters.Xml" : "1.0.0" |
を加える。
Setup.cs の Setup.ConfigureServices に XmlSerializerOutputFormatter と XmlSerializerInputFormatter を追加する。
1 2 3 4 5 6 7 8 9 | 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 クラスを作っておく。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 クラスを作る。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | [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形式でシリアライズしてやり取りするだけなので、クラス名やプロパティ名だけ合わせておけばよい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | 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 を使ってデータベースを作らないといけないかも。