Kobarin's Development Blog

C#やASP.NETなどについての記録です。

信頼レベル「Medium」(Medium-Trust)のサイトでは、ASP.NET MVCが実行できない可能性

私が個人的に契約しているWindowsホスティング会社に、MVCサイトを発行し実行したところ、セキュリティエラーが発生しました(System.Security.Policy.PolicyException)。
Visual Studioの開発環境で試してもエラーが出ないため、ホスティング会社に確認したところ「信頼レベルは”Medium”」だとの事なので、その辺りを疑ってみました。
VS上で同じ環境を作って試すため、web.config内に以下の行を付け加えてみました。

<system.web>
  <trust level="Medium" />
</system.web>

その結果、VSの開発環境でも同様のエラーが発生しました。



冒頭で発行したとするMVCのサイトは、VSで自動生成されたMVCサイトのままでほとんどに手を付けていない状態。
信頼レベルの制約で疑わしい点があるとするなら、

  1. OAuth
  2. 外部APIXML)へのアクセス
  3. SqlServerへのアクセス

程度ですが、2及び3については、既に発行済のASP.NET WebFormsで実行済みなので除外しました。
とすると、残るはOAuthのみ(他にあるかもしれないけど、それがダメならMedium-Trustって何も出来ないと同義ですよね)。


そこで、「こういう事例って他に結構あるのか?」と思い探してみると、こんな書込みが。
どうやら「ASP.NETの開発チームの公式見解としては、Medium-Trustは時代遅れ」という事です。
stackoverflow.com
理由としては、

  1. 「修正不可」とされていたMedium-Trustにまつわるバグを全て解決済み(デバッグではなく、Full-Trustにより解決したと思われる)
  2. ホスティング会社に対して、Medium-Trustから移行し、代わりにOSレベルの適切な分離をするようにガイダンスを提供済み
  3. これまで開発してきたフレームワークMVCやWebAPI等)からMedium-Trustのサポートを中止済み。今後これらのフレームワークにより作られたアプリケーションにはFull-Trustが求められる。

としている。つまりMedium-Trustは使うべきではないアクセス制限機能ということになります。
なお、ここでの「Medium-Trust」というのは「信頼レベル=Medium」というだけでなく、「Full」より下の「High」「Medium」「Low」「Minimum」の4つを全て含む総称としている点に注意です。


実際、このような書込みもあります。
stackoverflow.com
この中のリプライに

But, most of hosting provider offer full trust hosting now

とあるように、Full-Trustが米国(?)では一般的なようです。
ASP.NET MVCサイトのホスティング先をお探しの方は、あらかじめ運営会社に「信頼レベル」を確認していただく事をオススメします。

ASP.NET MVCでGoogleのOAuth 2.0 の認証をする

Visual StudioASP.NET MVCサイトのテンプレートには最初からOAuthの認証機能が付いているが、幾つか引っかかったのでメモしておきます。
基本的な流れは以下のサイトによくまとめられているため、割愛します(これ以上わかりやすく書けません)。
fnya.cocolog-nifty.com

ただ、上記の記事作成時点ではベータ版だったらしく、その後仕様が変わったのか、この通りに設定・操作を進めてもログインできません。
何故か、ログインページの「Google」ボタンを押しても、何も表示されないまま元のログインページに戻ってきてしまいます。

解決方法として見つかったのは、以下のスレッドです。
stackoverflow.com
これによると、Google Developer Consoleで「Google+ API」を有効にする必要があるようです。
Google Developer Consoleの左ペイン「APIとサービス」から「ライブラリ」を押し、「Google+ API」を有効にしてから再度実行するとOAuthログインできます。

Umbraco : 1つのデータベース内に複数サイト

Umbracoの開設は、下記サイトのようなチュートリアル通りに進めればほぼ難なく出来る事がわかりました。
www.buildinsider.net

ただ、複数サイトを作成しようとすると、幾つか問題があります。
Umbracoサイト毎にデータベースを用意するのであれば問題になりませんが、自社で使う簡易的なサイトや同じ顧客のためにサイトを複数作る時など、できれば同じデータベース内で管理したいと思います。
そうした場合、上記チュートリアルの通りに進めても上手くインストール出来ません。
Webサイト側はVisualStudiodで新たに作れば良いだけですが、データベース側(SQL server)ではTABLE(cmsContent等)が重複するため、インストール途中で失敗します。

そこで、以下の流れに沿って進める事で複数サイト開設ができたのでメモしておきます(例として2サイトを作成)。

  1. Umbracoサイトを2つ作成
  2. Umbraco用のデータベースを1つ作成
  3. SqlServerログインを2つ作成
  4. データベースロールを2つ作成
  5. 各DBロールに、スキーマレベルのCREATE TABLE権限を付与
  6. Umbracoインストー

設定例

なお、各サイトのログイン設定は以下の通りとします。

項目 サイト1 サイト2
データベース UmbracoDemoDb UmbracoDemoDb
ログイン umbuser1 umbuser2
スキーマ umb1 umb2
データベースロール umb_role1 umb_role2

それでは進めてみましょう。

手順

1.Umbracoサイトを2つ作成

Visual StudioでUmbracoのプロジェクトを2つ作成しましょう。この部分は説明の必要はないと思うので割愛します。

2.Umbraco用のデータベースを作成

SSMSでデータベースをUmbraco用に1つ作成します。ここでは「UmbracoDemoDb」というDBにします。

3.SqlServerログインを作成
  1. SqlServerManagementStudioを開く
  2. [セキュリティ]→[ログイン] で右クリックし、「新しいログイン」
  3. [ログイン名] に「umbuser1」を入力
  4. [SQL Server認証]を選択し、パスワードを登録
  5. [既定のデータベース]に「UmbracoDemoDb」を選択
  6. [ユーザーマッピング]で「UmbracoDemoDb」を選択し、[ユーザー]に「umbuser1」、[スキーマ]に「umb1」を入力

一般的な説明では「データベース ロール メンバーシップ」の「db_owner」にチェックを入れますが、ここはオフで。チェックを入れるとインストールに失敗します。

4.データベースロールを作成
  1. SSMSで [UmbracoDemoDb]→[セキュリティ]→[ロール]→[データベースロール] を右クリックし、「新しいデータベースロール作成」
  2. [ロール名]に「umb_role1」を入力
  3. [所有者]に「dbo」
  4. [このロールが所有するスキーマ]に「umb1」
  5. [このロールのメンバー]に「umbuser1」を選択
5.DBロールに、スキーマレベルのCREATE TABLE権限を付与

最後に、SSMSで以下のクエリーを実行します。

USE UmbracoDemoDb
GRANT ALTER ON Schema :: umb1 TO umb_role1
GRANT CREATE TABLE TO umb_role1
GO

このコードは以下の書込みを参考にしました。
serverfault.com
なお、実行すると以下のメッセージが表示されますが、正常に完了しているので大丈夫です。

sa、dbo、エンティティ所有者、information_schema、sys、または自分自身に対する権限を許可、拒否、または取り消すことはできません。

6.Umbracoインストー

以上で1サイト用のログインは完成です。
Umbracoのインストールで、ここで設定したログインを入力すれば、スキーマ付きでTABLEが登録されます。

2つ目以降のサイトも同様に、ログイン名・データベースロール名・スキーマ名を変えて設定し、Umbracoのインストール画面でログインすれば、指定したスキーマでTABLEが作成されます。
あと、Umbracoのよくある解説サイトでは、ログインを作成する際に「db_owner」を選択しますが、これをしてしまうとインストール時に失敗します(当方の場合は「umbracoMigrationが無効」と出ました)。
どうもアクセスできてしまうだけでエラーになってしまうので、ここではdb_ownerを外す事で指定スキーマにしかアクセス出来ないようにしています。

Umbraco+ASP.NET MVCによるサイト作成

