C # “ Dynamic”不能访问在另一个程序集中声明的匿名类型的属性

下面的代码工作得很好,只要我有类 ClassSameAssembly在同一个程序集类 Program。 但是当我将类 ClassSameAssembly移动到一个单独的程序集时,会抛出一个 RuntimeBinderException(见下文)。 有可能解决吗?

using System;


namespace ConsoleApplication2
{
public static class ClassSameAssembly
{
public static dynamic GetValues()
{
return new
{
Name = "Michael", Age = 20
};
}
}


internal class Program
{
private static void Main(string[] args)
{
var d = ClassSameAssembly.GetValues();
Console.WriteLine("{0} is {1} years old", d.Name, d.Age);
}
}
}

Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: “ object”不包含“ Name”的定义

at CallSite.Target(Closure , CallSite , Object )
at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
at ConsoleApplication2.Program.Main(String[] args) in C:\temp\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 23
40316 次浏览

I believe the problem is that the anonymous type is generated as internal, so the binder doesn't really "know" about it as such.

Try using ExpandoObject instead:

public static dynamic GetValues()
{
dynamic expando = new ExpandoObject();
expando.Name = "Michael";
expando.Age = 20;
return expando;
}

I know that's somewhat ugly, but it's the best I can think of at the moment... I don't think you can even use an object initializer with it, because while it's strongly typed as ExpandoObject the compiler won't know what to do with "Name" and "Age". You may be able to do this:

 dynamic expando = new ExpandoObject()
{
{ "Name", "Michael" },
{ "Age", 20 }
};
return expando;

but that's not much better...

You could potentially write an extension method to convert an anonymous type to an expando with the same contents via reflection. Then you could write:

return new { Name = "Michael", Age = 20 }.ToExpando();

That's pretty horrible though :(

You could use [assembly: InternalsVisibleTo("YourAssemblyName")] to make you assembly internals visible.

I ran into a similair problem and would like to add to Jon Skeets answer that there is another option. The reason I found out was that I realized that many extension methods in Asp MVC3 uses anonymous classes as input to provide html attributes (new {alt="Image alt", style="padding-top: 5px"} =>

Anyway - those functions use the constructor of the RouteValueDictionary class. I tried that myself, and sure enough it works - though only the first level (I used a multi-level structure). SO - in code this would be:

object o = new {
name = "theName",
props = new {
p1 = "prop1",
p2 = "prop2"
}
}
SeparateAssembly.TextFunc(o)


//In SeparateAssembly:
public void TextFunc(Object o) {
var rvd = new RouteValueDictionary(o);


//Does not work:
Console.WriteLine(o.name);
Console.WriteLine(o.props.p1);


//DOES work!
Console.WriteLine(rvd["name"]);


//Does not work
Console.WriteLine(rvd["props"].p1);
Console.WriteLine(rvd["props"]["p1"]);

SO... What is really going on here? A peek inside the RouteValueDictionary reveals this code (values ~= o above):

foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(values))
object obj2 = descriptor.GetValue(values);
//"this.Add" would of course need to be adapted
this.Add(descriptor.Name, obj2);
}

SO - using TypeDescriptor.GetProperties(o) we would be able to get the properties and values despite the anonymous type being constructed as internal in a separate assembly! And of course this would be quite easy to extend to make it recursive. And to make an extension method if you wanted.

Hope this helps!

/Victor

Here is a rudimentary version of an extension method for ToExpandoObject that I'm sure has room for polishing.

    public static ExpandoObject ToExpandoObject(this object value)
{
// Throw is a helper in my project, replace with your own check(s)
Throw<ArgumentNullException>.If(value, Predicates.IsNull, "value");


var obj = new ExpandoObject() as IDictionary<string, object>;


foreach (var property in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
obj.Add(property.Name, property.GetValue(value, null));
}


return obj as ExpandoObject;
}


[TestCase(1, "str", 10.75, 9.000989, true)]
public void ToExpandoObjectTests(int int1, string str1, decimal dec1, double dbl1, bool bl1)
{
DateTime now = DateTime.Now;


dynamic value = new {Int = int1, String = str1, Decimal = dec1, Double = dbl1, Bool = bl1, Now = now}.ToExpandoObject();


Assert.AreEqual(int1, value.Int);
Assert.AreEqual(str1, value.String);
Assert.AreEqual(dec1, value.Decimal);
Assert.AreEqual(dbl1, value.Double);
Assert.AreEqual(bl1, value.Bool);
Assert.AreEqual(now, value.Now);
}

ToExpando extension method (mentioned in Jon's answer) for the brave ones

public static class ExtensionMethods
{
public static ExpandoObject ToExpando(this object obj)
{
IDictionary<string, object> expando = new ExpandoObject();
foreach (PropertyDescriptor propertyDescriptor in TypeDescriptor.GetProperties(obj))
{
var value = propertyDescriptor.GetValue(obj);
expando.Add(propertyDescriptor.Name, value == null || new[]
{
typeof (Enum),
typeof (String),
typeof (Char),
typeof (Guid),
typeof (Boolean),
typeof (Byte),
typeof (Int16),
typeof (Int32),
typeof (Int64),
typeof (Single),
typeof (Double),
typeof (Decimal),
typeof (SByte),
typeof (UInt16),
typeof (UInt32),
typeof (UInt64),
typeof (DateTime),
typeof (DateTimeOffset),
typeof (TimeSpan),
}.Any(oo => oo.IsInstanceOfType(value))
? value
: value.ToExpando());
}


return (ExpandoObject)expando;
}
}

A cleaner solution would be:

var d = ClassSameAssembly.GetValues().ToDynamic();

Which is now an ExpandoObject.

Remember to reference:

Microsoft.CSharp.dll

Below solution worked for me in my console application projects

Put this [assembly: InternalsVisibleTo("YourAssemblyName")] in \Properties\AssemblyInfo.cs of the separate project with function returning dynamic object.

"YourAssemblyName" is the assembly name of calling project. You can get that through Assembly.GetExecutingAssembly().FullName by executing it in calling project.

If you're already using Newtonsoft.Json in your project (or you're willing to add it for this purpose), you could implement that horrible extension method Jon Skeet is referring to in his answer like this:

public static class ObjectExtensions
{
public static ExpandoObject ToExpando(this object obj)
=> JsonConvert.DeserializeObject<ExpandoObject>(JsonConvert.SerializeObject(obj));
}