WebSurfer's Home

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

MVC は maxJsonLength を無視

by WebSurfer 2013年8月7日 13:32

Ajax を使用する際、クライアントとサーバー間でやり取りするデータには JSON (JavaScript Object Notation) 形式の UTF-8 文字列を使うのがデファクトスタンダードになっています。

ASP.NET Web Forms アプリケーションで Ajax を使って Web サービスと通信を行う場合も、先の記事 ASP.NET AJAX と Web サービスjQuery AJAX と Web サービス でも書きましたように、データのやり取りには JSON 形式の文字列を使っています。

クライアントからサーバーへ送信できる JSON 文字列の長さは、セキュリティ対策のため、デフォルトで 102,400 文字に制限されています。この値は、web.config の jsonSerialization 要素 (ASP.NET 設定スキーマ) の設定によって変更できます。以下のような感じです。

<system.web.extensions>
    <scripting>
        <webServices>
            <jsonSerialization maxJsonLength="3000000"/>
        </webServices>
    </scripting>
</system.web.extensions>

上の maxJsonLength 要素の設定により、JSON 文字列のデシリアライズに使用されている(と思われる)JavaScriptSerializer クラスの MaxJsonLength プロパティの設定が書き換えられるようで、デフォルト値の 2,097,152 文字を超えて JSON 文字列を送信することも可能です。(どこまで可能かは未検証ですが、2,097,152 文字を超えて可能なのは ASP.NET 4 Web Forms および MVC3 で確認しました)

ところが、呼び出す相手が Web サービスのメソッドでなく、MVC のアクションメソッドの場合、web.config の maxJsonLength 要素の設定は無視され、文字数制限は JavaScriptSerializerクラスの MaxJsonLength プロパティのデフォルト値 2,097,152 文字となります。

このあたりは、いろいろググって調べている時に、stackoverflow の記事 Can I set an unlimited length for maxJsonLength in web.config? を見つけて分かりました。そのページの Answers の 2 つ目に書いてあります。

いろいろ調べましたが、クライアントからサーバーに送信される JSON 文字列の長さ制限を変更する方法は見つかりませんでした。JavaScriptSerializer クラスの MaxJsonLength プロパティのデフォルト値 2,097,152 文字で固定となってしまいます。

ただし、逆方向のサーバーからクライアントに送信される JSON 文字列の長さ制限(MaxJsonLength プロパティのデフォルト値 2,097,152 文字)を緩和するための workaround はあります。詳しくは、上に紹介した stackoverflow のページに書いてありますので見てください。

クライアントからサーバーに送信する JSON データの文字数の制限を変更する方法はないか調べているときにわかったこと(何故変更できないか)を以下に書いておきます。

MVC のアクションメソッドを呼び出す場合、2,097,152 文字を超えてクライアントからサーバーにJSON データを送信すると「ArgumentException: JSON JavaScriptSerializer を使用したシリアル化または逆シリアル化中にエラーが発生しました。文字列の長さが maxJsonLength プロパティで設定されている値を超えています。」というエラーとなります。

スタックトレースを見ると JavaScriptSerializer クラスの DeserializeObject メソッドで JSON 文字列を受け取って、それデシリアライズするときに例外(ArgumentException・・・その条件の一つが「input の長さが MaxJsonLength の値を超えています。」)が発生したことが分かります。

さらに、スタックトレースから、そのメソッドは JsonValueProviderFactory クラスの GetDeserializedObject メソッドで使われているのが分かります。CodePlex のサイトで、その ソースコード を調べたら以下のようになっていました。

private static object GetDeserializedObject(
                    ControllerContext controllerContext)
{
  if (!controllerContext.HttpContext.Request.
          ContentType.StartsWith("application/json", 
                       StringComparison.OrdinalIgnoreCase))
  {
    // not JSON request
    return null;
  }

  StreamReader reader = new StreamReader(
        controllerContext.HttpContext.Request.InputStream);
  string bodyText = reader.ReadToEnd();
  if (String.IsNullOrEmpty(bodyText))
  {
    // no JSON data
    return null;
  }

  JavaScriptSerializer serializer = new JavaScriptSerializer();
  object jsonData = serializer.DeserializeObject(bodyText);
  return jsonData;
} 

上のコードの通り、GetDeserializedObject メソッドで使われている JavaScriptSerializer オブジェクトの MaxJsonLength プロパティの設定を変更する手段はないので、MVC のアクションメソッドを呼び出している限りは制限を 2,097,152 文字を超えて設定する手段はなさそうです。

どうしても 2,097,152 文字を超えて設定する必要があれば(実際、そんな必要があるのか分かりませんが)、アクションメソッドではなく、web サービス(.asmx)を作ってそのメソッドを呼び出すようにして、web.config の maxRequestLength 要素を 2,097,152 文字を超えた値に設定することで対応が可能です。

aspnet:MaxJsonDeserializerMembers について

上記の JSON 文字数の制限に加えて、2011 年 12 月 30 日に公開されたセキュリティ更新プログラム MS11-100 で HTTP 要求内の JSON メンバーの最大数がデフォルトで 1,000 に制限されるようになりました。

この 1,000 という数は、クライアントからサーバーに送信される JSON オブジェクト内の key:value アイテムの総数です。

デフォルト値 1,000 の変更は、web.config の ASP.NET appSettings 要素で aspnet:MaxJsonDeserializerMembers キーを設定することで可能です。

この制限を越えると JsonValueProviderFactory の EntryLimitedDictionary.Add メソッドで InvalidOperationException がスローされます。

ちなみにエラーメッセージは「InvalidOperationException: JSON リクエストが大きすぎるため、逆シリアル化できませんでした。」となります。

