Tester si une propriété est disponible sur une variable dynamique

225

Ma situation est très simple. Quelque part dans mon code, j'ai ceci:

dynamic myVariable = GetDataThatLooksVerySimilarButNotTheSame();

//How to do this?
if (myVariable.MyProperty.Exists)   
//Do stuff

Donc, fondamentalement, ma question est de savoir comment vérifier (sans lever d'exception) qu'une certaine propriété est disponible sur ma variable dynamique. Je pourrais le faire GetType()mais je préfère éviter cela car je n'ai pas vraiment besoin de connaître le type d'objet. Tout ce que je veux vraiment savoir, c'est si une propriété (ou une méthode, si cela facilite la vie) est disponible. Des pointeurs?

crise circulaire
la source
1
Il y a quelques suggestions ici: stackoverflow.com/questions/2985161/… - mais aucune réponse acceptée jusqu'à présent.
Andrew Anderson
merci, je peux voir comment faire du sapin une des solutions, mais je me demandais s'il y avait quelque chose que je
manquais
Copie
Sebastian

Réponses:

159

Je pense qu'il n'y a aucun moyen de savoir si une dynamicvariable a un certain membre sans essayer d'y accéder, à moins que vous n'ayez réimplémenté la façon dont la liaison dynamique est gérée dans le compilateur C #. Ce qui inclurait probablement beaucoup de suppositions, car il est défini par l'implémentation, selon la spécification C #.

Vous devriez donc essayer d'accéder au membre et intercepter une exception, s'il échoue:

dynamic myVariable = GetDataThatLooksVerySimilarButNotTheSame();

try
{
    var x = myVariable.MyProperty;
    // do stuff with x
}
catch (RuntimeBinderException)
{
    //  MyProperty doesn't exist
} 
svick
la source
2
Je vais marquer cela comme la réponse car cela fait si longtemps, cela semble être la meilleure réponse
roundcrisis
8
Meilleure solution - stackoverflow.com/questions/2839598/...
ministrymason
20
@ministrymason Si vous voulez faire un casting IDictionaryet travailler avec cela, cela ne fonctionne que sur ExpandoObject, cela ne fonctionnera sur aucun autre dynamicobjet.
svick
5
RuntimeBinderExceptionest dans l' Microsoft.CSharp.RuntimeBinderespace de noms.
DavidRR
8
J'ai toujours envie d'utiliser try / catch au lieu de if / else est une mauvaise pratique en général, indépendamment de ces spécificités de ce scénario.
Alexander Ryan Baggett
74

Je pensais que je ferais une comparaison de la réponse de Martijn et la réponse de svick ...

Le programme suivant renvoie les résultats suivants:

Testing with exception: 2430985 ticks
Testing with reflection: 155570 ticks

void Main()
{
    var random = new Random(Environment.TickCount);

    dynamic test = new Test();

    var sw = new Stopwatch();

    sw.Start();

    for (int i = 0; i < 100000; i++)
    {
        TestWithException(test, FlipCoin(random));
    }

    sw.Stop();

    Console.WriteLine("Testing with exception: " + sw.ElapsedTicks.ToString() + " ticks");

    sw.Restart();

    for (int i = 0; i < 100000; i++)
    {
        TestWithReflection(test, FlipCoin(random));
    }

    sw.Stop();

    Console.WriteLine("Testing with reflection: " + sw.ElapsedTicks.ToString() + " ticks");
}

class Test
{
    public bool Exists { get { return true; } }
}

bool FlipCoin(Random random)
{
    return random.Next(2) == 0;
}

bool TestWithException(dynamic d, bool useExisting)
{
    try
    {
        bool result = useExisting ? d.Exists : d.DoesntExist;
        return true;
    }
    catch (Exception)
    {
        return false;
    }
}

bool TestWithReflection(dynamic d, bool useExisting)
{
    Type type = d.GetType();

    return type.GetProperties().Any(p => p.Name.Equals(useExisting ? "Exists" : "DoesntExist"));
}

En conséquence, je suggère d'utiliser la réflexion. Voir ci-dessous.


Répondant au commentaire insipide:

Les ratios sont des reflection:exceptiongraduations pour 100 000 itérations:

Fails 1/1: - 1:43 ticks
Fails 1/2: - 1:22 ticks
Fails 1/3: - 1:14 ticks
Fails 1/5: - 1:9 ticks
Fails 1/7: - 1:7 ticks
Fails 1/13: - 1:4 ticks
Fails 1/17: - 1:3 ticks
Fails 1/23: - 1:2 ticks
...
Fails 1/43: - 1:2 ticks
Fails 1/47: - 1:1 ticks

... assez juste - si vous vous attendez à ce qu'il échoue avec une probabilité inférieure à ~ 1/47, optez pour l'exception.


Ce qui précède suppose que vous courez à GetProperties()chaque fois. Vous pourrez peut-être accélérer le processus en mettant en cache le résultat de GetProperties()chaque type dans un dictionnaire ou similaire. Cela peut être utile si vous comparez sans cesse le même ensemble de types.

dav_i
la source
7
J'accepte et j'aime la réflexion dans mon travail le cas échéant. Les gains qu'il a sur Try / Catch se produisent uniquement lorsque l'exception est levée. Donc, ce que quelqu'un devrait demander avant d'utiliser la réflexion ici - est-ce susceptible d'être d'une certaine manière? 90% ou même 75% du temps, votre code passera-t-il? Alors Try / Catch est toujours optimal. Si c'est dans l'air, ou trop de choix pour que l'un soit le plus probable, alors votre réflexion est parfaite.
fade
@bland Réponse modifiée.
dav_i
1
Merci, ça a l'air vraiment complet maintenant.
fade
@dav_i ce n'est pas juste de comparer les deux car les deux se comportent différemment. La réponse de svick est plus complète.
nawfal
1
@dav_i Non, ils n'exécutent pas la même fonction. La réponse de Martijn vérifie si une propriété existe sur un type de temps de compilation régulier en C #, qui est déclaré dynamique (ce qui signifie qu'il ignore les contrôles de sécurité de temps de compilation). Alors que la réponse de svick vérifie si une propriété existe sur un objet vraiment dynamique , c'est-à-dire quelque chose qui implémente IIDynamicMetaObjectProvider. Je comprends la motivation de votre réponse et je l'apprécie. Il est juste de répondre ainsi.
nawfal
52