UmbracoとMVCの併存サイトを作ったのでメモします。
まずは当方の環境から。

はじめに

まず参考にさせていただいたのは以下の記事です。
www.buildinsider.net
この記事ではUmbracoのバージョンは6.1.1、MVCはver.4なので、やや古い印象はありますが概ね内容通り進めれば問題ないと思いますが、幾つか追加事項があるのでそれも含めて説明していきます。

A. 新規Webサイトの作成

  1. Visual Studio 2017(以降、VSと略)で新規プロジェクトを作成し、「ASP.NET Web Application (.NET Framework)」を選択
  2. テンプレートから「MVC」を選択
  3. これで空のMVCサイトが出来上がりましたが、この後Umbracoインストール時に上書きされる、ルート直下のweb.configを何処かにバックアップしておきましょう

B. Umbracoのインストー

  1. メニューの[Tools] → [Nuget Package Manager] → [Manage Nuget Packages for Solution]
  2. 「UmbracoCMS」を検索し、プロジェクトに[Install]
  3. ここまでが参考記事2までに沿った流れです

C. web.configの編集

  1. Umbracoによるweb.configとMVCのweb.config(A-3でバックアップしたファイル)をマージする(参考記事3の箇所)。1つ1つタグを追っていき、互いに存在しない行を付け足していきます。

D. 初期設定

  1. プロジェクトをBuildし、[Debug] → [Start Without Debugging]でサイトを起動
  2. この時、web.config周りでエラーが出たら、いったんコメントアウトして再度試してみましょう(私の場合も、忘れましたが幾つか削除しました)
  3. ブラウザが開き、ログインアカウントの設定画面が出ます。そのまま[Install]するとSQL Server Compact EditionとしてApp_Dataフォルダの下の.sdfファイルをデータベースとして使うことになります。SQL serverを使う場合は、[Customize]を押すとサーバー設定が出来ます。後々SQLserverで動作させる予定があるならこの場で設定しておきましょう。なお、後からweb.configのConnetionStringを変更しても上手く動作しませんでした(初期画面の女性が多く写った写真を背景にインジケーターがずっと点滅した場が続きました)。おそらくCEからSQLへのデータ移行を行う必要があるのかもしれません。

E. ルーティング設定

ASP.NET MVCアプリケーションの場合、既定では例えば http://example.jp/Product/Details/123 というアドレスを指定すると、Controller=Product、のAction=Details、id=123 としてルーティングされます。
しかしUmbracoをインストールしたことによりこのルーティング設定は無効化され、あらゆるアクセスがUmbracoで処理されることになります(RouteConrigが削除されたり書き換えられるわけではありません)。
本稿はMVCとの併存サイトを構築することが狙いなので、MVCアプリケーションにもアクセスできるようカスタムルーティングを設定する必要があります。
Umbracoを扱う以上は、基本は全てUmbracoで処理する事になると思われます。その中で、例えば「オンラインショップ部分のみMVCで動かしたい」と考えた場合、http://example.jp/onlineshop/で始まるをMVCアプリケーションとしてルーティング設定することになります。
参考記事5の通り、どこでも良いのでクラスファイルを作り、以下のように作成(参考記事ではGlobal.asax内だが、当方はApp_Code内に記載した)。
Global.asaxのApplication_Start内の文をOnApplicationStarted内に記述。

public class AppStart : IApplicationEventHandler
{
  public void OnApplicationInitialized(UmbracoApplicationBase umbracoApplication,
  ApplicationContext applicationContext)
  { }

  public void OnApplicationStarting(UmbracoApplicationBase umbracoApplication,
     ApplicationContext applicationContext)
  {
  }

  public void OnApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
  {
    AreaRegistration.RegisterAllAreas();
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
  }
}

次に、App_Start/RouteConfig.csを開き、以下のように書き直す。

public static void RegisterRoutes(RouteCollection routes)
{
  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
  routes.LowercaseUrls = true;  //小文字の有効化

  routes.MapRoute(
    name: "OnlineShop",
    url: "OnlineShop/{action}/{id}",
    defaults: new { controller = "OnlineShop", action = "Index", id = UrlParameter.Optional },
    namespaces: new[] { "(アプリケーション名).Controllers" }
  );
}

これにより、/onlineshop/で始まるアドレスのみMVCアプリケーションとして動作する事になります。
ちなみにnamespace部分は必ずしも必要ではなく、参考記事にも書かれていません。
ただ、当方の場合プロジェクト名を途中で変更したせいか旧プロジェクトのControlerが残ってしまったらしく、以下のような例外が発生してしまってました。

Multiple types were found that match the controller named 'OnlineShop'. This can happen if the route that services this request ('OnlineShop/{action}/{id}') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.

特に必要なければそのままでよいですが、もし上のようなエラーが現れる場合は、MapRouteでnamespaceを付け加えてみて下さい。
これについては、自分なりに探して以下に情報を参考にしました。
stackoverflow.com

F. UmbracoReservedPathsの設定

最後に、参考記事にもあるようにweb.configのappSettings内のUmbracoReservedPathsに以下のように追記して終了です。

<add key="umbracoReservedPaths" value="~/umbraco,~/install/,~/Content,~/Bundles"  />

以上で動作すると思われます。

SaveChanges()による特定フィールドの更新

EntityFrameworkを使って更新する一般的な事例として、よく以下のようなコードを見かけます。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Product product)
{
  if(ModelState.IsValid)
  {
    db.Entry(product).State = EntityState.Modified;
    db.SavaChanges();
    
    return RedirectToAction("List");
  }
  return View(product);
}

確かに全フィールドを対象としたフル更新型のフォームではこれで正しいですが、「価格だけの更新」「フラグの項目だけ更新」といったように特定フィールドだけの部分更新をしたい場面も多々あると思います。
以下の回答を参考にしたので、解説いたします。
stackoverflow.com

価格フィールドに限定した更新の例

まず上記コードを基に部分更新に作り変えたのが以下のコードです。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult EditPrice([Bind(Include = "ProductId,ListPrice")] Product product)
{
  if(ModelState.IsValid)
  {
    db.Products.Attach(product);  // Attachが必要。後述。
    db.Entry(product).Property(m => m.ListPrice).IsModified = true; // 対象フィールドを更新指定
    db.SavaChanges();
    
    return RedirectToAction("List");
  }
  return View(product);
}

こんな感じで、数行の変更で部分更新が完成です。

説明

忘れないでほしいのは

db.Products.Attach(product);

の部分で、これを書かないと以下のようなエラーが出ます。

Member 'IsModified' cannot be called for property 'ListPrice' because the entity of type 'Product' does not exist in the context. To add an entity to the context call the Add or Attach method of DbSet.

ちょっと意味がわかりにくですが簡単に説明すると、
「いきなり『db.Entry(product)』してもエンティティが出来ないので、AddかAttachしてくれよ」
みたいな意味のようです。
今回は追加ではなく更新なのでAttachになります。

次の行で更新対象のフィールドを指定しています。

db.Entry(product).Property(m => m.ListPrice).IsModified = true;

もちろん、複数ある場合は同じように

db.Entry(product).Property(m => m.ListPrice).IsModified = true;
db.Entry(product).Property(m => m.StandardCost).IsModified = true;
db.Entry(product).Property(m => m.Name).IsModified = true;

のように付け足すだけです。

ASP.NET MVC で複数レコードを一括更新するフォーム

MVCのScaffoldilng機能は秀逸で、Index、Details、Create、Edit、DeleteのController+Viewをものの数秒で作り上げてくれます。
ただ、Create・Edit・Deleteいずれも1レコード単位の機能であり「一括変更」する機能はないため、自前で作る必要があります。
本稿では、特に利用頻度が多い思われるEdit(編集機能)の一括機能を取り上げていきたいと思います。


