C # "dinamico" non può accedere alle proprietà di tipi anonimi dichiarati in un altro assembly


87

Il codice seguente funziona bene fintanto che ho una classe ClassSameAssemblynello stesso assembly della classe Program. Ma quando sposto la classe ClassSameAssemblyin un assembly separato, RuntimeBinderExceptionviene lanciato un (vedi sotto). È possibile risolverlo?

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: "oggetto" non contiene una definizione per "Nome"

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: su CallSite.Target (Closure, CallSite, Object) su System.Dynamic.UpdateDelegates.UpdateAndExecute1 [T0, TRet] (CallSite site, T0 arg0) su ConsoleApplication2.Program.Main (String [] args) in C: \ temp \ Projects \ ConsoleApplication2 \ ConsoleApplication2 \ Program.cs: riga 23 in System.AppDomain._nExecuteAssembly (RuntimeAssembly assembly, String [] args) in System.AppDomain.nExecuteAssembly (RuntimeAssembly assembly, String [] args) in System.AppDomain.ExecuteAssembly ( String assemblyFile, Evidence assemblySecurity, String [] args)
mehanik

in Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly () in System.Threading.ThreadHelper.ThreadStart_Context (stato dell'oggetto) in System.Threading.ExecutionContext.Run (ExecutionContext executionContext, ContextCallback callback, Object stato ignore in Bozza). ExecutionContext.Run (ExecutionContext executionContext, ContextCallback callback, Object state) su System.Threading.ThreadHelper.ThreadStart () InnerException:
mehanik

qualsiasi soluzione finale con il codice sorgente completo?
Kiquenet

Risposte:


116

Credo che il problema sia che il tipo anonimo viene generato come internal, quindi il raccoglitore non lo "sa" realmente come tale.

Prova a utilizzare ExpandoObject invece:

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

So che è un po 'brutto, ma è il meglio a cui riesco a pensare al momento ... Non penso che tu possa nemmeno usare un inizializzatore di oggetti con esso, perché sebbene sia fortemente digitato come ExpandoObjectil compilatore non saprà cosa fare con "Nome" ed "Età". Si può essere in grado di fare questo:

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

ma non è molto meglio ...

Potresti potenzialmente scrivere un metodo di estensione per convertire un tipo anonimo in un expando con gli stessi contenuti tramite reflection. Quindi potresti scrivere:

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

È piuttosto orribile però :(


1
Grazie Jon. Ho appena avuto lo stesso problema usando una classe che era privata dell'assemblea.
Dave Markle

2
Mi piacerebbe qualcosa come il tuo orribile esempio alla fine, ma non così orribile. Per utilizzare: oggetti di scena dinamici = new {Metadata = DetailModelMetadata.Create, PageTitle = "New Content", PageHeading = "Content Management"}; e aggiungere gli oggetti di scena nominati come membri dinamici sarebbe fantastico!
ProfK

L' interfaccia estemporanea del framework open source fa molto con il dlr, ha una sintassi di inizializzazione inline che funziona per qualsiasi oggetto dinamico o statico. return Build<ExpandoObject>.NewObject(Name:"Micheal", Age: 20);
jbtule

1
qualsiasi esempio di codice sorgente completo per un metodo di estensione per convertire un tipo anonimo in un expando?
Kiquenet

1
@ Md.lbrahim: Fondamentalmente non puoi. Dovresti farlo su objecto su un tipo generico (puoi richiedere che sia una classe ...) e controllare il tipo al momento dell'esecuzione.
Jon Skeet

63

Potresti usare [assembly: InternalsVisibleTo("YourAssemblyName")]per rendere visibili gli interni dell'assieme.


2
La risposta di Jon è più completa, ma in realtà fornisce una soluzione ragionevolmente semplice per me. Grazie :)
kelloti

Ho sbattuto la testa per ore su diversi forum ma non ho trovato una risposta semplice tranne questa. Grazie Luca. Ma ancora non riesco a capire perché un tipo dinamico non è accessibile all'esterno di un assembly come fa nello stesso assembly? Voglio dire, perché questa restrizione in .Net.
Faisal Mq

@FaisalMq è perché il compilatore che genera le classi anonime le dichiara "interne". Non so quale sia la vera ragione.
ema

2
Sì, penso che questa risposta sia importante, perché non voglio cambiare il codice di lavoro, devo solo provarlo da un altro assembly
PandaWood

Una nota da aggiungere qui è che è necessario riavviare Visual Studio dopo questa modifica affinché funzioni.
Rady

11

Ho riscontrato un problema simile e vorrei aggiungere alla risposta di Jon Skeets che esiste un'altra opzione. Il motivo per cui l'ho scoperto è che mi sono reso conto che molti metodi di estensione in Asp MVC3 utilizzano classi anonime come input per fornire attributi html (new {alt = "Image alt", style = "padding-top: 5px"} =>

Comunque, queste funzioni usano il costruttore della classe RouteValueDictionary. L'ho provato io stesso, e abbastanza sicuro funziona, anche se solo il primo livello (ho usato una struttura a più livelli). COSÌ - nel codice questo sarebbe:

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

Allora ... cosa sta succedendo veramente qui? Un'occhiata all'interno di RouteValueDictionary rivela questo codice (valori ~ = o sopra):

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

COSÌ - utilizzando TypeDescriptor.GetProperties (o) saremmo in grado di ottenere le proprietà e i valori nonostante il tipo anonimo venga costruito come interno in un assembly separato! E ovviamente sarebbe abbastanza facile estenderlo per renderlo ricorsivo. E per creare un metodo di estensione, se lo desideri.

Spero che sia di aiuto!

/Vincitore


Scusa per quella confusione. Codice aggiornato da prop1 => p1 dove appropriato. Tuttavia, l'idea con l'intero post era di proporre TypeDescriptor.GetProperties come opzione per risolvere il problema, che si spera fosse comunque chiaro ...
Victor

È davvero stupido che la dinamica non possa farlo per noi. Amo davvero e odio davvero la dinamica.
Chris Marisic

2

Ecco una versione rudimentale di un metodo di estensione per ToExpandoObject che sono sicuro ha spazio per la lucidatura.

    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

Una soluzione più pulita sarebbe:

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

Che ora è un ExpandoObject.

Ricordati di fare riferimento:

Microsoft.CSharp.dll

1

La soluzione sotto ha funzionato per me nei miei progetti di applicazioni console

Metti questo [assembly: InternalsVisibleTo ("YourAssemblyName")] in \ Properties \ AssemblyInfo.cs del progetto separato con la funzione che restituisce l'oggetto dinamico.

"YourAssemblyName" è il nome dell'assembly del progetto chiamante. Puoi ottenerlo tramite Assembly.GetExecutingAssembly (). FullName eseguendolo nella chiamata del progetto.


0

Metodo di estensione ToExpando (menzionato nella risposta di Jon) per i coraggiosi

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

Se stai già utilizzando Newtonsoft.Json nel tuo progetto (o sei disposto ad aggiungerlo per questo scopo), potresti implementare quell'orribile metodo di estensione a cui fa riferimento Jon Skeet nella sua risposta in questo modo:

public static class ObjectExtensions
{
    public static ExpandoObject ToExpando(this object obj)
        => JsonConvert.DeserializeObject<ExpandoObject>(JsonConvert.SerializeObject(obj));
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.