ChatGPT解决这个技术问题 Extra ChatGPT

Test if a property is available on a dynamic variable

My situation is very simple. Somewhere in my code I have this:

dynamic myVariable = GetDataThatLooksVerySimilarButNotTheSame();

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

So, basically my question is how to check (without throwing an exception) that a certain property is available on my dynamic variable. I could do GetType() but I'd rather avoid that since I don't really need to know the type of the object. All that I really want to know is whether a property (or method, if that makes life easier) is available. Any pointers?

There are a couple of suggestions here: stackoverflow.com/questions/2985161/… - but no accepted answer so far.
thanks, I can see how to make fir one of the solutions, tho I was wondering if there is anything I m missing out

s
svick

I think there is no way to find out whether a dynamic variable has a certain member without trying to access it, unless you re-implemented the way dynamic binding is handled in the C# compiler. Which would probably include a lot of guessing, because it is implementation-defined, according to the C# specification.

So you should actually try to access the member and catch an exception, if it fails:

dynamic myVariable = GetDataThatLooksVerySimilarButNotTheSame();

try
{
    var x = myVariable.MyProperty;
    // do stuff with x
}
catch (RuntimeBinderException)
{
    //  MyProperty doesn't exist
} 

I ll mark this as the answer as its been so long, it does seem to be the best answer
@ministrymason If you mean casting to IDictionary and work with that, that works only on ExpandoObject, it won't work on any other dynamic object.
RuntimeBinderException is in the Microsoft.CSharp.RuntimeBinder namespace.
I still feel like using try/catch instead of if/else is bad practice in general regardless of this specifics of this scenario.
C
Community

I thought I'd do a comparison of Martijn's answer and svick's answer...

The following program returns the following results:

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

As a result I'd suggest using reflection. See below.

Responding to bland's comment:

Ratios are reflection:exception ticks for 100000 iterations:

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

...fair enough - if you expect it to fail with a probability with less than ~1/47, then go for exception.

The above assumes that you're running GetProperties() each time. You may be able to speed up the process by caching the result of GetProperties() for each type in a dictionary or similar. This may help if you're checking against the same set of types over and again.


I agree and like reflection in my work where appropriate. The gains it has over Try/Catch is solely when the exception is thrown. So what someone should ask before using reflection here - is it likely to be a certain way? 90% or even 75% of the time, will your code pass? Then Try/Catch is still optimal. If its up in the air, or too many choices for one to be most likely, then your reflection is spot on.
@bland Edited answer.
Thanks, looks really complete now.
@dav_i it's not fair to compare both since both behave differently. svick's answer is more complete.
@dav_i No, they dont perform the same function. Martijn's answer checks if a property exist on a regular compile time type in C#, that is declared dynamic (meaning it ignores compile time safety checks). Whereas svick's answer checks if a property exists on a truly dynamic object, ie something that implements IIDynamicMetaObjectProvider. I do understand the motivation behind your answer, and appreciate it. It's fair to answer so.
M
Martijn

Maybe use reflection?

dynamic myVar = GetDataThatLooksVerySimilarButNotTheSame();
Type typeOfDynamic = myVar.GetType();
bool exist = typeOfDynamic.GetProperties().Where(p => p.Name.Equals("PropertyName")).Any(); 

Quote from the question ". I could do GetType() but I'd rather avoid that"
Doesn't this have the same drawbacks as my suggestion? RouteValueDictionary uses reflection to get properties.
You can just do without the Where: .Any(p => p.Name.Equals("PropertyName"))
Please see my answer for answer comparison.
As a one-liner: ((Type)myVar.GetType()).GetProperties().Any(x => x.Name.Equals("PropertyName")). The cast to type is required to make the compiler happy about the lambda.
k
karask

Just in case it helps someone:

If the method GetDataThatLooksVerySimilarButNotTheSame() returns an ExpandoObject you can also cast to a IDictionary before checking.

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

if (((IDictionary<string, object>)test).ContainsKey("foo"))
{
    Console.WriteLine(test.foo);
}

