C # 'dynamic' ne peut pas accéder aux propriétés des types anonymes déclarés dans un autre assembly

87

Le code ci-dessous fonctionne bien tant que j'ai la classe ClassSameAssemblydans le même assembly que la classe Program. Mais lorsque je déplace la classe ClassSameAssemblyvers un assembly séparé, un RuntimeBinderException(voir ci-dessous) est lancé. Est-il possible de le résoudre?

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: 'objet' ne contient pas de définition pour 'Nom'

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
la source
StackTrace: à CallSite.Target (Closure, CallSite, Object) à System.Dynamic.UpdateDelegates.UpdateAndExecute1 [T0, TRet] (site CallSite, T0 arg0) à ConsoleApplication2.Program.Main (String [] args) dans C: \ temp \ Projects \ ConsoleApplication2 \ ConsoleApplication2 \ Program.cs: ligne 23 à System.AppDomain._nExecuteAssembly (assemblage RuntimeAssembly, String [] args) à System.AppDomain.nExecuteAssembly (assemblage RuntimeAssembly, String [] args) à System.AppDomain.ExecuteAssembly ( String assemblyFile, Evidence assemblySecurity, String [] args)
mehanik
à Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly () à System.Threading.ThreadHelper.ThreadStart_Context (état de l'objet) à System.Threading.ExecutionContext.Run (ExecutionContext executionContext, ContextCallback callback, à l'état de l'objet ignorer.technologie. ExecutionContext.Run (ExecutionContext executionContext, ContextCallback callback, Object state) à System.Threading.ThreadHelper.ThreadStart () InnerException:
mehanik
une solution finale avec le code source complet?
Kiquenet

Réponses:

116

Je crois que le problème est que le type anonyme est généré en tant que internal, de sorte que le classeur ne le "sait" pas vraiment en tant que tel.

Essayez plutôt d'utiliser ExpandoObject:

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

Je sais que c'est un peu moche, mais c'est le meilleur auquel je puisse penser pour le moment ... Je ne pense pas que vous puissiez même utiliser un initialiseur d'objet avec, car bien qu'il soit fortement typé car ExpandoObjectle compilateur ne saura pas quoi faire avec "Nom" et "Âge". Vous pourrez peut- être faire ceci:

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

mais ce n'est pas beaucoup mieux ...

Vous pourriez potentiellement écrire une méthode d'extension pour convertir un type anonyme en un expando avec le même contenu via la réflexion. Ensuite, vous pourriez écrire:

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

C'est assez horrible cependant :(

Jon Skeet
la source
1
Merci Jon. J'ai juste eu le même problème en utilisant une classe qui se trouvait être privée pour l'assembly.
Dave Markle
2
J'aimerais quelque chose comme votre horrible exemple à la fin, mais pas aussi horrible. Pour utiliser: dynamic props = new {Metadata = DetailModelMetadata.Create, PageTitle = "New Content", PageHeading = "Content Management"}; et avoir les accessoires nommés ajoutés en tant que membres dynamiques serait génial!
ProfK
L' interface impromptue du framework open source fait beaucoup avec le dlr, elle a une syntaxe d'initialisation en ligne qui fonctionne pour tout objet dynamique ou statique. return Build<ExpandoObject>.NewObject(Name:"Micheal", Age: 20);
jbtule
1
un exemple de code source complet pour une méthode d'extension pour convertir un type anonyme en un expando?
Kiquenet
1
@ Md.lbrahim: Vous ne pouvez pas, en gros. Vous devrez le faire sur objectou sur un type générique (vous pouvez exiger que ce soit une classe ...) et vérifier le type au moment de l'exécution.
Jon Skeet
63

Vous pouvez utiliser [assembly: InternalsVisibleTo("YourAssemblyName")]pour rendre visibles les composants internes de votre assemblage.

ema
la source
2
La réponse de Jon est plus complète, mais cela me fournit en fait une solution de contournement assez simple. Merci :)
kelloti
Je me cognais la tête pendant des heures sur différents forums mais je n'ai trouvé aucune réponse simple à part celle-ci. Merci Luke. Mais je ne comprends toujours pas pourquoi un type dynamique n'est pas accessible en dehors d'un assembly comme il le fait dans le même assembly? Je veux dire pourquoi cette restriction dans .Net.
Faisal Mq
@FaisalMq c'est parce que le compilateur qui génère les classes anonymes les déclare "internes". Je ne sais pas quelle est la vraie raison.
ema
2
Oui, je pense que cette réponse est importante, car je ne veux pas changer le code de travail, j'ai juste besoin de le tester à partir d'un autre assemblage
PandaWood
Une note à ajouter ici est que vous devez redémarrer Visual Studio après cette modification pour que cela fonctionne.
Rady
11

J'ai rencontré un problème similaire et je voudrais ajouter à la réponse de Jon Skeets qu'il existe une autre option. La raison pour laquelle j'ai découvert est que j'ai réalisé que de nombreuses méthodes d'extension dans Asp MVC3 utilisent des classes anonymes comme entrée pour fournir des attributs html (new {alt = "Image alt", style = "padding-top: 5px"} =>

Quoi qu'il en soit - ces fonctions utilisent le constructeur de la classe RouteValueDictionary. J'ai essayé moi-même, et bien sûr cela fonctionne - bien que seulement le premier niveau (j'ai utilisé une structure à plusieurs niveaux). SO - dans le code, ce serait:

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 ... Que se passe-t-il vraiment ici? Un coup d'œil à l'intérieur du RouteValueDictionary révèle ce code (valeurs ~ = o ci-dessus):

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 - en utilisant TypeDescriptor.GetProperties (o), nous pourrions obtenir les propriétés et les valeurs malgré le type anonyme étant construit comme interne dans un assembly séparé! Et bien sûr, ce serait assez facile à étendre pour le rendre récursif. Et pour faire une méthode d'extension si vous le vouliez.

J'espère que cela t'aides!

/Victor

Victor
la source
Désolé pour cette confusion. Code mis à jour à partir de prop1 => p1 le cas échéant. Pourtant - l'idée avec l'ensemble du message était de proposer TypeDescriptor.GetProperties comme une option pour résoudre le problème, ce qui, espérons-le, était clair de toute façon ...
Victor
C'est vraiment stupide que la dynamique ne puisse pas faire ça pour nous. J'aime vraiment et déteste vraiment la dynamique.
Chris Marisic
2

Voici une version rudimentaire d'une méthode d'extension pour ToExpandoObject qui, j'en suis sûr, a de la place pour le polissage.

    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
la source
1

Une solution plus propre serait:

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

Qui est maintenant un ExpandoObject.

N'oubliez pas de mentionner:

Microsoft.CSharp.dll
Zylv3r
la source
1

La solution ci-dessous a fonctionné pour moi dans mes projets d'application console

Placez cet [assembly: InternalsVisibleTo ("YourAssemblyName")] dans \ Properties \ AssemblyInfo.cs du projet séparé avec la fonction renvoyant un objet dynamique.

"YourAssemblyName" est le nom d'assembly du projet appelant. Vous pouvez l'obtenir via Assembly.GetExecutingAssembly (). FullName en l'exécutant en appelant project.

Shah
la source
0

Méthode d'extension ToExpando (mentionnée dans la réponse de Jon) pour les courageux

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
la source
0

Si vous utilisez déjà Newtonsoft.Json dans votre projet (ou si vous êtes prêt à l'ajouter à cette fin), vous pouvez implémenter cette horrible méthode d'extension à laquelle Jon Skeet fait référence dans sa réponse comme ceci:

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