Razor中的动态匿名类型导致RuntimeBinderException异常

161
我遇到了以下错误:

'object' 不包含 'RatingName' 的定义

但当查看匿名动态类型时,它显然包含 RatingName。

错误的截图

我知道可以使用 Tuple 解决该问题,但我想了解这个错误消息出现的原因。
12个回答

245

在我看来,匿名类型具有内部属性是.NET框架设计上的一个糟糕决定。

这里有一个快速而不错的扩展可以解决这个问题,即立即将匿名对象转换为ExpandoObject。

public static ExpandoObject ToExpando(this object anonymousObject)
{
    IDictionary<string, object> anonymousDictionary =  new RouteValueDictionary(anonymousObject);
    IDictionary<string, object> expando = new ExpandoObject();
    foreach (var item in anonymousDictionary)
        expando.Add(item);
    return (ExpandoObject)expando;
}

使用起来非常简单

return View("ViewName", someLinq.Select(new { x=1, y=2}.ToExpando());

当然,在你的视角中:

@foreach (var item in Model) {
     <div>x = @item.x, y = @item.y</div>
}

3
+1 我特别在寻找 HtmlHelper.AnonymousObjectToHtmlAttributes,我知道这一定已经被包含了,不想用类似手写的代码重新发明轮子。 - Chris Marisic
3
与仅创建强类型支持模型相比,这个的性能如何? - GONeale
这个答案对我很有帮助,但让我困惑的另一部分是我需要更新 WebViewPage 来指定模型是动态的。这可以通过修改 .cshtml 继承为以下内容来完成: @inherits WebViewPage<dynamic> - Randall Borck
如果我的 .Select 使用预定义的投影,会怎么样?例如:someLinq.Select(preDefinedProjection().ToExpando()),其中 preDefinedProjection() 返回一个 Expression<Func<SomeType, dynamic>> - sports
另外需要补充的一点是,如果你正在处理模型上的属性而不是模型本身,则可能需要将属性转换为 dynamic,例如:public class Model { public IEnumerable<ExpandoObject> Property { get; set; } 然后在视图中:@foreach ( dynamic item in Model.Property) @item.anonPropName - ngless
显示剩余4条评论

52

我在相关问题中找到了答案。 答案在David Ebbo的博客文章“将匿名对象传递给MVC视图并使用动态访问它们”中指定。

原因是控制器中传递的匿名类型是内部类型,因此只能从声明它的程序集内部进行访问。由于视图被单独编译,动态绑定器会抱怨无法越过程序集边界。

但是如果您思考一下,这种来自动态绑定器的限制实际上是相当人为的,因为如果您使用私有反射,没有任何东西会阻止您访问那些内部成员(甚至在Medium信任级别下也可以工作)。因此,默认的动态绑定器正在努力执行C#编译规则(其中无法访问内部成员),而不是让您执行CLR运行时允许的操作。


赶上我了 :) 我在使用Razor Engine(http://razorengine.codeplex.com 上的一个先驱)时遇到了这个问题。 - Buildstarted
这并不是一个真正的答案,不会再多说有关“被接受的答案”的事情! - DATEx2
4
它解释了错误发生的原因,这也是问题所在。您提供了一个很好的解决方法,我给您点赞了 :) - Lucas
FYI:这个答案现在已经非常过时了 - 正如作者在引用的博客文章开头所说的那样。 - Simon_Weaver
@Simon_Weaver 但是这篇更新的帖子并没有解释在MVC3+中应该如何工作。我在MVC4中遇到了同样的问题。有关于使用dynamic的当前“神圣”方式的任何指针吗? - Cristian Diaconescu
MVC 5.2.3中也存在同样的问题!!!使用RouteDataValues对象通过反射提取成员的ExpandoObject解决方案可行。这确实是Razor中某些人为限制所导致的,因为在发生错误的上下文中完全没有阻止此转换执行的东西。 - Triynko

26

使用 ToExpando 方法是最佳解决方案。

这是一个不需要 System.Web 程序集的版本

public static ExpandoObject ToExpando(this object anonymousObject)
{
    IDictionary<string, object> expando = new ExpandoObject();
    foreach (PropertyDescriptor propertyDescriptor in TypeDescriptor.GetProperties(anonymousObject))
    {
        var obj = propertyDescriptor.GetValue(anonymousObject);
        expando.Add(propertyDescriptor.Name, obj);
    }

    return (ExpandoObject)expando;
}

1
这是一个更好的答案。不确定替代答案中的 HtmlHelper 是否会使用下划线。 - Den
+1 对于通用目的的答案,这在 ASP/MVC 之外也很有用。 - codenheim
嵌套的动态属性怎么办?它们将继续保持动态...例如:{ foo: "foo", nestedDynamic: { blah: "blah" } } - sports

17

不要从匿名类型创建模型,然后尝试像这样将匿名对象转换为ExpandoObject...

var model = new 
{
    Profile = profile,
    Foo = foo
};

return View(model.ToExpando());  // not a framework method (see other answers)

你可以直接创建ExpandoObject

dynamic model = new ExpandoObject();
model.Profile = profile;
model.Foo = foo;

return View(model);

然后在您的视图中,将模型类型设置为动态 @model dynamic,您可以直接访问属性:

接下来返回:

然后在您的视图中,将模型类型设置为动态 @model dynamic,您可以直接访问属性:

@Model.Profile.Name
@Model.Foo

通常我会建议为大多数视图使用强类型视图模型,但有时这种灵活性很方便。


@yohal 当然可以 - 我想这是个人偏好。我更喜欢使用ViewBag来处理与页面模型无关的杂项页面数据 - 也许与模板相关,并将Model作为主要模型。 - Simon_Weaver
2
顺便说一句,你不必添加@model dynamic,因为它是默认的。 - yoel halb
正是我所需要的,实现将匿名对象转换为动态对象的方法太耗费时间了......非常感谢。 - h-rai

5
您可以使用框架即时接口来将匿名类型包装在一个接口中。
您只需返回IEnumerable<IMadeUpInterface>,并在Linq的末尾使用.AllActLike<IMadeUpInterface>();。这是因为它使用DLR调用匿名属性,并使用声明匿名类型的程序集的上下文。

1
很棒的小技巧 :) 不知道它是否比只有一堆公共属性的普通类更好,至少在这种情况下 - Andrew

