WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ASP.NET Core MVC でファイルアップロード

by WebSurfer 2020年1月19日 12:33

ASP.NET Core 3.1 MVC アプリ(注:.NET Framework の ASP.NET MVC ではありません)でファイルをアップロードする方法について書きます。

ASP.NET Core MVC でファイルのアップロード

.NET Framework ベースの(Core ではない)MVC5 アプリでファイルをアップロードする方法は、先の記事「MVC でファイルのアップロード」に書きましたのでそちらを見てください。

上のリンクの MVC5 アプリの記事と同様に、普通に form を submit して POST 送信する場合と、jQuery Ajax を利用して非同期で送信する場合の両方の例を紹介します。ちなみに、上の画像は jQuery Ajax を使ってアップロードした結果です。

本題に入る前に、Microsoft の記事「ASP.NET Core でファイルをアップロードする」の「セキュリティの考慮事項」のセクションを一読することをお勧めします。

この記事では上に紹介した Microsoft の記事に書かれたセキュリティ関する配慮がされていませんので注意してください。例えば「アプリと同じディレクトリツリーに、アップロードしたファイルを保持しないでください」とありますが、この記事ではアプリケーションルート直下の UploadedFiles というフォルダに、アップロードされたファイルをチェックせず、ユーザーによって指定されたファイル名でそのまま保存するコードになっています。

この記事ではセキュリティの話はちょっと置いといて、単純にファイルをアップロードするにはどうするかということを書きます。気をつけるべき点は以下の通りです。

  1. View では form 要素の enctype 属性に "multipart/form-data" が設定されるようにする。
  2. Controller のアクションメソッドでは、アップロードされたファイルがバインドされるパラメータまたはクラスのプロパティは IFormFile 型であること。(MVC5 の HttpPostedFileBase 型ではなくて)  
  3. 上で述べたバインドされるパラメータまたはクラスのプロパティの名前は、html ソースの <input type="file" ... /> の name 属性と一致させる。
  4. Internet Explorer (IE) でファイルをアップロードすると、クライアント PC でのフルパスがファイル名として送信されることがある(先の記事「IE でアップロードする際のファイル名」を参照)。その場合、IFormFile.FileName でファイル名を取得するとクライアント PC でのフルパスになるので、必ず Path.GetFileName を使うこと。  
  5. ワーカープロセスがアップロードするホルダに対する「書き込み」権限を持っていること。(IIS でホストする場合です。Kestrel の場合も権限が必要なのは同じだと思いますが詳しいことは未調査で分かりません)
  6. IIS でも Kestrel も最大要求本文サイズに 30,000,000 バイトの制限がある。詳しくは上に紹介した Microsoft の記事の「IIS の内容の長さの制限」または「Kestrel の最大要求本文サイズ」のセクションを見てください。変更方法も書いてあります。

jQuery Ajax を使ってファイルをアップロードする場合は、上記に加えて以下の点に注意してください。

  1. XMLHttpRequest を使用して送信するためのキーと値のペアのセットを取得するために FormData オブジェクトを利用する。詳しくは MDN の記事「FormData オブジェクトの利用」にありますのでそちらを参照してください。
  2. ASP.NET Core MVC 組み込みの CSRF 防止機能は Ajax でもそのまま使えるので、Controller のアクションメソッドへの [ValidateAntiForgeryToken] を忘れずに設定する。(ASP.NET Core 2.0 以降では FormTagHelperが HTML フォームの要素に偽造防止トークンを挿入するので、View で明示的に @Html.AntiForgeryToken() を書く必要はないそうです。詳しくは Microsoft の記事「ASP.NET Core でのクロスサイト要求偽造 (XSRF/CSRF) 攻撃を防ぐ」を見てください。

上の画像を表示するのに使ったコードを以下に書いておきます。

Model

using Microsoft.AspNetCore.Http;

namespace MvcCoreApp.Models
{
    public class UploadModels
    {
        public string CustomField { get; set; }
        public IFormFile PostedFile { get; set; }
    }
}

View

@model MvcCoreApp.Models.UploadModels

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

<h1>Upload</h1>

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

    <div class="form-group">
      <div class="col-md-10">
        <input type="button" id="ajaxUpload" 
            value="Ajax Upload" class="btn btn-primary" />
        <div id="result"></div>
      </div>
    </div>
  </div>
</div>

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

      // 追加データを以下のようにして送信できる。フォーム
      // データの一番最後に追加されて送信される
      fd.append("CustomField", "This is some extra data");

      $.ajax({
        url: '/fileupload',
        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>
}

Controler / Action Method

Core では Server.MapPath メソッドと Request.IsAjaxRequest メソッドが使えない点に注意してください。それに代わる手段は以下のコードに書いてあります。

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

namespace MvcCoreApp.Controllers
{
  public class UploadController : Controller
  {
    // Core では Server.MapPath が使えないことの対応
    private readonly IWebHostEnvironment _hostingEnvironment;

    public UploadController(
                    IWebHostEnvironment hostingEnvironment)
    {
      _hostingEnvironment = hostingEnvironment;
    }

    [HttpGet("/fileupload")]
    public IActionResult Index()
    {
        return View();
    }

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

        // アプリケーションルートの物理パスを取得。Core では
        // Server.MapPath は使えないので以下のようにする
        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 = "ファイルアップロードに失敗しました";
      }

      // Core では Request.IsAjaxRequest() は使えない
      if (Request.Headers["X-Requested-With"] == 
                                          "XMLHttpRequest")
      {
        return Content(result);
      }
      else
      {
        ViewBag.Result = result;
        return View();
      }
    }
  }
}