なお、web サービス(.asmx)のメソッドにも aspnet:MaxJsonDeserializerMembers による制限が有効かどうかは未確認です。

Tags: , ,

MVC

EditorFor と DisplayFor の違い

by WebSurfer 2013年3月27日 21:38

Html Helper の EditorFor と DisplayFor が、POST 要求に対する応答で、異なった値を表示することがあります。その理由と解決策を備忘録として書いておきます。

EditFor と DisplayFor の違い

上の画像を表示した Model, View, Controller のコードは以下の通りです(説明に関係ない部分は省略してあります)。

Model

public class FileModel
{
  public string FileName { get; set; }
}

View

@model MvcApplication1.Models.FileModel

@using (Html.BeginForm())
{
  @Html.EditorFor(m => m.FileName)
  <input type="submit" value="POST" />
  @Html.DisplayFor(m => m.FileName)
}

Controller

public class FileController : Controller
{
  [HttpPost]
  public ActionResult Index(FileModel m)
  {
    m.FileName += ".ext";
    return View(m);
  }
}

ユーザーがテキストボックス(EditorFor 相当)にファイル名を入力し、form を submit(POST 要求)すると、Controller で model の FileName プロパティに拡張子 ".ext" を追加します。

この場合、応答画面では、DisplayFor には拡張子 ".ext" が追加されて表示されるものの、EditorFor には ".ext" は追加されません。 上の画像の例を見てください。

つまり、EditorFor には POST された値がそのまま使われています。

何故そうなるかと言うと、例えば、EditorFor の入力にユーザーが間違った値を入力した場合、サーバーに POST された時に検証 NG とし、エラーメッセージ(例:入力が間違っています)を表示するとともに、EditorFor にはユーザー入力をそのまま表示したいという理由だそうです。

具体的には、POST された値は ModelState ディクショナリ(model ではない)に格納されていて、Html Helper はまず ModelState ディクショナリを調べて、そこに値があればそれを表示するようになっています。

詳しくは以下のページを見てください。

ASP.NET MVC’s Html Helpers Render the Wrong Value!

value が POST されるか否かが問題になります。例えば、EditorFor に限らず、HiddenFor もその value は POST されるので、結果は EditorFor と同じになります。

一方、DisplayFor には、POST される value などというものはないので、model の値が使われます。

EditorFor にも DisplayFor と同様に model の値(拡張子 .ext が追加されたもの)を表示するには、ModelState ディクショナリを Clear することです。

そうすれば model から値を取ってくるので、期待した結果になります。具体的には以下のコードを追加します。

if (ModelState.IsValid)
{
  ModelState.Clear();
}

ただしこのようにして、検証結果 OK で POST 要求への応答をそのまま返すのは、二重 POST の問題が起こりうるので、好ましくないようです。

二重 POST 問題

MVC に限らず、Web アプリケーション開発の基本として、Post/Redirect/Get (PRG) パターンを使う・・・即ち、POST 要求への応答をそのまま返すのは検証結果 NG の場合のみとし、検証結果 OK の場合は、例え同じページを表示するにしても、リダイレクトしてブラウザに GET 要求させるのがよいそうです。(詳しくはリンク先を見てください)

Post/Redirect/Get による問題解決

Post/Redirect/Get パターンを使うように、上記のコードを書き直すと、以下のようになると思います。

public class FileController : Controller
{
  [HttpGet]
  public ActionResult Index()
  {
    FileModel model;

    object obj = TempData["ValidationResult"];
    if (obj is FileModel)
    {
      // 検証 OK ⇒ 再確認
      model = (FileModel)obj;
    }
    else
    {
      // 初期画面
      model = new FileModel();
    }
    return View(model);
  }

  [HttpPost]
  public ActionResult Index(FileModel m)
  {
    if (ModelState.IsValid)
    {
      // 検証 OK ⇒ 再確認
      m.FileName =
        ModelState["FileName"].Value.AttemptedValue
        + ".ext";
      TempData["ValidationResult"] = m;
      return RedirectToAction("Index");
    }
    else
    {
      // 検証失敗
      return View(m);
    }
  }
}

Tags:

MVC

ASP.NET MVC の勉強を始めました

by WebSurfer 2011年4月16日 21:51
ASP.NET MVC 実践プログラミング

ASP.NET MVC の勉強を始めました。

リリースされた当初は一時の流行だろうと思っていましたが、バージョン 3 がリリースされるに至って、どうもそうではなさそうな感じがします。

というわけで、時代に取り残されてはいけないと思って(笑)買ったのが左の写真の本です。

ASP.NET(MVC ではない)のいいところは、.NET Framework ベースの Windows アプリケーションの開発経験者には親しみやすいところです。

実際、自分の場合、HTML や JavaScript の知識が全くなかったのですが、Windows アプリケーションを作るときのように Visual Studio でサーバーコントロールをドラッグ&ドロップしてページに貼り付け、ウィザードを使ってプロパティを設定すれば、とりあえず動く Web アプリケーションは簡単に作成できました。

しかし、世の中の主流らしい PHP ベースの Web アプリケーションの開発者にとっては、サーバーコントロール、ポストバック、イベントドリブン、ビューステートなどの ASP.NET 固有の概念は馴染みにくいものだそうです。

それらが使えるから、ASP.NET(MVC ではない)には高い生産性があるのですが、その生産性を捨てても ASP.NET MVC を選ぶだけの理由があるかもしれないと期待しています。

ASP.NET(MVC ではない)の知識も十分でない自分が、余計なものに手を出すべきではないのかもしれませんけどね。

まぁ、Web アプリケーション開発でメシを食っているわけではない自分にとっては所詮趣味の問題に過ぎないので、自己満足でいいんです。(笑)

Tags:

MVC

About this blog

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

Calendar

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

View posts in large calendar