Not sure why this answer doesn't have more votes, because it does exactly what was asked for (no exception throw or reflection).
@Wolfshead This answer is great if you know that your dynamic object is an ExpandoObject or something else which implements IDictionary but if it happens to be something else, then this will fail.
I
Ian Kemp

The two common solutions to this include making the call and catching the RuntimeBinderException, using reflection to check for the call, or serialising to a text format and parsing from there. The problem with exceptions is that they are very slow, because when one is constructed, the current call stack is serialised. Serialising to JSON or something analogous incurs a similar penalty. This leaves us with reflection but it only works if the underlying object is actually a POCO with real members on it. If it's a dynamic wrapper around a dictionary, a COM object, or an external web service, then reflection won't help.

Another solution is to use IDynamicMetaObjectProvider to get the member names as the DLR sees them. In the example below, I use a static class (Dynamic) to test for the Age field and display it.

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

It turns out that the Dynamitey nuget package already does this. (nuget.org/packages/Dynamitey)
Not working, tested on VS2022 and GetMembersName returns null parsing object with many properties
q
quetzalcoatl

Denis's answer made me think to another solution using JsonObjects,

a header property checker:

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

or maybe better:

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

for example:

dynamic json = JsonConvert.DeserializeObject(data);
string header = hasHeader(json) ? json.header : null;

Is there a chance to know what is wrong with this answer please?
Don't know why this got voted down, worked great for me. I moved the Predicate for each property into a helper class and called the Invoke method to return a bool from each one.
@CharlesHETIER Because this question isn't about JSON.
@IanKemp I think you missed the purpose of the code here. This particular idea for solving the problem with Json API may not be the natural way, but it does offer a way of solving the question.
D
Diego Santin

Well, I faced a similar problem but on unit tests.

Using SharpTestsEx you can check if a property existis. I use this testing my controllers, because since the JSON object is dynamic, someone can change the name and forget to change it in the javascript or something, so testing for all properties when writing the controller should increase my safety.

Example:

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

Now, using SharTestsEx:

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

Using this, i test all existing properties using "Should().NotThrow()".

It's probably out of topic, but can be usefull for someone.


Thanks, very useful. Using SharpTestsEx I use the following line to also test the value of the dynamic property: ((string)(testedObject.MyName)).Should().Be("I am a testing object");
W
Wolfshead

Following on from the answer by @karask, you could wrap the function as a helper like so:

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

J
Junek

For me this works:

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

null does not mean the property does not exist
I know but if it is null I don't need to do anything with the value therefore for my usecase it is ok
S
Shibumi

If you control the type being used as dynamic, couldn't you return a tuple instead of a value for every property access? Something like...

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

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

Possibly a naive implementation, but if you construct one of these internally each time and return that instead of the actual value, you can check Exists on every property access and then hit Value if it does with value being default(T) (and irrelevant) if it doesn't.

That said, I might be missing some knowledge on how dynamic works and this might not be a workable suggestion.


V
Vaccano

Here is the other way:

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

where did you get the idea the question is about testing JObject's properties? Your answer is limited to objects/classes that expose IEnumerable over their properties. Not guaranteed by dynamic. dynamic keyword is much wider subject. Go check if you can test for Count in dynamic foo = new List<int>{ 1,2,3,4 } like that
F
Fred Mauroy

In my case, I needed to check for the existence of a method with a specific name, so I used an interface for that

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

Also, interfaces can contain more than just methods:

Interfaces can contain methods, properties, events, indexers, or any combination of those four member types.

From: Interfaces (C# Programming Guide)

Elegant and no need to trap exceptions or play with reflexion...


S
SimperT

I know this is really old post but here is a simple solution to work with dynamic type in c#.

can use simple reflection to enumerate direct properties or can use the object extention method or use GetAsOrDefault method to get a new strongly typed object with value if exists or default if not exists.

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

M
Mukesh Bhojwani

As ExpandoObject inherits the IDictionary<string, object> you can use the following check

dynamic myVariable = GetDataThatLooksVerySimilarButNotTheSame();

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

You can make a utility method to perform this check, that will make the code much cleaner and re-usable


I think that's already been covered in other answers here stackoverflow.com/a/29693414 (along with potential issue in the comments) and here stackoverflow.com/a/37938989