WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

Web API でファイルアップ/ダウンロード (CORE)

by WebSurfer 17. January 2021 17:41

ファイルをアップロード/ダウンロードする相手が ASP.NET Core 3.1 Web API の場合はどのようにすれば良いかについて書きます。

Web API へファイルアップロード

コードを書いてみましたが、先の記事「ASP.NET Core 3.1 Web API」に書いた、(1) Controller は ControllerBase クラスを継承、(2) ApiControllerAttribute 属性を付与、(3) ルーティングは RouteAttibute 属性を付与して設定、アクションメソッドに [HttpGet], [HttpPost] 属性を付与する以外は MVC の場合とほとんど変わりませんでした。

(MVC の場合は、先の記事「ASP.NET Core MVC でファイルアップロード」と「ASP.NET Core MVC でファイルダウンロード」を見てください)

それでこの記事の話は終わってしまうのですが、それではちょっとブログの記事としては寂しいし、今後の参考になるかもしれないので検証に使ったコードを下にアップしておきます。

Web API コントローラー/アクションメソッド

using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using System.IO;

namespace WebAPI.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class FileUpDownloadController : ControllerBase
    {
        // 物理パスの取得用
        private readonly IWebHostEnvironment _hostingEnvironment;

        public FileUpDownloadController(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }

        [HttpPost]
        public async Task<IActionResult> PostFile(IFormFile postedFile)
        {
            string result = "";
            if (postedFile != null && postedFile.Length > 0)
            {
                // アップロードされたファイル名を取得。ブラウザが IE 
                // の場合 postedFile.FileName はクライアント側でのフ
                // ルパスになることがあるので Path.GetFileName を使う
                string filename = Path.GetFileName(postedFile.FileName);

                // アプリケーションルートの物理パスを取得
                // wwwroot の物理パスは WebRootPath プロパティを使う
                string contentRootPath = _hostingEnvironment.ContentRootPath;
                string filePath = contentRootPath + "\\" + 
                                  "UploadedFiles\\" + filename;

                using (var stream = new FileStream(filePath, FileMode.Create))
                {
                    await postedFile.CopyToAsync(stream);
                }

                result = filename + " (" + postedFile.ContentType +
                         ") - " + postedFile.Length +
                         " bytes アップロード完了";
            }
            else
            {
                result = "ファイルアップロードに失敗しました";
            }

            return Content(result);
        }

        [HttpGet]
        [ResponseCache(Duration = 0, 
            Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult GetFile(string filename = "sample1.jpg")
        {
            if (string.IsNullOrEmpty(filename))
            {
                return NotFound("引数が null または空");
            }

            // アプリケーションルートの物理パスを取得
            string contentRootPath = _hostingEnvironment.ContentRootPath;

            // ダウンロードするファイルの物理パス
            string physicalPath = contentRootPath + "\\" + 
                                  "Files\\" + filename;

            if (!System.IO.File.Exists(physicalPath))
            {
                return NotFound("指定されたパスにファイルが無い");
            }

            // Content-Disposition ヘッダを設定(RFC 6266 対応してない)
            Response.Headers.Append("Content-Disposition",
                "attachment;filename="+filename);
            
            return new PhysicalFileResult(physicalPath, "image/jpeg");
        }
    }
}

アップロードの検証に使った View

@{
    ViewData["Title"] = "Upload";
}

<h1>Upload</h1>

<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post" enctype="multipart/form-data">
            <div>
                <div>
                    <p>Upload file using this form:</p>
                    @* name 属性はモデルのクラスのプロパティ名と同じ
                       にしないとサーバー側でモデルバインディングさ
                       れないので注意。大文字小文字は区別しない。*@
                    <input type="file" name="postedfile" />
                </div>
            </div>

        </form>
        <div>
            <div>
                <input type="button" id="ajaxUpload" value="Upload" />
                <div id="result"></div>
            </div>
        </div>
    </div>
</div>

<script src="~/Scripts/jquery.js"></script>
<script type="text/javascript">
    //<![CDATA[
    $(function () {
        $('#ajaxUpload').on('click', function (e) {
            // FormData オブジェクトの利用
            var fd = new FormData(document.querySelector("form"));

            $.ajax({
                url: '/FileUpDownload',
                method: 'post',
                data: fd,
                processData: false, // jQuery にデータを処理させない
                contentType: false  // contentType を設定させない
            }).done(function(response) {
                $("#result").empty;
                $("#result").text(response);
            }).fail(function( jqXHR, textStatus, errorThrown ) {
                $("#result").empty;
                $("#result").text('textStatus: ' + textStatus +
                    ', errorThrown: ' + errorThrown);
            });
        });
    });
    //]]>
</script>

ダウンロードの検証はブラウザのアドレスバーにコントローラーの URL を入力して FileUpDownload を GET 要求すれば可能です。ファイル名はデフォルトで "sample1.jpg" となっていますが、クエリ文字列で別のファイルを指定できます。

Tags: , , ,

Upload Download

Web API に XML データを送信

by WebSurfer 9. August 2020 15:08

ASP.NET Web API 2 に XML 形式のデータを送信し、サーバー側のアクションメソッドで受け取る方法を書きます。(注: .NET Framework 版の話です。Core 版は未検証・未確認)

UTF-8 の場合

ASP.NET Web API で XML データを送信するというのは需要がなさそうで、あまり役には立たないかもしれませんが、せっかく考えたことなので整理して備忘録として残しておくことにしました。

(1) UTF-8 の場合

元の話は Teratail のスレッド「ASP.NET Web APIでPOSTされたXMLデータをXML形式で取得する方法」のものです。

サンプル XML データは以下の通りです。

<?xml version="1.0" encoding="utf-8"?>
<sample code="1.0">
  <data>
   <id>100</id>
  </data>
  <value>
    <name>日本語</name>
    <version>1.0.0</version>
  </value>
</sample>

この XML データを ASP.NET Web API に POST 送信し、サーバー側でアクションメソッド Post(SampleData value) の引数 value にモデルバインドするにはどうしたらよいかという話です。SampleData クラスの定義は以下の通り。属性を付与した理由は下に書きます。

namespace WebApi.Models
{
    [System.Xml.Serialization.XmlRoot("sample")]
    public class SampleData
    {
        [System.Xml.Serialization.XmlAttribute("code")]
        public string Code { get; set; }

        [System.Xml.Serialization.XmlElement("data")]
        public Data Data { get; set; }

        [System.Xml.Serialization.XmlElement("value")]
        public Value Value { get; set; }
    }

    public class Data
    {
        [System.Xml.Serialization.XmlElement("id")]
        public string Id { get; set; }
    }

    public class Value
    {
        [System.Xml.Serialization.XmlElement("name")]
        public string Name { get; set; }

        [System.Xml.Serialization.XmlElement("version")]
        public string Version { get; set; }
    }
}

XML データの文字コードが UTF-8 であれば以下の 3 つの設定でこの記事の一番上の画像の通りモデルバインドできます。

  1. 要求ヘッダに Content-Type: application/xml; charset=UTF-8 の設定

    Microsoft のドキュメント How WebAPI does Parameter Binding には content-type の設定だけで済みそうなことが書いてありますが、自分が試した限りでは下の 2, 3 も必要でした。
  2. App_Start/WebApiConfig.cs へ以下の設定の追加

    config.Formatters.XmlFormatter.UseXmlSerializer = true;  
  3. モデルのクラス、プロパティに XmlRoot, XmlAttribute, XmlElement 属性の付与

    xml コードの要素名とそれをバインドするクラス、プロパティ名を関連付けるために必要です。たとえ同名でも、上の例のように大文字小文字の違いがある場合は、プロパティにXmlElement 属性を付与してその引数に xml の要素名を設定しないとバインドされません。また、xml コードの属性値(この記事の例では code)をバインドする場合は、それに該当するプロパティへの XmlAttribute 属性の付与が必要です。

(2) Shift_JIS の場合

XML データの文字コードが Shift_JIS の場合はどうでしょう? (これも Teratail のスレッド「ASP.NET Web API POSTされた文字エンコードShift_JISのXMLデータをモデルバインドできない」の話です)

上の「(1) UTF-8 の場合」のような方法ではうまくいかないようです(null がバインドされる)。もちろん XML データを encoding="shift_jis" とし、要求ヘッダを Content-Type: application/xml; charset=shift_jis としたのですがダメでした。

検索するなどして調べてみましたが成果がなかったです。Shift_JIS ではヒットしないので UTF-16 に範囲を広げてググってみましたが、stackoverflow のスレッド Web API not able to bind model for POST with utf-16 encoded XML など、できなかったという記事しか見つかりませんでした。

そこを何とかするには、Microsoft のドキュメント How WebAPI does Parameter Binding の最初の方に書いてあるように、HttpRequestMessage クラスを引数に持つメソッドを使う他には手がなさそうな感じです。

HttpRequestMessage.Content プロパティで HttpContent オブジェクトを取得し、HttpContent.ReadAsStringAsync メソッドで POST されてきた XML 文字列を読んで、XDocument.Load メソッドで XDocument オブジェクトを作ってそれを操作するようにしてみました。

なお、この方法を取った場合、上の「UTF-8 の場合」で行った「2. App_Start/WebApiConfig.cs へ以下の設定の追加」と「3. モデルのクラス、プロパティに XmlRoot, XmlAttribute, XmlElement 属性の付与」は必要ありません。

検証は以下のようにしました。

まず Fiddler の Composer を使って、要求ヘッダに Content-Type: application/xml; charset=shift_jis を付与し、Request Body に XML データを設定して送信します。

Fiddler の Composer で送信

Fiddler の Inspectors で HexView を選択し、Request Body に設定して送信した XML データの 16 進数表現をチェックします。XML データの中の <name>日本語</name> の「日本語」の文字コードは 93 FA 96 7B 8C EA と Shift_JIS になっているのが分かります。

要求ヘッダとボディの 16 進数データ

Visual Studio のデバッガでアクションメソッドの Local 変数を確認します。「日本語」は文字化けせず期待通り取得できています。

アクションメソッドの Local 変数

ちょっと予想外だったのは、Fiddler の Composer の Request Body に書いた「日本語」の文字コードが Shift_JIS になることと、HttpContent.ReadAsStringAsync メソッドがそれを正しく「日本語」と読んでくれることです。そんなに都合よくできているというのが信じ難く、何か見落としがあるかも。(汗)

Tags: , , ,

Web API

Web API と要求ヘッダの application/xml

by WebSurfer 7. June 2020 14:12

.NET Framework 版の ASP.NET Web API を要求する際、要求ヘッダの Accept に application/xml が含まれると応答は JSON ではなく XML になります。

ASP.NET Web API の応答

知ってました? 実は自分は知らなかったです。(汗) ちなみに、MVC の Json メソッド、および Core 3.1 Web API と MVC の Json メソッドは、要求ヘッダの Accept に application/xml が含まれていても JSON が返ってきます。

Web API の応答をチェックするには Postman などのツールを使うのが一般的なようです。でも、GET 要求ならブラウザのアドレスバーに直接 URL を入力して応答を見ることでも十分だったので自分は IE11 を使ってそうしてました。それで期待した通り JSON 文字列が返ってきてました。

ところが、Chrome, Edge, Firefox では応答が JSON ではなく XML になってしまいます。理由は要求ヘッダの Accept に application/xml が含まれるからのようです。ハマったのは上の画像のようなサーバーエラーとなってしまったことです(HTTP 500 応答が返ってきます。上の画像は形式は XML ですが中身はエラーメッセージです)。

エラーメッセージ "The 'ObjectContent`1' type failed to serialize the response body for content type 'application/xml; charset=utf-8'." によるとシリアライズに失敗したということです。

IE11 や jQuery ajax を使った検証ではシリアライズには問題はないことは確認しており、ブラウザに Chrome などを使ったからと言ってそれがシリアライズの部分に影響があるとは考えられなかったです。何故なのでしょう?

循環参照の問題でした。

上の画像の InnerException のエラーメッセージにある Blog_E36679EB... D5AD0 という文字列が、先の記事「JSON シリアライズの際の循環参照エラー」にもあったのを思い出して気が付きました。

.NET Framework 版の ASP.NET Web API の JSON シリアライザは Newtonsoft の Json.NET のもので、JsonIgnoreAttribute クラスという属性が利用できます。なので、問題のプロパティに [JsonIgnore] を付与してシリアライズの際の循環参照エラーは回避していました。

でも [JsonIgnore] が有効なのは JSON にシリアライズするときのみで、XML にシリアライズするときは循環参照の問題は回避できません。

Chrome, Edge などを使った時は、ASP.NET Web API は要求ヘッダの Accept に application/xml が含まれるのを見て、XML にシリアライズしようとして循環参照の問題に陥ったということのようです。

Tags: , , ,

Web API

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  May 2021  >>
MoTuWeThFrSaSu
262728293012
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar