.NET Framework 版の ASP.NET MVC アプリでは、Controller で作成されて View に渡された匿名型のオブジェクトには View 内部ではアクセスできず、
アクセスしようとすると以下の画像のように RuntimeBinderException がスローされるということを書きます。
理由は、Microsoft のドキュメント「匿名型」に書いてあるように、「匿名型のアクセシビリティ レベルは internal であるため」です。internal 型またはメンバは、同じアセンブリのファイル内でのみしかアクセスできません。
.NET Framework 版の MVC アプリでは、Controller など拡張子が cs のファイルは Visual Studio で単一アセンブリにコンパイルされ、bin フォルダに配置されます。
一方、View (.cshtml) は、デフォルトではランタイムコンパイルとなり、アプリをデプロイした後サーバーで動的にアセンブリにコンパイルされ、サーバーの Temporary ASP.NET Files フォルダに保存されます。
という訳で、Controller と View とは違うアセンブリになるため、Controller で作成された匿名クラスのプロパティは View では見えず、アクセスしようとすると上の画像のように RuntimeBinderException がスローされます。
ただし、ASP.NET Core アプリの場合は、Controller と View はデフォルトで単一アセンブリにコンパイルされるので、上に書いたような問題は起きません。(ASP.NET Core のコンパイルについて、詳しくは Microsoft のドキュメント「ASP.NET Core での Razor ファイルのコンパイル」を見てください)
上の画像を表示した MVC アプリの Controler と View のコードを以下に載せておきます。Visual Studio 2022 のテンプレートを使って作成した .NET Framework 4.8 の MVC5 アプリです。
Controller
using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Threading.Tasks;
using System.Net;
using System.Web.Mvc;
using Mvc5App2.Models;
namespace Mvc5App2.Controllers
{
public class ProductsController : Controller
{
private NORTHWINDEntities db = new NORTHWINDEntities();
public async Task<ActionResult> Test()
{
var products = db.Products
.Select(p => new
{
Id = p.ProductID,
Name = p.ProductName,
Price = p.UnitPrice
});
ViewBag.List = await products.ToListAsync();
return View();
}
}
}
View
@{
ViewBag.Title = "Test";
}
<h2>Test</h2>
<br />
<table class="table">
<tr>
<th>Id</th>
<th>Name</th>
<th>Price</th>
</tr>
@foreach (var item in ViewBag.List)
{
<tr>
<td>@item.Id</td>
<td>@item.Name</td>
<td>@item.Price</td>
</tr>
}
</table>
解決策は、匿名型を使うのは止めて、以下のようなカスタムクラスを定義し、
public class DTO
{
public int Id { get; set; }
public string Name { get; set; }
public decimal? Price { get; set; }
}
以下のように List<DTO> 型のデータを生成して View に渡すことです。
public async Task<ActionResult> Test()
{
var products = db.Products
.Select(p => new DTO // List<DTO> を生成
{
Id = p.ProductID,
Name = p.ProductName,
Price = p.UnitPrice
});
ViewBag.List = await products.ToListAsync();
return View();
}
なお、上にも書きましたように、ASP.NET Core アプリの場合は、Controller と View はデフォルトで同じアセンブリにコンパイルされるので、上に書いた問題は起きません。
なので、匿名型を使っても以下の画像の通り期待した結果が得られます。