Port DynamicMetaObjectProviderDebugView (dotnet/corefx#38656)
authorCharles Stoner <chucks@microsoft.com>
Wed, 19 Jun 2019 08:26:05 +0000 (01:26 -0700)
committerDan Moseley <danmose@microsoft.com>
Wed, 19 Jun 2019 08:26:05 +0000 (01:26 -0700)
* Port DynamicMetaObjectProviderDebugView

* Remove System.Diagnostics.CodeAnalysis.SuppressMessage attributes

* Add explicit private modifier

Commit migrated from https://github.com/dotnet/corefx/commit/af04307b356e38e2b74df1f05f735ebc462850da

src/libraries/Microsoft.CSharp/src/ILLinkTrim.xml [new file with mode: 0644]
src/libraries/Microsoft.CSharp/src/Microsoft.CSharp.csproj
src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/DynamicDebuggerProxy.cs [new file with mode: 0644]
src/libraries/Microsoft.CSharp/src/Resources/Strings.resx
src/libraries/Microsoft.CSharp/tests/DynamicDebuggerProxyTests.cs [new file with mode: 0644]
src/libraries/Microsoft.CSharp/tests/Microsoft.CSharp.Tests.csproj

diff --git a/src/libraries/Microsoft.CSharp/src/ILLinkTrim.xml b/src/libraries/Microsoft.CSharp/src/ILLinkTrim.xml
new file mode 100644 (file)
index 0000000..a0422f8
--- /dev/null
@@ -0,0 +1,8 @@
+<linker>
+  <assembly fullname="Microsoft.CSharp">
+    <!-- Required by the Expression Evaluator -->
+    <type fullname="Microsoft.CSharp.RuntimeBinder.DynamicMetaObjectProviderDebugView"/>
+    <!-- Required by the Expression Evaluator -->
+    <type fullname="Microsoft.CSharp.RuntimeBinder.DynamicMetaObjectProviderDebugView/DynamicDebugViewEmptyException"/>
+  </assembly>
+</linker>
\ No newline at end of file
index eed56f7..679e3ac 100644 (file)
@@ -29,6 +29,7 @@
     <Compile Include="Microsoft\CSharp\RuntimeBinder\CSharpSetIndexBinder.cs" />
     <Compile Include="Microsoft\CSharp\RuntimeBinder\CSharpSetMemberBinder.cs" />
     <Compile Include="Microsoft\CSharp\RuntimeBinder\CSharpUnaryOperationBinder.cs" />
+    <Compile Include="Microsoft\CSharp\RuntimeBinder\DynamicDebuggerProxy.cs" />
     <Compile Include="Microsoft\CSharp\RuntimeBinder\Error.cs" />
     <Compile Include="Microsoft\CSharp\RuntimeBinder\Errors\ErrorCode.cs" />
     <Compile Include="Microsoft\CSharp\RuntimeBinder\Errors\ErrorFacts.cs" />
diff --git a/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/DynamicDebuggerProxy.cs b/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/DynamicDebuggerProxy.cs
new file mode 100644 (file)
index 0000000..6169d54
--- /dev/null
@@ -0,0 +1,464 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Dynamic;
+using System.Linq.Expressions;
+using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
+
+namespace Microsoft.CSharp.RuntimeBinder
+{
+    [Serializable]
+    [EditorBrowsable(EditorBrowsableState.Never)]
+    internal sealed class DynamicBindingFailedException : Exception
+    {
+        public DynamicBindingFailedException()
+            : base()
+        {
+        }
+
+        private DynamicBindingFailedException(SerializationInfo info, StreamingContext context) 
+            : base(info, context) 
+        { 
+        }
+    }
+
+    internal sealed class GetMemberValueBinder : GetMemberBinder
+    {
+        public GetMemberValueBinder(string name, bool ignoreCase)
+            : base(name, ignoreCase)
+        {
+        }
+
+        public override DynamicMetaObject FallbackGetMember(DynamicMetaObject self, DynamicMetaObject onBindingError)
+        {
+            if (onBindingError == null)
+            {
+                var v = new List<DynamicMetaObject> { self };
+                var error = new DynamicMetaObject(System.Linq.Expressions.Expression.Throw(
+                    System.Linq.Expressions.Expression.Constant(new DynamicBindingFailedException(), typeof(Exception)), typeof(object)), System.Dynamic.BindingRestrictions.Combine(v));
+                return error;
+            }
+            return onBindingError;
+        }
+    }
+
+    internal sealed class DynamicMetaObjectProviderDebugView
+    {
+        [System.Diagnostics.DebuggerDisplay("{value}", Name = "{name, nq}", Type = "{type, nq}")]
+        internal class DynamicProperty
+        {
+            [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+            private readonly string name;
+
+            [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+            private readonly object value;
+
+            [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+            private readonly string type;
+
+            public DynamicProperty(string name, object value)
+            {
+                this.name = name;
+                this.value = value;
+                this.type = value == null ? "<null>" : value.GetType().ToString();
+            }
+        }
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private IList<KeyValuePair<string, object>> results = null;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private object obj;
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)]
+        internal DynamicProperty[] Items
+        {
+            get
+            {
+                if (results == null || results.Count == 0)
+                {
+                    results = QueryDynamicObject(obj);
+                    if (results == null || results.Count == 0)
+                    {
+                        throw new DynamicDebugViewEmptyException();
+                    }
+                }
+                DynamicProperty[] pairArray = new DynamicProperty[results.Count];
+                for (int i = 0; i < results.Count; i++)
+                {
+                    pairArray[i] = new DynamicProperty(results[i].Key, results[i].Value);
+                }
+                return pairArray;
+            }
+        }
+
+        public DynamicMetaObjectProviderDebugView(object arg)
+        {
+            this.obj = arg;
+        }
+
+        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
+        private static readonly ParameterExpression parameter = Expression.Parameter(typeof(object), "debug");
+
+        public static object TryEvalBinaryOperators<T1, T2>(
+            T1 arg1, 
+            T2 arg2, 
+            CSharpArgumentInfoFlags arg1Flags,
+            CSharpArgumentInfoFlags arg2Flags,
+            ExpressionType opKind,
+            Type accessibilityContext)
+        {
+            CSharpArgumentInfo arg1Info = CSharpArgumentInfo.Create(arg1Flags, null);
+            CSharpArgumentInfo arg2Info = CSharpArgumentInfo.Create(arg2Flags, null);
+
+            CSharpBinaryOperationBinder binder = new CSharpBinaryOperationBinder(
+                opKind,
+                false, // isChecked
+                CSharpBinaryOperationFlags.None,
+                accessibilityContext,
+                new CSharpArgumentInfo[] { arg1Info, arg2Info });
+
+            var site = CallSite<Func<CallSite, T1, T2, object>>.Create(binder);
+            return site.Target(site, arg1, arg2);
+        }
+
+        public static object TryEvalUnaryOperators<T>(T obj, ExpressionType oper, Type accessibilityContext)
+        {
+            if (oper == ExpressionType.IsTrue || oper == ExpressionType.IsFalse)
+            {
+                var trueFalseSite = CallSite<Func<CallSite, T, bool>>
+                    .Create(new Microsoft.CSharp.RuntimeBinder.CSharpUnaryOperationBinder(oper,
+                        false,
+                        accessibilityContext,
+                        new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }));
+                return trueFalseSite.Target(trueFalseSite, obj);
+            }
+
+            var site = CallSite<Func<CallSite, T, object>>
+                .Create(new Microsoft.CSharp.RuntimeBinder.CSharpUnaryOperationBinder(oper,
+                    false,
+                    accessibilityContext,
+                    new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }));
+            return site.Target(site, obj);
+        }
+
+        public static K TryEvalCast<T, K>(T obj, Type type, CSharpBinderFlags kind, Type accessibilityContext)
+        {
+            var site = CallSite<Func<CallSite, T, K>>.Create(Binder.Convert(kind, type, accessibilityContext));
+            return site.Target(site, obj);
+        }
+
+        /// <summary>
+        /// Creates array of types that describes delegate's signature and array of 
+        /// CSharpArgumentInfoFlags that describe each of the arguments.
+        /// </summary>
+        private static void CreateDelegateSignatureAndArgumentInfos(
+            object[] args,
+            Type[] argTypes,
+            CSharpArgumentInfoFlags[] argFlags,
+            out Type[] delegateSignatureTypes,
+            out CSharpArgumentInfo[] argInfos)
+        {
+            int numberOfArguments = args.Length;
+            Debug.Assert((numberOfArguments == argTypes.Length) && (numberOfArguments == argFlags.Length), "Argument arrays size mismatch.");
+
+            delegateSignatureTypes = new Type[numberOfArguments + 2];
+            delegateSignatureTypes[0] = typeof(CallSite);
+
+            argInfos = new CSharpArgumentInfo[numberOfArguments];
+
+            for (int i = 0; i < numberOfArguments; i++)
+            {
+                if (argTypes[i] != null)
+                {
+                    delegateSignatureTypes[i + 1] = argTypes[i];
+                }
+                else if (args[i] != null)
+                {
+                    delegateSignatureTypes[i + 1] = args[i].GetType();
+                }
+                else
+                {
+                    delegateSignatureTypes[i + 1] = typeof(object);
+                }
+
+                argInfos[i] = CSharpArgumentInfo.Create(argFlags[i], null);
+            }
+
+            delegateSignatureTypes[numberOfArguments + 1] = typeof(object); // type of return value
+        }
+
+        /// <summary>
+        /// Creates a delegate based on type array that describe its signature and invokes it.
+        /// </summary>
+        /// <returns>Result of invoking the delegate.</returns>
+        private static object CreateDelegateAndInvoke(Type[] delegateSignatureTypes, CallSiteBinder binder, object[] args)
+        {
+            Type delegateType = Expression.GetDelegateType(delegateSignatureTypes);
+            var site = CallSite.Create(delegateType, binder);
+
+            Delegate target = (Delegate)site.GetType().GetField("Target").GetValue(site);
+
+            object[] argsWithSite = new object[args.Length + 1];
+            argsWithSite[0] = site;
+            args.CopyTo(argsWithSite, 1);
+
+            object result = target.DynamicInvoke(argsWithSite);
+            return result;
+        }
+
+        /// <summary>
+        /// DynamicOperatorRewriter in EE generates call to this method to dynamically invoke a method.
+        /// </summary>
+        /// <param name="methodArgs">Array that contains method arguments. The first element is an object on which method should be called.</param>
+        /// <param name="argTypes">Type of each argument in methodArgs.</param>
+        /// <param name="argFlags">Flags describing each argument.</param>
+        /// <param name="methodName">Name of a method to invoke.</param>
+        /// <param name="accessibilityContext">Type that determines context in which method should be called.</param>
+        /// <param name="typeArguments">Generic type arguments if there are any.</param>
+        /// <returns>Result of method invocation.</returns>
+        public static object TryEvalMethodVarArgs(
+            object[] methodArgs, 
+            Type[] argTypes,
+            CSharpArgumentInfoFlags[] argFlags, 
+            string methodName, 
+            Type accessibilityContext, 
+            Type[] typeArguments)
+        {
+            Type[] delegateSignatureTypes = null;
+            CSharpArgumentInfo[] argInfos = null;
+
+            CreateDelegateSignatureAndArgumentInfos(
+                methodArgs,
+                argTypes,
+                argFlags,
+                out delegateSignatureTypes,
+                out argInfos);
+
+            CallSiteBinder binder;
+            if (String.IsNullOrEmpty(methodName))
+            {
+                //null or empty indicates delegate invocation.
+                binder = new CSharpInvokeBinder(
+                    CSharpCallFlags.ResultDiscarded, 
+                    accessibilityContext,
+                    argInfos);
+            }
+            else
+            {
+                binder = new CSharpInvokeMemberBinder(
+                    CSharpCallFlags.ResultDiscarded,
+                    methodName,
+                    accessibilityContext,
+                    typeArguments,
+                    argInfos);
+            }
+
+            return CreateDelegateAndInvoke(delegateSignatureTypes, binder, methodArgs);
+        }
+
+        /// <summary>
+        /// DynamicOperatorRewriter in EE generates call to this method to dynamically invoke a property getter
+        /// with no arguments.
+        /// </summary>
+        /// <typeparam name="T">Type of object on which property is defined.</typeparam>
+        /// <param name="obj">Object on which property is defined.</param>
+        /// <param name="propName">Name of a property to invoke.</param>
+        /// <param name="accessibilityContext">Type that determines context in which method should be called.</param>
+        /// <param name="isResultIndexed">Determines if COM binder should return a callable object.</param>
+        /// <returns>Result of property invocation.</returns>
+        public static object TryGetMemberValue<T>(T obj, string propName, Type accessibilityContext, bool isResultIndexed)
+        {
+            // In most cases it's ok to use CSharpArgumentInfoFlags.None since target of property call is dynamic.
+            // The only possible case when target is not dynamic but we still treat is as dynamic access is when 
+            // one of arguments is dynamic. This is only possible for indexed properties since we call this method and 
+            // TryGetMemberValueVarArgs afterwards. 
+             
+            CSharpGetMemberBinder binder = new CSharpGetMemberBinder(
+                propName,
+                isResultIndexed,
+                accessibilityContext,
+                new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) });
+
+            var site = CallSite<Func<CallSite, T, object>>.Create(binder);
+            return site.Target(site, obj);
+        }
+
+        /// <summary>
+        /// DynamicOperatorRewriter in EE generates call to this method to dynamically invoke a property/indexer getter.
+        /// </summary>
+        /// <param name="propArgs">Array that contains property arguments. The first element is an object on 
+        /// which indexer should be called or call to TryGetMemberValue that selects the right property in case of
+        /// indexed properties.</param>
+        /// <param name="argTypes">Type of each argument in propArgs.</param>
+        /// <param name="argFlags">Flags describing each argument.</param>
+        /// <param name="accessibilityContext">Type that determines context in which method should be called.</param>
+        /// <returns>Result of property invocation.</returns>
+        public static object TryGetMemberValueVarArgs(
+            object[] propArgs,
+            Type[] argTypes,
+            CSharpArgumentInfoFlags[] argFlags,
+            Type accessibilityContext)
+        {
+            Type[] delegateSignatureTypes = null;
+            CSharpArgumentInfo[] argInfos = null;
+
+            CreateDelegateSignatureAndArgumentInfos(
+                propArgs,
+                argTypes,
+                argFlags,
+                out delegateSignatureTypes,
+                out argInfos);
+
+            CallSiteBinder binder = new CSharpGetIndexBinder(accessibilityContext, argInfos);
+
+            return CreateDelegateAndInvoke(delegateSignatureTypes, binder, propArgs);
+        }
+
+        /// <summary>
+        /// DynamicOperatorRewriter in EE generates call to this method to dynamically invoke a property setter
+        /// with no arguments.
+        /// </summary>
+        /// <typeparam name="TObject">Type of object on which property is defined.</typeparam>
+        /// <typeparam name="TValue">Type of value property needs to be set to.</typeparam>
+        /// <param name="obj">Object on which property is defined.</param>
+        /// <param name="propName">Name of a property to invoke.</param>
+        /// <param name="value">Value property needs to be set to.</param>
+        /// <param name="valueFlags"></param>
+        /// <param name="accessibilityContext">Type that determines context in which method should be called.</param>
+        /// <returns>Result of property invocation.</returns>
+        public static object TrySetMemberValue<TObject, TValue>(
+            TObject obj, 
+            string propName, 
+            TValue value, 
+            CSharpArgumentInfoFlags valueFlags, 
+            Type accessibilityContext)
+        {
+            CSharpArgumentInfo targetArgInfo = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null);
+            CSharpArgumentInfo valueArgInfo = CSharpArgumentInfo.Create(valueFlags, null);
+
+            CSharpSetMemberBinder binder = new CSharpSetMemberBinder(
+                propName,
+                false, // isCompoundAssignment 
+                false, // isChecked 
+                accessibilityContext,
+                new CSharpArgumentInfo[] { targetArgInfo, valueArgInfo });
+
+            var site = CallSite<Func<CallSite, TObject, TValue, object>>.Create(binder);
+            return site.Target(site, obj, value);
+        }
+
+        /// <summary>
+        /// DynamicOperatorRewriter in EE generates call to this method to dynamically invoke a property/indexer setter.
+        /// </summary>
+        /// <param name="propArgs">Array that contains property arguments. The first element is an object on 
+        /// which indexer should be called or call to TrySetMemberValue that selects the right property in case of
+        /// indexed properties. The last argument is value that property should be set to.</param>
+        /// <param name="argTypes">Type of each argument in propArgs.</param>
+        /// <param name="argFlags">Flags describing each argument.</param>
+        /// <param name="accessibilityContext">Type that determines context in which method should be called.</param>
+        /// <returns>Result of property invocation.</returns>
+        public static object TrySetMemberValueVarArgs(
+            object[] propArgs,
+            Type[] argTypes,
+            CSharpArgumentInfoFlags[] argFlags,
+            Type accessibilityContext)
+        {
+            Type[] delegateSignatureTypes = null;
+            CSharpArgumentInfo[] argInfos = null;
+
+            CreateDelegateSignatureAndArgumentInfos(
+                propArgs,
+                argTypes,
+                argFlags,
+                out delegateSignatureTypes,
+                out argInfos);
+
+            CallSiteBinder binder = new CSharpSetIndexBinder(/*isCompoundAssignment */ false, /* isChecked */ false, accessibilityContext, argInfos);
+
+            return CreateDelegateAndInvoke(delegateSignatureTypes, binder, propArgs);
+        }
+
+        //Called when we don't know if the member is a property or a method
+        internal static object TryGetMemberValue(object obj, string name, bool ignoreException)
+        {
+            // if you want to ignore case for VB, this is how you set it .. make it a member and add a ctor to init it
+            bool ignoreCase = false;
+            object value = null;
+
+            var site = CallSite<Func<CallSite, object, object>>.Create(new GetMemberValueBinder(name, ignoreCase));
+
+            try
+            {
+                value = site.Target(site, obj);
+            }
+            catch (DynamicBindingFailedException exp)
+            {
+                if (ignoreException)
+                    value = null;
+                else
+                    throw exp;
+            }
+            catch (MissingMemberException exp)
+            {
+                if (ignoreException)
+                    value = SR.GetValueonWriteOnlyProperty;
+                else
+                    throw exp;
+            }
+            return value;
+        }
+
+        private static IList<KeyValuePair<string, object>> QueryDynamicObject(object obj)
+        {
+            IDynamicMetaObjectProvider ido = obj as IDynamicMetaObjectProvider;
+            if (ido != null)
+            {
+                DynamicMetaObject mo = ido.GetMetaObject(parameter);
+                List<string> names = new List<string>(mo.GetDynamicMemberNames());
+                names.Sort();
+                if (names != null)
+                {
+                    var result = new List<KeyValuePair<string, object>>();
+                    foreach (string name in names)
+                    {
+                        object value;
+                        if ((value = TryGetMemberValue(obj, name, true)) != null)
+                        {
+                            result.Add(new KeyValuePair<string, object>(name, value));
+                        }
+                    }
+                    return result;
+                }
+            }
+            return new KeyValuePair<string, object>[0];
+        }
+
+        [Serializable]
+        internal class DynamicDebugViewEmptyException : Exception
+        {
+            public DynamicDebugViewEmptyException()
+            {
+            }
+
+            protected DynamicDebugViewEmptyException(SerializationInfo info, StreamingContext context)
+                : base(info, context)
+            {
+            }
+
+            public string Empty
+            {
+                get
+                {
+                    return SR.EmptyDynamicView;
+                }
+            }
+        }
+    }
+}
index 851a849..ecde81c 100644 (file)
   <data name="BindToVoidMethodButExpectResult" xml:space="preserve">
     <value>Cannot implicitly convert type 'void' to 'object'</value>
   </data>