MVCではWebPagesに比べると簡単に実現できるのですが、実はちょっと癖があります。
ここでは、一般的に考えつく方法(IEnumerable + foreach)では出来ない事を示しつつ説明していくので、手っ取り早く方法を知りたい方はいきなり後半に飛んで下さい。

Controllerの記述

Controller側としては、こんな流れを想定します。

  • モデルの一覧表示
  • 受取側(HttpPost側)でforeachで1件ずつ更新処理
public ActionResult PostMultiRows()
{
  // 一覧の取得
  var products = from p in db.Products
                 select p;

  return View(products);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult PostMultiRows(IEnumerable<Product> products)
{
  MyRepository rp = new MyRepository(); // Productの取得や更新を行うレポジトリが既に作成されていると仮定

  // 1件ずつ更新
  foreach(var p in products)
  {
    rp.UpdateProduct(p);
  }

  return View("PostMultiRows");
}

Viewの記述

View側では、モデル一覧をバインドしつつ、EditorForで入力欄を用意します。
ここでは、簡略化するために、ProductId、Name、StandardPrice、ListPriceの4項目に絞っています。

@model IEnumerable<MvcDemo.Models.Product>

@using (Html.BeginForm())
{
  @Html.AntiForgeryToken()

  <table class="table">

    @foreach (var item in Model)
    {
      <tr>
        <td>
          @Html.HiddenFor(m => item.ProductID)
          @Html.EditorFor(m => item.Name, new { htmlAttributes = new { @class = "form-control" } })
        </td>
        <td>
          @Html.EditorFor(m => item.StandardCost, new { htmlAttributes = new { @class = "form-control" } })
        </td>
        <td>
          @Html.EditorFor(m => item.ListPrice, new { htmlAttributes = new { @class = "form-control" } })
        </td>
      </tr>
    }
  </table>

  <div>
    <input type="submit" value="SUBMIT" class="btn btn-primary" />
  </div>
}

ひとまず完成。早速POSTしてみる。

簡単ですがひとまず出来上がりましたので、SUBMITボタンを押すと‥

オブジェクト参照がオブジェクト インスタンスに設定されていません

foreach(var p in products)

こんなエラーメッセージが現れるのではないでしょうか?
ASP.NETで最も良く見るエラーかもしれませんが、foreachの冒頭で発生していることから、うまく受け渡しができていない事がわかります。

foreachでは送信できない。for-loopが必要。

解決方法を言ってしまうと、View側ではforeachでなくforでループさせる必要があります。
当方の力不足のためハッキリした理由は説明できません(StackOverFlowで見つけただけ)ので、HTMLソースを見ての推測になりますが、
inputコントロール名が「[5].StandadCost」等となっている事から、おそらくループ内の各コントロールを識別可能になった事が原因ではないかと考えます。
また、これに伴い、IEnumerableではfor-loop指定ができないため、ViewモデルをIList<モデル名>に変更する必要もあります。

@model IList<MvcDemo.Models.Product>

@using (Html.BeginForm())
{
  @Html.AntiForgeryToken()

  <table class="table">

    @for (int i = 0; i < Model.Count(); i++)
    {
      <tr>
        <td>
          @Html.HiddenFor(m => Model[i].ProductID)
          @Html.EditorFor(m => Model[i].Name, new { htmlAttributes = new { @class = "form-control" } })
        </td>
        <td>
          @Html.EditorFor(m => Model[i].StandardCost, new { htmlAttributes = new { @class = "form-control" } })
        </td>
        <td>
          @Html.EditorFor(m => Model[i].ListPrice, new { htmlAttributes = new { @class = "form-control" } })
        </td>
      </tr>
    }

  </table>

  <div>
    <input type="submit" value="SUBMIT" class="btn btn-primary" />
  </div>
}

これにより、更にContoller側にも変更が生じます。
IEnumerable型をList側に変換してViewに渡すようにして下さい。

public ActionResult PostMultiRows()
{
  var products = _rp.GetProducts(1, null).ToList();

  return View(products);
}

これで実行可能です。

いかがでしたでしょうか?
StackOverFlowでも質問している人が何人かいた事からも結構引っかかりやすいかもしれませんが、知ってしまえばかなり簡単だと思います。

ASP.NET MVC のDisplayFormatを表示用と編集用で分ける方法

タイトルが分かりにくいですが、正確には

  • DisplayFormat属性のDataFormatStringを、表示のみする場面(例:Index、Details)と編集する場面(例:Create、Edit)で別々に作る

といった内容です。

結論「別々に作る事はできない」

という事で、当然ですがDataFormatStringは1つしか作れないため、プロパティ自体を表示用と編集用に別々に用意する事で対応しましたが、とりあえず1つ1つ流れを見ていきます。

例えば、以下のようなProductエンティティがあるとします。

public class Product {
  public int ProductID { get; set; }
  …
  public decimal StandardCost { get; set; }
  …
}

まずはDisplayFormat

StandardCostはdecimal型なので、上記のままだと表示時も編集時も「1000.00」のような表記になりますが、このままだと見にくいと感じます。
そこで、期待する表記は「3桁区切りカンマ、小数点以下は必要次第で表示」だとします。

  • 例1:「1000」の場合なら「1,000」
  • 例2:「1234.5」の場合なら「1,234.5」

そこでDisplayFormatを下記のように記述します。

public class Product {
  …
  [DisplayFormat(DataFormatString = "{0:#,##0.#}", ApplyFormatInEditMode = true)]
  public decimal StandardCost { get; set; }
  …
}

ApplyFormatInEditMode は、編集時でも同じフォーマットを適用する場合に使う属性です(既定でfalse)。

編集の時はカンマ不要

これで期待する通りの表示になりますが、困った事に編集用のTextboxでも「1,000」のようにカンマが現れてしまいます。
しかもそのままだと検証に引っかかり「The field StandardCost must be a number(StandardCostは数字でなければなりません)」と修正を求められます。変更もないのにカンマを削除するのは面倒ですよね。


それでは…とApplyFormatInEditModeを取り除いてみます。

public class Product {
  …
  [DisplayFormat(DataFormatString = "{0:#,##0.#}")]
  public decimal StandardCost { get; set; }
  …
}

こうすると表示は変化ありませんが、編集では「1000.00」と小数点以下の余計な部分が現れてしまいます。
このままでも問題はないのですが、何となく鬱陶しいですよね。

DisplayFormatの限界…(面倒だけど)プロパティを2つ作る

このようにDisplayFormatは、表示と編集とで表記を統一する機能はあっても、別々に定義することが出来ないのです。
そこで苦肉の策となりますが、別々に定義するのであれば、以下のようにプロパティを2つに分けるしかありません。

public class Product {

  // 編集用
  [DisplayFormat(DataFormatString = "{0:#0.#}", ApplyFormatInEditMode = true)]
  public decimal StandardCost { get; set; }

  // 表示用
  [DisplayFormat(DataFormatString = "{0:#,##0.#}")]
  public decimal StandardCostDisplay { get { return StandardCost; } }
}

「表示用」でStandardCostDisplayというStandardCostを参照するだけのプロパティを作り、希望通りのフォーマットにします。
一方、「編集用」ではApplyFormatInEditModeを付け、カンマは適用しないフォーマットにします。

Viewに少し修正を加えて終了

最後に、View側を少し修正し、IndexやDetailsといった表示のViewではStandardCostDisplayをバインドし、CreateやEditはStandardCostをバインドさせます。

Index, Details
  <td>
    @Html.DisplayFor(model => model.StandardCostDisplay)
  </td>
Create, Edit
  @Html.EditorFor(model => model.StandardCost, new { htmlAttributes = new { @class = "form-control" } })

希望

本当はEditFormatみたいな属性があれば最も話が早いと思うんですが…まぁ現状ではこれしか思いつきませんでした。