ASP.NET Core MVC の Web API で XML 形式のデータを扱う

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

カテゴリー: ASP.NET パーマリンク