C # 'dynamic' kann nicht auf Eigenschaften von anonymen Typen zugreifen, die in einer anderen Assembly deklariert wurden

87

Der folgende Code funktioniert gut, solange ich eine Klasse ClassSameAssemblyin derselben Assembly wie die Klasse habe Program. Wenn ich die Klasse ClassSameAssemblyin eine separate Assembly verschiebe, wird ein RuntimeBinderException(siehe unten) ausgelöst. Ist es möglich, es zu lösen?

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: 'Objekt' enthält keine Definition für '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
mehanik
quelle
StackTrace: bei CallSite.Target (Closure, CallSite, Object) bei System.Dynamic.UpdateDelegates.UpdateAndExecute1 [T0, TRet] (CallSite-Site, T0 arg0) bei ConsoleApplication2.Program.Main (String [] args) in C: \ temp \ Projects \ ConsoleApplication2 \ ConsoleApplication2 \ Program.cs: Zeile 23 unter System.AppDomain._nExecuteAssembly (RuntimeAssembly-Assembly, String [] args) unter System.AppDomain.nExecuteAssembly (RuntimeAssembly-Assembly, String [] args) unter System.AppDomain.ExecuteA String AssemblyDatei, Evidence AssemblySecurity, String [] Argumente)
Mehanik
bei Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly () bei System.Threading.ThreadHelper.ThreadStart_Context (Objektstatus) bei System.Threading.ExecutionContext.Run (ExecutionContext ExecutionContext, ContextCallback callback, ObjectCt. ExecutionContext.Run (ExecutionContext executeContext, ContextCallback-Rückruf, Objektstatus) bei System.Threading.ThreadHelper.ThreadStart () InnerException:
mehanik
Gibt es eine endgültige Lösung mit vollständigem Quellcode?
Kiquenet

Antworten:

116

Ich glaube, das Problem ist, dass der anonyme Typ als generiert wird internal, so dass der Ordner nicht wirklich darüber Bescheid weiß.

Versuchen Sie stattdessen, ExpandoObject zu verwenden:

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

Ich weiß, dass das etwas hässlich ist, aber es ist das Beste, was ich mir im Moment vorstellen kann ... Ich glaube nicht, dass Sie damit sogar einen Objektinitialisierer verwenden können, da ExpandoObjectder Compiler zwar stark typisiert ist, aber nicht weiß, was er tun soll mit "Name" und "Alter". Sie können der Lage sein , dies zu tun:

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

aber das ist nicht viel besser ...

Sie könnten möglicherweise eine Erweiterungsmethode schreiben, um einen anonymen Typ durch Reflektion in ein Expoo mit demselben Inhalt umzuwandeln. Dann könnten Sie schreiben:

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

Das ist allerdings ziemlich schrecklich :(

Jon Skeet
quelle
1
Danke Jon. Ich hatte gerade das gleiche Problem mit einer Klasse, die für die Versammlung zufällig privat war.
Dave Markle
2
Ich würde so etwas wie dein schreckliches Beispiel am Ende lieben, nur nicht so schrecklich. Verwendung: dynamische Requisiten = neu {Metadata = DetailModelMetadata.Create, PageTitle = "Neuer Inhalt", PageHeading = "Inhaltsverwaltung"}; und die genannten Requisiten als dynamische Mitglieder hinzufügen zu lassen, wäre toll!
ProfK
Die spontane Open-Source-Framework- Schnittstelle hat viel mit der DLL zu tun. Sie verfügt über eine Inline-Initialisierungssyntax , die für jedes Objekt dynamisch oder statisch funktioniert. return Build<ExpandoObject>.NewObject(Name:"Micheal", Age: 20);
jbtule
1
Gibt es ein vollständiges Quellcodebeispiel für eine Erweiterungsmethode zum Konvertieren eines anonymen Typs in eine Erweiterung?
Kiquenet
1
@ Md.lbrahim: Das kannst du im Grunde nicht. Sie müssten dies entweder für objectoder für einen generischen Typ tun (Sie können verlangen, dass es sich um eine Klasse handelt ...) und den Typ zur Ausführungszeit überprüfen.
Jon Skeet
62

Sie können verwenden [assembly: InternalsVisibleTo("YourAssemblyName")], um Ihre Baugruppeneinbauten sichtbar zu machen.

ema
quelle
2
Jons Antwort ist vollständiger, aber dies bietet mir tatsächlich eine einigermaßen einfache Problemumgehung. Danke :)
Kelloti
Ich schlug stundenlang in verschiedenen Foren mit dem Kopf, fand aber keine einfache Antwort außer dieser. Danke Luke. Aber ich kann immer noch nicht verstehen, warum ein dynamischer Typ außerhalb einer Assembly nicht wie in derselben Assembly zugänglich ist. Ich meine, warum ist diese Einschränkung in .Net.
Faisal Mq
@FaisalMq, weil der Compiler, der die anonymen Klassen generiert, sie als "intern" deklariert. Ich weiß nicht, was der wahre Grund ist.
ema
1
Ja, ich denke, diese Antwort ist wichtig, weil ich den
Arbeitscode
Ein Hinweis, den Sie hier hinzufügen sollten, ist, dass Sie Visual Studio nach dieser Änderung neu starten müssen, damit dies funktioniert.
Rady
11

Ich bin auf ein ähnliches Problem gestoßen und möchte Jon Skeets Antwort hinzufügen, dass es eine andere Option gibt. Der Grund, warum ich herausfand, war, dass ich feststellte, dass viele Erweiterungsmethoden in Asp MVC3 anonyme Klassen als Eingabe verwenden, um HTML-Attribute bereitzustellen (new {alt = "Image alt", style = "padding-top: 5px"} =>

Wie auch immer - diese Funktionen verwenden den Konstruktor der RouteValueDictionary-Klasse. Ich habe das selbst versucht und sicher funktioniert es - obwohl nur die erste Ebene (ich habe eine mehrstufige Struktur verwendet). SO - im Code wäre dies:

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 ... Was ist hier wirklich los? Ein Blick in das RouteValueDictionary zeigt diesen Code (Werte ~ = o oben):

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 - mit TypeDescriptor.GetProperties (o) könnten wir die Eigenschaften und Werte abrufen, obwohl der anonyme Typ in einer separaten Assembly als intern konstruiert wird! Und natürlich wäre dies ziemlich einfach zu erweitern, um es rekursiv zu machen. Und um eine Erweiterungsmethode zu erstellen, wenn Sie möchten.

Hoffe das hilft!

/Sieger

Sieger
quelle
Entschuldigung für diese Verwirrung. Code gegebenenfalls von prop1 => p1 aktualisiert. Trotzdem - die Idee mit dem gesamten Beitrag war, TypeDescriptor.GetProperties als Option zur Lösung des Problems vorzuschlagen, was hoffentlich sowieso klar war ...
Victor
Das ist wirklich dumm, dass Dynamic das nicht für uns tun kann. Ich liebe und hasse Dynamik.
Chris Marisic
2

Hier ist eine rudimentäre Version einer Erweiterungsmethode für ToExpandoObject, die sicher Platz zum Polieren bietet.

    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);
    }