+  <data name="EmptyDynamicView" xml:space="preserve">
+    <value>No further information on this object could be discovered</value>
+  </data>
+  <data name="GetValueonWriteOnlyProperty" xml:space="preserve">
+    <value>Write Only properties are not supported</value>
+  </data>
   <data name="BadBinaryOps" xml:space="preserve">
     <value>Operator '{0}' cannot be applied to operands of type '{1}' and '{2}'</value>
   </data>
   <data name="SK_VARIABLE" xml:space="preserve">
     <value>variable</value>
   </data>
-</root>
\ No newline at end of file
+</root>
diff --git a/src/libraries/Microsoft.CSharp/tests/DynamicDebuggerProxyTests.cs b/src/libraries/Microsoft.CSharp/tests/DynamicDebuggerProxyTests.cs
new file mode 100644 (file)
index 0000000..94359a9
--- /dev/null
@@ -0,0 +1,137 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Reflection;
+using Xunit;
+
+namespace Microsoft.CSharp.RuntimeBinder.Tests
+{
+    public class DynamicDebuggerProxyTests
+    {
+        private static readonly Type _debugViewType = GetType("DynamicMetaObjectProviderDebugView");
+
+        [Fact]
+        public void Items_Empty()
+        {
+            var obj = new DynamicTestObject(new Dictionary<string, object>());
+            var debugView = CreateDebugView(obj);
+
+            var exceptionType = GetType("DynamicMetaObjectProviderDebugView+DynamicDebugViewEmptyException");
+            var exception = Assert.Throws(exceptionType, () => GetItems(debugView));
+
+            // Get resource string.
+            var itemValue = (string)exceptionType.GetMethod("get_Empty", BindingFlags.Public | BindingFlags.Instance).Invoke(exception, new object[0]);
+            Assert.NotNull(itemValue);
+        }
+
+        [Fact]
+        public void Items()
+        {
+            var obj = new DynamicTestObject(new Dictionary<string, object>() { { "F", 1 }, { "G", "" } });
+            var debugView = CreateDebugView(obj);
+            var items = GetItems(debugView);
+            Assert.Equal(2, items.Length);
+        }
+
+        [Fact]
+        public void TryGetMemberValue()
+        {
+            var obj = new DynamicTestObject(new Dictionary<string, object>() { { "F", 1 } });
+            Assert.Equal(1, TryGetMemberValue(obj, "F", ignoreException: false));
+        }
+
+        [Fact]
+        public void TryGetMemberValue_BindingFailed()
+        {
+            var obj = new DynamicTestObject(new Dictionary<string, object>());
+
+            // ignoreException: true
+            Assert.Null(TryGetMemberValue(obj, "F", ignoreException: true));
+
+            // ignoreException: false
+            var exceptionType = GetType("DynamicBindingFailedException");
+            Assert.Throws(exceptionType, () => GetItems(TryGetMemberValue(obj, "F", ignoreException: false)));
+        }
+
+        [Fact]
+        public void TryGetMemberValue_MissingMember()
+        {
+            var obj = new DynamicTestObject(new Dictionary<string, object>(), throwIfMissing: true);
+
+            // ignoreException: true
+            string message = (string)TryGetMemberValue(obj, "F", ignoreException: true);
+            Assert.NotNull(message);
+
+            // ignoreException: false
+            Assert.Throws<MissingMemberException>(() => GetItems(TryGetMemberValue(obj, "F", ignoreException: false)));
+        }
+
+        private static Type GetType(string typeName)
+        {
+            return typeof(Binder).Assembly.GetType("Microsoft.CSharp.RuntimeBinder." + typeName, throwOnError: true);
+        }
+
+        private static object CreateDebugView(object arg)
+        {
+            return Activator.CreateInstance(_debugViewType, new object[] { arg });
+        }
+
+        private static object[] GetItems(object debugView)
+        {
+            var method = _debugViewType.GetMethod("get_Items", BindingFlags.NonPublic | BindingFlags.Instance);
+            return (object[])InvokeAndUnwrapException(method, debugView, new object[0]);
+        }
+
+        private static object TryGetMemberValue(object obj, string name, bool ignoreException)
+        {
+            var method = _debugViewType.GetMethod("TryGetMemberValue", BindingFlags.NonPublic | BindingFlags.Static);
+            return InvokeAndUnwrapException(method, null, new object[] { obj, name, ignoreException });
+        }
+
+        private static object InvokeAndUnwrapException(MethodInfo method, object obj, object[] args)
+        {
+            try
+            {
+                return method.Invoke(obj, args);
+            }
+            catch (TargetInvocationException e)
+            {
+                throw e.InnerException;
+            }
+        }
+
+        private sealed class DynamicTestObject : DynamicObject
+        {
+            private readonly Dictionary<string, object> _members;
+            private readonly bool _throwIfMissing;
+
+            internal DynamicTestObject(Dictionary<string, object> members, bool throwIfMissing = false)
+            {
+                _members = members;
+                _throwIfMissing = throwIfMissing;
+            }
+
+            public override IEnumerable<string> GetDynamicMemberNames()
+            {
+                return _members.Keys;
+            }
+
+            public override bool TryGetMember(GetMemberBinder binder, out object result)
+            {
+                if (_members.TryGetValue(binder.Name, out result))
+                {
+                    return true;
+                }
+                if (_throwIfMissing)
+                {
+                    throw new MissingMemberException();
+                }
+                return false;
+            }
+        }
+    }
+}
index 48b12b6..43e5d2f 100644 (file)
@@ -14,6 +14,7 @@
     <Compile Include="ArrayHandling.cs" />
     <Compile Include="AssignmentTests.cs" />
     <Compile Include="BindingErrors.cs" />
+    <Compile Include="DynamicDebuggerProxyTests.cs" />
     <Compile Include="EnumUnaryOperationTests.cs" />
     <Compile Include="ExplicitConversionTests.cs" />
     <Compile Include="Helpers.cs" />