Peut-être utiliser la réflexion?

dynamic myVar = GetDataThatLooksVerySimilarButNotTheSame();
Type typeOfDynamic = myVar.GetType();
bool exist = typeOfDynamic.GetProperties().Where(p => p.Name.Equals("PropertyName")).Any(); 
Martijn
la source
2
Citation de la question ". Je pourrais faire GetType () mais je préfère éviter ça"
roundcrisis
Cela n'a-t-il pas les mêmes inconvénients que ma suggestion? RouteValueDictionary utilise la réflexion pour obtenir des propriétés .
Steve Wilkes
12
Vous pouvez simplement vous passer de Where:.Any(p => p.Name.Equals("PropertyName"))
dav_i
Veuillez consulter ma réponse pour comparer les réponses.
dav_i
3
En tant que one-liner: ((Type)myVar.GetType()).GetProperties().Any(x => x.Name.Equals("PropertyName")). Le transtypage en type est requis pour rendre le compilateur satisfait du lambda.
MushinNoShin
38

Juste au cas où cela aiderait quelqu'un:

Si la méthode GetDataThatLooksVerySimilarButNotTheSame()renvoie un, ExpandoObjectvous pouvez également transtyper en un IDictionaryavant de vérifier.

dynamic test = new System.Dynamic.ExpandoObject();
test.foo = "bar";

if (((IDictionary<string, object>)test).ContainsKey("foo"))
{
    Console.WriteLine(test.foo);
}
karask
la source
3
Je ne sais pas pourquoi cette réponse n'a pas plus de votes, car elle fait exactement ce qui était demandé (pas d'exception ni de réflexion).
Wolfshead
7
@Wolfshead Cette réponse est excellente si vous savez que votre objet dynamique est un ExpandoObject ou quelque chose d'autre qui implémente IDictionary <chaîne, objet> mais s'il se trouve que c'est autre chose, alors cela échouera.
Damian Powell
9

Les deux solutions courantes à cela incluent l'appel et la capture RuntimeBinderException, l'utilisation de la réflexion pour vérifier l'appel, ou la sérialisation au format texte et l'analyse à partir de là. Le problème avec les exceptions est qu'elles sont très lentes, car lorsqu'une est construite, la pile d'appels actuelle est sérialisée. La sérialisation vers JSON ou quelque chose d'analogue entraîne une pénalité similaire. Cela nous laisse avec réflexion mais cela ne fonctionne que si l'objet sous-jacent est en fait un POCO avec de vrais membres dessus. S'il s'agit d'un wrapper dynamique autour d'un dictionnaire, d'un objet COM ou d'un service Web externe, la réflexion n'aidera pas.

Une autre solution consiste à utiliser le DynamicMetaObjectpour obtenir les noms des membres tels que le DLR les voit. Dans l'exemple ci-dessous, j'utilise une classe statique ( Dynamic) pour tester le Agechamp et l'afficher.

class Program
{
    static void Main()
    {
        dynamic x = new ExpandoObject();

        x.Name = "Damian Powell";
        x.Age = "21 (probably)";

        if (Dynamic.HasMember(x, "Age"))
        {
            Console.WriteLine("Age={0}", x.Age);
        }
    }
}

public static class Dynamic
{
    public static bool HasMember(object dynObj, string memberName)
    {
        return GetMemberNames(dynObj).Contains(memberName);
    }

    public static IEnumerable<string> GetMemberNames(object dynObj)
    {
        var metaObjProvider = dynObj as IDynamicMetaObjectProvider;

        if (null == metaObjProvider) throw new InvalidOperationException(
            "The supplied object must be a dynamic object " +
            "(i.e. it must implement IDynamicMetaObjectProvider)"
        );

        var metaObj = metaObjProvider.GetMetaObject(
            Expression.Constant(metaObjProvider)
        );

        var memberNames = metaObj.GetDynamicMemberNames();

        return memberNames;
    }
}
Damian Powell
la source
Il s'avère que le Dynamiteypaquet nuget le fait déjà. ( nuget.org/packages/Dynamitey )
Damian Powell
8

La réponse de Denis m'a fait penser à une autre solution utilisant JsonObjects,

un vérificateur de propriété d'en-tête:

Predicate<object> hasHeader = jsonObject =>
                                 ((JObject)jsonObject).OfType<JProperty>()
                                     .Any(prop => prop.Name == "header");

ou peut-être mieux:

Predicate<object> hasHeader = jsonObject =>
                                 ((JObject)jsonObject).Property("header") != null;

par exemple:

dynamic json = JsonConvert.DeserializeObject(data);
string header = hasHeader(json) ? json.header : null;
Charles HETIER
la source
1
Y a-t-il une chance de savoir ce qui ne va pas avec cette réponse s'il vous plaît?
Charles HETIER
Je ne sais pas pourquoi cela a été rejeté, a très bien fonctionné pour moi. J'ai déplacé le prédicat pour chaque propriété dans une classe d'assistance et j'ai appelé la méthode Invoke pour renvoyer un booléen de chacune.
markp3rry
7

Eh bien, j'ai fait face à un problème similaire mais lors de tests unitaires.

En utilisant SharpTestsEx, vous pouvez vérifier si une propriété existe. J'utilise cela pour tester mes contrôleurs, car comme l'objet JSON est dynamique, quelqu'un peut changer le nom et oublier de le changer dans le javascript ou quelque chose, donc tester toutes les propriétés lors de l'écriture du contrôleur devrait augmenter ma sécurité.

Exemple:

dynamic testedObject = new ExpandoObject();
testedObject.MyName = "I am a testing object";

Maintenant, en utilisant SharTestsEx:

Executing.This(delegate {var unused = testedObject.MyName; }).Should().NotThrow();
Executing.This(delegate {var unused = testedObject.NotExistingProperty; }).Should().Throw();

En utilisant cela, je teste toutes les propriétés existantes en utilisant "Should (). NotThrow ()".

C'est probablement hors sujet, mais peut être utile à quelqu'un.

Diego Santin
la source
Merci, très utile. En utilisant SharpTestsEx, j'utilise la ligne suivante pour tester également la valeur de la propriété dynamique:((string)(testedObject.MyName)).Should().Be("I am a testing object");
Remko Jansen
2

À la suite de la réponse de @karask, vous pouvez envelopper la fonction comme une aide comme ceci:

public static bool HasProperty(ExpandoObject expandoObj,
                               string name)
{
    return ((IDictionary<string, object>)expandoObj).ContainsKey(name);
}
Wolfshead
la source
2

Pour moi, cela fonctionne:

if (IsProperty(() => DynamicObject.MyProperty))
  ; // do stuff



delegate string GetValueDelegate();

private bool IsProperty(GetValueDelegate getValueMethod)
{
    try
    {
        //we're not interesting in the return value.
        //What we need to know is whether an exception occurred or not

        var v = getValueMethod();
        return v != null;
    }
    catch (RuntimeBinderException)
    {
        return false;
    }
    catch
    {
        return true;
    }
}
bouffon
la source
nullne signifie pas que la propriété n'existe pas
quetzalcoatl
Je sais, mais si elle est nulle, je n'ai rien à faire avec la valeur, donc pour mon utilisation, c'est ok
Jester
0

Si vous contrôlez le type utilisé comme dynamique, ne pourriez-vous pas retourner un tuple au lieu d'une valeur pour chaque accès à la propriété? Quelque chose comme...

public class DynamicValue<T>
{
    internal DynamicValue(T value, bool exists)
    {
         Value = value;
         Exists = exists;
    }

    T Value { get; private set; }
    bool Exists { get; private set; }
}