4

编写控制台应用程序并将Mono.Cecil作为参考添加(您现在可以从NuGet中添加),然后编写以下代码:

static void Main(string[] args)
{
    var asmFile = args[0];
    Console.WriteLine("Making anonymous types public for '{0}'.", asmFile);

    var asmDef = AssemblyDefinition.ReadAssembly(asmFile, new ReaderParameters
    {
        ReadSymbols = true
    });

    var anonymousTypes = asmDef.Modules
        .SelectMany(m => m.Types)
        .Where(t => t.Name.Contains("<>f__AnonymousType"));

    foreach (var type in anonymousTypes)
    {
        type.IsPublic = true;
    }

    asmDef.Write(asmFile, new WriterParameters
    {
        WriteSymbols = true
    });
}

以上代码将从输入参数中获取程序集文件,并使用Mono.Cecil将其从internal更改为public,从而解决了问题。

我们可以在网站的Post Build事件中运行该程序。我写了一篇关于此的中文博客文章,但我相信您只需阅读代码和截图即可理解。 :)


2

根据被接受的答案,我已经在控制器中进行了重写,以使其在一般情况下和后台工作。

这是代码:

protected override void OnResultExecuting(ResultExecutingContext filterContext)
{
    base.OnResultExecuting(filterContext);

    //This is needed to allow the anonymous type as they are intenal to the assembly, while razor compiles .cshtml files into a seperate assembly
    if (ViewData != null && ViewData.Model != null && ViewData.Model.GetType().IsNotPublic)
    {
       try
       {
          IDictionary<string, object> expando = new ExpandoObject();
          (new RouteValueDictionary(ViewData.Model)).ToList().ForEach(item => expando.Add(item));
          ViewData.Model = expando;
       }
       catch
       {
           throw new Exception("The model provided is not 'public' and therefore not avaialable to the view, and there was no way of handing it over");
       }
    }
}

