C # „dynamiczny” nie może uzyskać dostępu do właściwości z typów anonimowych zadeklarowanych w innym zestawie


87

Poniższy kod działa dobrze, o ile mam klasę ClassSameAssemblyw tym samym zestawie co klasa Program. Ale kiedy przenoszę klasę ClassSameAssemblydo oddzielnego zestawu, RuntimeBinderExceptionrzucany jest znak (patrz poniżej). Czy można to rozwiązać?

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: „obiekt” nie zawiera definicji dla „nazwy”

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: w CallSite.Target (Closure, CallSite, Object) w System.Dynamic.UpdateDelegates.UpdateAndExecute1 [T0, TRet] (CallSite site, T0 arg0) w ConsoleApplication2.Program.Main (String [] args) w C: \ temp \ Projects \ ConsoleApplication2 \ ConsoleApplication2 \ Program.cs: wiersz 23 w System.AppDomain._nExecuteAssembly (zestaw RuntimeAssembly, String [] args) w System.AppDomain.nExecuteAssembly (zestaw RuntimeAssembly, String [] args) w System.AppDomain.ExecuteAssembly ( ciąg assemblyFile, Evidence assemblySecurity, ciąg [] arg)
Mehanik

w Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly () w System.Threading.ThreadHelper.ThreadStart_Context (Object state) at System.Threading.ExecutionContext.Run (ExecutionContext ExecutionContext, ConteanextCallback callback, ignore System, ignore System). ExecutionContext.Run (ExecutionContext ExecutionContext, wywołanie zwrotne ContextCallback, stan obiektu) w System.Threading.ThreadHelper.ThreadStart () InnerException:
mehanik

jakieś ostateczne rozwiązanie z pełnym kodem źródłowym?
Kiquenet

Odpowiedzi:


116

Uważam, że problem polega na tym, że typ anonimowy jest generowany jako internal, więc segregator tak naprawdę nie „wie” o nim jako takim.

Zamiast tego spróbuj użyć ExpandoObject:

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

Wiem, że to trochę brzydkie, ale jest to najlepsze, o czym w tej chwili przychodzi mi do głowy ... Nie sądzę, aby można było nawet użyć z nim inicjatora obiektu, ponieważ podczas gdy jest mocno wpisany, ponieważ ExpandoObjectkompilator nie będzie wiedział, co robić z „imieniem” i „wiekiem”. Państwo może być w stanie to zrobić:

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

ale to nie jest dużo lepsze ...

Możesz potencjalnie napisać metodę rozszerzenia, aby przekonwertować typ anonimowy na expando z tą samą zawartością przez odbicie. Wtedy możesz napisać:

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

Ale to dość okropne :(


1
Dzięki Jon. Po prostu miałem ten sam problem podczas korzystania z klasy, która była prywatna dla zestawu.
Dave Markle

2
Chciałbym jednak na koniec czegoś takiego jak twój okropny przykład, ale nie tak okropny. Aby użyć: dynamic props = new {Metadata = DetailModelMetadata.Create, PageTitle = "New Content", PageHeading = "Content Management"}; i dodanie nazwanych rekwizytów jako dynamicznych członków byłoby świetne!
ProfK

Zaimprowizowany interfejs frameworka open source robi wiele z dlr, ma wbudowaną składnię inicjującą, która działa dla dowolnego obiektu dynamicznego lub statycznego. return Build<ExpandoObject>.NewObject(Name:"Micheal", Age: 20);
jbtule,

1
jakikolwiek pełny przykład kodu źródłowego dla metody rozszerzenia, aby przekonwertować typ anonimowy na rozszerzenie?
Kiquenet

1
@ Md.lbrahim: Zasadniczo nie możesz. Musiałbyś to zrobić na objecttypie ogólnym lub na typie ogólnym (możesz wymagać, aby był to klasa ...) i sprawdzić typ w czasie wykonywania.
Jon Skeet

63

Możesz użyć, [assembly: InternalsVisibleTo("YourAssemblyName")]aby wyświetlić wewnętrzne elementy zespołu.


2
Odpowiedź Jona jest bardziej kompletna, ale w rzeczywistości zapewnia to dość proste obejście. Dzięki :)
kelloti

