C # 'динамічний' не може отримати доступ до властивостей анонімних типів, оголошених в іншій збірці


87

Наведений нижче код працює добре, якщо я маю клас 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: "об'єкт" не містить визначення "Ім'я"

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

StackTrace: на CallSite.Target (Closure, CallSite, Object) на System.Dynamic.UpdateDelegates.UpdateAndExecute1 [T0, TRet] (Сайт CallSite, T0 arg0) на ConsoleApplication2.Program.Main (String [] args) в Cgs: args). \ Projects \ ConsoleApplication2 \ ConsoleApplication2 \ Program.cs: рядок 23 у System.AppDomain._nExecuteAssembly (збірка RuntimeAssembly, String [] аргументи) в System.AppDomain.nExecuteAssembly (збірка RuntimeAssembly, String [] args) в System.App args) в Systemsse. String AssemblyFile, Evidence AssemblySecurity, String [] args)
mehanik

в Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly () в System.Threading.ThreadHelper.ThreadStart_Context (стан об'єкта) в System.Threading.ExecutionContext.Run (ExecutionContext изпълнениеContextrecon, ContextContext, ContextContext, ContextContext, Context, ExecutionContext.Run (ExecutionContext execuContext, ContextCallback callback, Object state) у System.Threading.ThreadHelper.ThreadStart () InnerException:
mehanik

будь-яке остаточне рішення з повним вихідним кодом?
Кікенет

Відповіді:


116

Я вважаю, що проблема полягає в тому, що анонімний тип генерується як internal, тому сполучна речовина насправді не "знає" про нього як такого.

Спробуйте замість цього використовувати ExpandoObject:

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

Я знаю, що це трохи потворно, але це найкраще, що я можу придумати на даний момент ... Я не думаю, що ви навіть можете використовувати з ним ініціалізатор об'єкта, тому що, хоча він набраний сильно, оскільки ExpandoObjectкомпілятор не знатиме, що робити з "Ім'я" та "Вік". Ви можете зробити це:

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

але це не набагато краще ...

Ви потенційно можете написати метод розширення для перетворення анонімного типу в експандон із однаковим вмістом за допомогою відображення. Тоді ви могли написати:

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

Це досить жахливо :(


1
Дякую, Джон. Я просто мав ту саму проблему з використанням класу, який виявився приватним для збірки.
Dave Markle

2
Я хотів би в кінці щось подібне до вашого жахливого прикладу, але не такого жахливого. Для використання: динамічний реквізит = новий {Metadata = DetailModelMetadata.Create, PageTitle = "Новий вміст", PageHeading = "Керування вмістом"}; і додати вказані реквізити як динамічні члени було б чудово!
ProfK

Імпровізований інтерфейс фреймворку з відкритим кодом багато робить з dlr, він має синтаксис вбудованої ініціалізації, який працює для будь-якого динамічного або статичного об’єкта. return Build<ExpandoObject>.NewObject(Name:"Micheal", Age: 20);
jbtule

1
будь-який повний зразок вихідного коду для методу розширення для перетворення анонімного типу в експандон?
Kiquenet

1
@ Md.lbrahim: В принципі, ви не можете. Вам доведеться це робити objectабо на загальному типі (можна вимагати, щоб це був клас ...) і перевіряти тип під час виконання.
Джон Скіт

63

Ви можете використовувати, [assembly: InternalsVisibleTo("YourAssemblyName")]щоб зробити внутрішні елементи збірки видимими.


2
Відповідь Джона є більш повною, але це насправді забезпечує для мене досить просте обхідне рішення. Дякую :)
kelloti

Я годинами бився головою по різних форумах, але не знайшов простої відповіді, крім цього. Дякую Лука. Але все одно я не можу зрозуміти, чому динамічний тип недоступний за межами збірки, як це робиться в тій же збірці? Я маю на увазі, чому це обмеження в .Net.
Faisal Mq

@FaisalMq це тому, що компілятор, який генерує анонімні класи, оголошує їх "внутрішніми". Не знаю, яка справжня причина.
ema,

2
Так, я вважаю, що ця відповідь важлива, оскільки я не хочу змінювати робочий код, мені просто потрібно протестувати його з іншої збірки
PandaWood

Одним приміткою, яку слід додати тут, є те, що вам потрібно перезапустити Visual Studio після цієї зміни, щоб це працювало.
Раді

11

Я зіткнувся з подібною проблемою і хотів би додати до Джона Скітса відповідь, що є ще один варіант. Причиною, яку я дізнався, було те, що я зрозумів, що багато методів розширення в Asp MVC3 використовують анонімні класи як вхідні дані для надання атрибутів html (new {alt = "Image alt", style = "padding-top: 5px"} =>

У будь-якому випадку - ці функції використовують конструктор класу RouteValueDictionary. Я спробував це сам, і, звичайно, це працює - хоча лише перший рівень (я використовував багаторівневу структуру). ТАК - у коді це буде:

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"]);

ТАК ... Що тут насправді відбувається? Зазирнути всередину RouteValueDictionary розкриває цей код (значення ~ = o вище):

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);
}

ТАК - за допомогою TypeDescriptor.GetProperties (o) ми змогли б отримати властивості та значення, незважаючи на те, що анонімний тип будується як внутрішній в окремій збірці! І, звичайно, це було б досить легко розширити, щоб зробити його рекурсивним. І зробити метод розширення, якщо хочете.

Сподіваюся, це допомагає!

/ Віктор


Вибачте за цю плутанину. Код оновлено з prop1 => p1, де це доречно. Тим не менше - ідея з усім дописом полягала в тому, щоб висунути TypeDescriptor.GetProperties як варіант вирішення проблеми, яка, сподіваюся, і так була зрозумілою ...
Віктор

Це справді дурно, що динамік не може зробити це за нас. Я обоє дуже люблю і дуже ненавиджу динаміку.
Кріс Марісіч

2

Ось елементарна версія методу розширення для ToExpandoObject, який, я впевнений, має місце для полірування.

    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);
    }

1

Більш чистим рішенням буде:

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

Що зараз є ExpandoObject.

Не забувайте посилатися:

Microsoft.CSharp.dll

1

Наведене нижче рішення працювало для мене в проектах консольних додатків

Помістіть цей [Assembly: InternalsVisibleTo ("YourAssemblyName")] в \ Properties \ AssemblyInfo.cs окремого проекту з функцією, що повертає динамічний об'єкт.

"YourAssemblyName" - це назва збірки проекту, що викликає. Ви можете отримати це через Assembly.GetExecutingAssembly (). FullName, виконавши його у виклику проекту.


0

Метод розширення ToExpando (згаданий у відповіді Джона) для сміливих

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;
    }
}

0

Якщо ви вже використовуєте Newtonsoft.Json у своєму проекті (або ви готові додати його для цієї мети), ви можете реалізувати той жахливий метод розширення, на який посилається Джон Скіт у своїй відповіді так:

public static class ObjectExtensions
{
    public static ExpandoObject ToExpando(this object obj)
        => JsonConvert.DeserializeObject<ExpandoObject>(JsonConvert.SerializeObject(obj));
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.