Tags: , , , ,

Upload Download

ASP.NET Core 3

by WebSurfer 2019年12月15日 20:53

ASP.NET Core 3 の本

ASP.NET Core の勉強を始めようと思って左の画像の本を買って PC に Visual Studio Community 2019 をインストールしました。

自分が今まで使ってきた Visual Studio 2015 にはすでに ASP.NET Core の開発環境が提供されていたのですが、先の記事に書いた「ASP.NET Core で文字化け」という問題があったのです。

まだ枯れてないフレームワークには他にもいろいろ不具合がありそうな気がしましたし、勉強を始めたはいいが途中で開発中止になってしまう可能性もないわけではありませんでしたし、手を出すのは時期尚早と思ってました。(言い訳です(笑))

どうやら開発中止になるどころか Microsoft も Core への移行を推奨するそうですし、2019 年 11 月にバージョン 3.1 がリリースされて安定してきたそうなので、時代に取り残されてはいけないと思って本を買ってきた次第です。

実は、上に紹介した時事を書いた頃少しだけ勉強したのですが、サーバー周りが旧来の ASP.NET とは大幅に異なっていて、今の知識が役に立たなそうなのにビビってました。

Model, View, Controller のコードの基本的なところにはそんなに違いはないようですが、それでもやはり従来の MVC の知識が通用しないところがありそうです。

.NET Framework ベースの ASP.NET の知識も十分とは言えない自分が余計なものに手を出すべきではないのかもしれません。

でも、ソフト開発でメシを食っているわけではない自分にとっては所詮趣味の問題に過ぎないので、自己満足できればいいと思うことにします。(笑)

Tags:

CORE

ASP.NET Core で文字化け

by WebSurfer 2017年6月16日 20:37

ASP.NET Core のプロジェクトで、スキャフォールディング機能を利用して生成した View に日本語を書き込むと、文字化けするという話を書きます。元の話は Teratail の記事「ASP.NET Core でスキャフォールディングすると文字化けする」です。(注: VS2015 ASP.NET Core v1 の話です。VS2019 ASP.NET Core v3.1 では直っていました)

ASP.NET Core での文字化け

上の画像で赤枠で囲った部分が問題の文字化けです。どのような理由でそうなるかを以下に説明します。