Godzinami waliłem głową na różnych forach, ale nie znalazłem prostej odpowiedzi poza tym. Dzięki, Luke. Ale nadal nie mogę zrozumieć, dlaczego typ dynamiczny nie jest dostępny poza zespołem, tak jak w tym samym zestawie? Mam na myśli, dlaczego jest to ograniczenie w .Net.
Faisal Mq

@FaisalMq dzieje się tak, ponieważ kompilator, który generuje klasy anonimowe, deklaruje je jako „wewnętrzne”. Nie wiem, co jest prawdziwym powodem.
ema

2
Tak, myślę, że ta odpowiedź jest ważna, ponieważ nie chcę zmieniać działającego kodu, wystarczy, że przetestuję go z innego zestawu
PandaWood

Należy tutaj dodać, że po wprowadzeniu tej zmiany należy ponownie uruchomić program Visual Studio, aby to zadziałało.
Rady

11

Napotkałem podobny problem i chciałbym dodać do Jona Skeetsa odpowiedź, że istnieje inna opcja. Powodem, dla którego się dowiedziałem, było to, że zdałem sobie sprawę, że wiele metod rozszerzających w Asp MVC3 używa anonimowych klas jako danych wejściowych do dostarczania atrybutów html (new {alt = "Image alt", style = "padding-top: 5px"} =>

W każdym razie - te funkcje używają konstruktora klasy RouteValueDictionary. Sam spróbowałem i na pewno działa - choć tylko na pierwszym poziomie (użyłem konstrukcji wielopoziomowej). SO - w kodzie będzie to:

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

WIĘC ... Co tu się naprawdę dzieje? Wgląd do RouteValueDictionary ujawnia ten kod (wartości ~ = o powyżej):

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 - używając TypeDescriptor.GetProperties (o) bylibyśmy w stanie uzyskać właściwości i wartości pomimo tego, że typ anonimowy był konstruowany jako wewnętrzny w oddzielnym zestawie! Oczywiście byłoby to dość łatwe do rozszerzenia, aby było rekurencyjne. I zrobić metodę rozszerzenia, jeśli chcesz.

Mam nadzieję że to pomoże!

/Zwycięzca


Przepraszam za to zamieszanie. Kod zaktualizowany z prop1 => p1 tam, gdzie to konieczne. Mimo wszystko - pomysł z całym postem polegał na przedstawieniu TypeDescriptor.GetProperties jako opcji rozwiązania problemu, który, miejmy nadzieję, był jasny ...
Victor

To naprawdę głupie, że dynamika nie może tego zrobić za nas. Naprawdę kocham i nienawidzę dynamiki.
Chris Marisic,

2

Oto podstawowa wersja metody rozszerzenia ToExpandoObject, która z pewnością ma miejsce do polerowania.

    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

Czystszym rozwiązaniem byłoby:

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

Który jest teraz ExpandoObject.

Pamiętaj, aby odnieść się do:

Microsoft.CSharp.dll

1

Poniższe rozwiązanie sprawdziło się w moich projektach aplikacji konsolowych

Umieść to [assembly: InternalsVisibleTo ("YourAssemblyName")] w \ Properties \ AssemblyInfo.cs oddzielnego projektu z funkcją zwracającą obiekt dynamiczny.

„YourAssemblyName” to nazwa zestawu wywołującego projekt. Możesz to uzyskać poprzez Assembly.GetExecutingAssembly (). FullName, wykonując to w wywołaniu projektu.


0

Metoda rozszerzenia ToExpando (wspomniana w odpowiedzi Jona) dla odważnych

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

Jeśli już używasz Newtonsoft.Json w swoim projekcie (lub chcesz dodać go w tym celu), możesz zaimplementować tę okropną metodę rozszerzenia, do której odwołuje się Jon Skeet w swojej odpowiedzi :

public static class ObjectExtensions
{
    public static ExpandoObject ToExpando(this object obj)
        => JsonConvert.DeserializeObject<ExpandoObject>(JsonConvert.SerializeObject(obj));
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.