Ryan Rodemoyer
quelle
1

Eine sauberere Lösung wäre:

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

Welches ist jetzt ein ExpandoObject.

Denken Sie daran, zu verweisen:

Microsoft.CSharp.dll
Zylv3r
quelle
0

ToExpando-Erweiterungsmethode (in Jons Antwort erwähnt) für die Mutigen

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;
    }
}
Matas Vaitkevicius
quelle
0

Die folgende Lösung hat für mich in meinen Konsolenanwendungsprojekten funktioniert

Fügen Sie diese [Assembly: InternalsVisibleTo ("YourAssemblyName")] in \ Properties \ AssemblyInfo.cs des separaten Projekts ein, wobei die Funktion ein dynamisches Objekt zurückgibt.

"YourAssemblyName" ist der Assemblyname des aufrufenden Projekts. Sie können dies über Assembly.GetExecutingAssembly (). FullName erhalten, indem Sie es im aufrufenden Projekt ausführen.

Schah
quelle
0

Wenn Sie Newtonsoft.Json bereits in Ihrem Projekt verwenden (oder bereit sind, es zu diesem Zweck hinzuzufügen), können Sie diese schreckliche Erweiterungsmethode implementieren, auf die sich Jon Skeet in seiner Antwort wie folgt bezieht :

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