现在你可以将一个匿名对象作为模型传递,并且它将按预期工作。

0
RuntimeBinderException的触发原因,我认为在其他帖子中已经有了很好的答案。我只关注于解释我是如何使其正常工作的。
参考@DotNetWise的答案和Binding views with Anonymous type collection in ASP.NET MVC,
首先,创建一个静态类进行扩展。
public static class impFunctions
{
    //converting the anonymous object into an ExpandoObject
    public static ExpandoObject ToExpando(this object anonymousObject)
    {
        //IDictionary<string, object> anonymousDictionary = new RouteValueDictionary(anonymousObject);
        IDictionary<string, object> anonymousDictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(anonymousObject);
        IDictionary<string, object> expando = new ExpandoObject();
        foreach (var item in anonymousDictionary)
            expando.Add(item);
        return (ExpandoObject)expando;
    }
}

在控制器中

    public ActionResult VisitCount()
    {
        dynamic Visitor = db.Visitors
                        .GroupBy(p => p.NRIC)
                        .Select(g => new { nric = g.Key, count = g.Count()})
                        .OrderByDescending(g => g.count)
                        .AsEnumerable()    //important to convert to Enumerable
                        .Select(c => c.ToExpando()); //convert to ExpandoObject
        return View(Visitor);
    }

在视图中,@model IEnumerable(动态类型,不是模型类),这非常重要,因为我们将绑定匿名类型对象。
@model IEnumerable<dynamic>

@*@foreach (dynamic item in Model)*@
@foreach (var item in Model)
{
    <div>x=@item.nric, y=@item.count</div>
}

在 foreach 中使用 var 或者 dynamic 都没有报错。
顺便说一下,创建一个新的 ViewModel,使其匹配新字段也可以是将结果传递到视图的方法。

0

使用ExpandoObject扩展是有效的,但在使用嵌套匿名对象时会出现问题。

例如

var projectInfo = new {
 Id = proj.Id,
 UserName = user.Name
};

var workitem = WorkBL.Get(id);

return View(new
{
  Project = projectInfo,
  WorkItem = workitem
}.ToExpando());

为了实现这个目标,我使用了这个。

public static class RazorDynamicExtension
{
    /// <summary>
    /// Dynamic object that we'll utilize to return anonymous type parameters in Views
    /// </summary>
    public class RazorDynamicObject : DynamicObject
    {
        internal object Model { get; set; }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            if (binder.Name.ToUpper() == "ANONVALUE")
            {
                result = Model;
                return true;
            }
            else
            {
                PropertyInfo propInfo = Model.GetType().GetProperty(binder.Name);

                if (propInfo == null)
                {
                    throw new InvalidOperationException(binder.Name);
                }

                object returnObject = propInfo.GetValue(Model, null);

                Type modelType = returnObject.GetType();
                if (modelType != null
                    && !modelType.IsPublic
                    && modelType.BaseType == typeof(Object)
                    && modelType.DeclaringType == null)
                {
                    result = new RazorDynamicObject() { Model = returnObject };
                }
                else
                {
                    result = returnObject;
                }

                return true;
            }
        }
    }

    public static RazorDynamicObject ToRazorDynamic(this object anonymousObject)
    {
        return new RazorDynamicObject() { Model = anonymousObject };
    }
}

在控制器中的用法相同,只需使用 ToRazorDynamic() 而不是 ToExpando()。

在视图中获取整个匿名对象只需在末尾添加 ".AnonValue"。

var project = @(Html.Raw(JsonConvert.SerializeObject(Model.Project.AnonValue)));
var projectName = @Model.Project.Name;

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接