Peut-être une implémentation naïve, mais si vous en construisez une à chaque fois en interne et que vous renvoyez cela au lieu de la valeur réelle, vous pouvez vérifier Existschaque accès à la propriété, puis frapper Valuesi c'est le cas avec value étant default(T)(et non pertinent) si ce n'est pas le cas.

Cela dit, je manque peut-être des connaissances sur le fonctionnement de la dynamique et ce n'est peut-être pas une suggestion viable.

Shibumi
la source
0

Dans mon cas, je devais vérifier l'existence d'une méthode avec un nom spécifique, j'ai donc utilisé une interface pour cela

var plugin = this.pluginFinder.GetPluginIfInstalled<IPlugin>(pluginName) as dynamic;
if (plugin != null && plugin is ICustomPluginAction)
{
    plugin.CustomPluginAction(action);
}

De plus, les interfaces peuvent contenir plus que de simples méthodes:

Les interfaces peuvent contenir des méthodes, des propriétés, des événements, des indexeurs ou toute combinaison de ces quatre types de membres.

De: Interfaces (Guide de programmation C #)

Élégant et pas besoin de piéger les exceptions ou de jouer avec la réflexion ...

Fred Mauroy
la source
0

Je sais que c'est vraiment un ancien post, mais voici une solution simple pour travailler avec dynamictaper c#.

  1. peut utiliser une réflexion simple pour énumérer les propriétés directes
  2. ou peut utiliser la objectméthode d'extension
  3. ou utilisez la GetAsOrDefault<int>méthode pour obtenir un nouvel objet fortement typé avec une valeur s'il existe ou par défaut s'il n'existe pas.
public static class DynamicHelper
{
    private static void Test( )
    {
        dynamic myobj = new
                        {
                            myInt = 1,
                            myArray = new[ ]
                                      {
                                          1, 2.3
                                      },
                            myDict = new
                                     {
                                         myInt = 1
                                     }
                        };

        var myIntOrZero = myobj.GetAsOrDefault< int >( ( Func< int > )( ( ) => myobj.noExist ) );
        int? myNullableInt = GetAs< int >( myobj, ( Func< int > )( ( ) => myobj.myInt ) );

        if( default( int ) != myIntOrZero )
            Console.WriteLine( $"myInt: '{myIntOrZero}'" );

        if( default( int? ) != myNullableInt )
            Console.WriteLine( $"myInt: '{myNullableInt}'" );

        if( DoesPropertyExist( myobj, "myInt" ) )
            Console.WriteLine( $"myInt exists and it is: '{( int )myobj.myInt}'" );
    }

    public static bool DoesPropertyExist( dynamic dyn, string property )
    {
        var t = ( Type )dyn.GetType( );
        var props = t.GetProperties( );
        return props.Any( p => p.Name.Equals( property ) );
    }

    public static object GetAs< T >( dynamic obj, Func< T > lookup )
    {
        try
        {
            var val = lookup( );
            return ( T )val;
        }
        catch( RuntimeBinderException ) { }

        return null;
    }

    public static T GetAsOrDefault< T >( this object obj, Func< T > test )
    {
        try
        {
            var val = test( );
            return ( T )val;
        }
        catch( RuntimeBinderException ) { }

        return default( T );
    }
}
SimperT
la source
0

Comme ExpandoObjecthérite du, IDictionary<string, object>vous pouvez utiliser la vérification suivante

dynamic myVariable = GetDataThatLooksVerySimilarButNotTheSame();

if (((IDictionary<string, object>)myVariable).ContainsKey("MyProperty"))    
//Do stuff

Vous pouvez créer une méthode utilitaire pour effectuer cette vérification, qui rendra le code beaucoup plus propre et réutilisable.

Mukesh Bhojwani
la source
-1

Voici l'autre voie:

using Newtonsoft.Json.Linq;

internal class DymanicTest
{
    public static string Json = @"{
            ""AED"": 3.672825,
            ""AFN"": 56.982875,
            ""ALL"": 110.252599,
            ""AMD"": 408.222002,
            ""ANG"": 1.78704,
            ""AOA"": 98.192249,
            ""ARS"": 8.44469
}";

    public static void Run()
    {
        dynamic dynamicObject = JObject.Parse(Json);

        foreach (JProperty variable in dynamicObject)
        {
            if (variable.Name == "AMD")
            {
                var value = variable.Value;
            }
        }
    }
}
Denis
la source
2
D'où vous est venue l'idée que la question est de tester les propriétés de JObject? Votre réponse se limite aux objets / classes qui exposent IEnumerable sur leurs propriétés. Non garanti par dynamic. dynamicmot-clé est un sujet beaucoup plus large. Allez voir si vous pouvez tester Countdans ce dynamic foo = new List<int>{ 1,2,3,4 }genre
Quetzalcoatl