C # dynamic не может получить доступ к свойствам анонимных типов, объявленных в другой сборке

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) в C: \ temp \ Projects \ ConsoleApplication2 \ ConsoleApplication2 \ Program.cs: строка 23 в System.AppDomain._nExecuteAssembly (сборка RuntimeAssembly, String [] args) в System.AppDomain.nExecuteAssembly (сборка RuntimeAssembly, String [] args) в System.AppDomain.ExecuteAssembly ( String
AssemblyFile
в Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly () в System.Threading.ThreadHelper.ThreadStart_Context (состояние объекта) в System.Threading.ExecutionContext.Run (ExecutionContext executeContext, обратный вызов ContextCallback, состояние объекта, Boolean ignorextreadxing). ExecutionContext.Run (ExecutionContext executionContext, обратный вызов ContextCallback, состояние объекта) в System.Threading.ThreadHelper.ThreadStart () InnerException:
mehanik
какое-нибудь окончательное решение с полным исходным кодом?
Kiquenet

Ответы:

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
Спасибо, Джон. У меня была такая же проблема с использованием класса, который оказался частным для сборки.
Дэйв Маркл
2
Я бы хотел что-то вроде твоего ужасного примера в конце, хотя и не такого ужасного. Для использования: dynamic props = new {Metadata = DetailModelMetadata.Create, PageTitle = "New Content", PageHeading = "Content Management"}; и было бы здорово добавить именованные реквизиты в качестве динамических членов!
ProfK
Импровизированный интерфейс фреймворка с открытым исходным кодом многое делает с dlr, у него есть встроенный синтаксис инициализации, который работает для любого динамического или статического объекта. return Build<ExpandoObject>.NewObject(Name:"Micheal", Age: 20);
jbtule 07
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
Zylv3r
источник
1

Ниже решение сработало для меня в моих проектах консольных приложений.

Поместите эту [сборку: 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));
}
Huysentruitw
источник