(注:何行にもわたって書き込むとサーバーエラーになり、"An error occurred during the compilation of a resource required to process this request. Please review the following specific error details and modify your source code appropriately. "�" is not valid at the start of a code block. Only identifiers, keywords, comments, "(" and "{" are valid." というエラーメッセージが出ることもあります)

検証に使った環境は以下の通りです。Visual Studio 2017 でも同じ問題が出るとのことです。

  • Visual Studio 2015 Community 2015 Update 3
  • ASP.NET Core Web Application (.NET Core) のテンプレート使用
  • Windows 10 Professional 64-bit
  • IIS 10.0 Express

そもそもの原因は、スキャフォールディング機能を利用して生成した View の文字コードが Shift_JIS になってしまうことです(日本語 OS の場合)。.NET Framework ベースの MVC プロジェクトの View はデフォルトでは BOM 付きの UTF-8 になります。それを Shift_JIS に変換しても文字化けの問題は出ません。何故か、Core では Shift_JIS では対応できず、上の画像のように文字化けします。

Fiddler を使って応答をキャプチャし、問題の文字 "�" のバイト列を調べてみたら EF BF BD となっていました。

MSDN ライブラリ「.NET で文字エンコーディング クラスを使用する方法」によると "Unicode デコーダーでは、デコードできない 2 バイトのシーケンスが REPLACEMENT_CHARACTER (U+FFFD) に置き換えられます" とのことです。U+FFFD は UTF-8 のバイト列では EF BF BD となります。

ということは、Shift_JIS の日本語の文字列は(多分、他の非 ASCII 文字も)、ASP.NET Core ではその文字列を処理する際デコードできず、REPLACEMENT_CHARACTER に置き換えられ、その UTF-8 のバイト列 EF BF BD がサーバーから送られてきたということのようです。

何故 Core ベースのアプリでは View の文字コードが Shift_JIS になってしまうのか、何故 Shift_JIS ではデコードできないのかは不明ですが、解決するには View の文字コードを UTF-8 にするだけで OK です。

ただし、Visual Studio のオプション設定などで自動的に UTF-8 で保存する方が見つかりません。なので、プリミティブな方法ですが、メモ帳で当該 View を開いて UTF-8 で保存し直すことでとりあえず解決しました。

最後に、上の画像を表示した View をどのように作ったかを書いておきます。

まず、以下のような Controller と Model を作ります。(Controller と Model が一緒になっているのは単に分けるのが面倒だったからです)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace AspNetCore.Controllers
{
  public class SampleModel
  {
    public string FirstName { set; get; }
    public string LastName { set; get; }
  }

  public class HomeController : Controller
  {
    public IActionResult Sample()
    {
      ViewData["SubTitle"] = "日本語";

      var model = new List<SampleModel>
      {
        new SampleModel() {FirstName="山田",LastName="太郎"},
        new SampleModel() {FirstName="日本",LastName="花子"}
      };

      return View(model);
    }
  }
}

上のコードにある SampleModel をベースにスキャフォールディング機能を利用して View を自動生成します。具体的には、以下の画像のように、Visual Studio のソリューションエクスプローラで、View のフォルダを右クリック ⇒[追加(D)]⇒[ビュー(V)...]で「ビューの追加」ダイアログを開き、必要な情報を入力して[追加]ボタンをクリックします。

「ビューの追加」ダイアログ

生成される View のコードは以下のようになります。文字コードは Shift_JIS になりますが、この時点では日本語が含まれていませんので問題は出ないです。

@model IEnumerable<AspNetCore.Controllers.SampleModel>

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

<h2>Sample</h2>

<p>
  <a asp-action="Create">Create New</a>
</p>
<table class="table">
  <thead>
    <tr>
      <th>
        @Html.DisplayNameFor(model => model.FirstName)
      </th>
      <th>
         @Html.DisplayNameFor(model => model.LastName)
      </th>
      <th></th>
    </tr>
  </thead>
  <tbody>
@foreach (var item in Model) {
    <tr>
      <td>
        @Html.DisplayFor(modelItem => item.FirstName)
      </td>
      <td>
        @Html.DisplayFor(modelItem => item.LastName)
      </td>
            
      ・・・中略・・・

    </tr>
}
  </tbody>
</table>

Visual Studio のエディタから上のコードの一部を、以下のように日本語にします。

@model IEnumerable<AspNetCore.Controllers.SampleModel>

@{
    ViewData["Title"] = "日本語";
}

<h2>@ViewData["SubTitle"]</h2>

<p>日本語</p>

<p>
  <a asp-action="Create">Create New</a>
</p>

・・・以下略・・・

これを Visual Studio のバイナリエディタで開いたのが以下の画像です。"日本語" の文字列が 93 FA 96 7B 8C EA と Shift_JIS のバイト列になっているのが分かるでしょうか。

バイナリエディタで表示

この状態の View を表示させたのが一番上の画像です。

文字化けしているのは ViewData["Title"] = "日本語"; として _Layout.cshtml に渡し、それから title タグに設定した文字列(一番上の画像で上の赤枠)と、<p>日本語</p> とした部分(下の赤枠)です。

Controller で ViewData["SubTitle"] = "日本語"; としたり、Model から DisplayFor 経由で View に渡した日本語の文字列は問題ありません。

一般的にまだ枯れてないフレームワークにはいろいろ不具合があると思いますが、今回の問題もそういった不具合の一つということでしょうか。

ちなみに、ビューのフォルダを右クリック⇒[追加(D)]をクリック ⇒[新しい項目(W)...]をクリック ⇒[MVC ビューページ]を選択して名前を付けて[追加(A)]をクリック・・・で生成される空の View ファイルは UTF-8 になります。なので、やはり本来は UTF-8 になるべきなのであろうと思います。

ググって調べてもこの問題は日本語の記事には見つからなかったのですが、英語の stackoverflow には同様な問題の報告がありました。

Special characters in Razor template not being encoded correctly

Asp net core MVC View creating view default encoding VS 2017

前者の記事のコメントに known issue だとして Encoding in scaffolded Items へのリンクが張ってあって、その記事によると "closed this on May 6 2016" となっていますが、直ってません。どういうことかは不明です。

Tags: , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar