Add .NET debugging attributes (#14097)

I've been working with a large Protobuf model and noticed debugging issues that can easily be improved.

Add debugging attributes to collections, `ByteString`, and descriptors. No impact on runtime behavior. Debugger type proxies are what the debugger displays by default, but full data is available by selecting "Raw View".

`RepeatedField` and `MapField` now display their item count. This is standard across .NET collections. For example, .NET's list and dictionary both display `Count = {Count}`.

Note that previously, Protobuf collections displayed the result of `ToString`, which returned JSON. The JSON debug view isn't useful when there is too much content to display at once because it's truncated. That experience will be fairly common.

Count benefits:
* Always useful
* Makes debugging Protobuf collections feel more like regular .NET collections
* Collection contents is now easier to access with debugger type proxies

No dependency between this PR and https://github.com/protocolbuffers/protobuf/pull/13838. Each can be merged independently.

**MapField before:**

![image](https://github.com/protocolbuffers/protobuf/assets/303201/9dd3baa9-4432-446e-9049-1f7268d5be4c)

**MapField after:**

![image](https://github.com/protocolbuffers/protobuf/assets/303201/ac4aea33-e339-49e0-9a67-c174d2608393)

**RepeatedField before:**

![image](https://github.com/protocolbuffers/protobuf/assets/303201/16353785-bef4-4489-a3ab-de2437d51d4e)

**RepeatedField after:**

![image](https://github.com/protocolbuffers/protobuf/assets/303201/f3de7680-ded9-41d8-aac0-84a9e7b65c98)

**ByteString before:**

![image](https://github.com/protocolbuffers/protobuf/assets/303201/4febc400-1eb7-46ee-911e-a7698783a358)

**ByteString after:**

![image](https://github.com/protocolbuffers/protobuf/assets/303201/7635080e-9bb0-4f61-9a39-afbb9e575051)

**Descriptor before:**

![image](https://github.com/protocolbuffers/protobuf/assets/303201/5b89792b-a16a-4641-a50c-5355b5230b5d)

**Descriptor after:**

![image](https://github.com/protocolbuffers/protobuf/assets/303201/5047dc1e-c93b-43d5-bb8c-a6976a9ae6da)

**TypeRegistry before:**

![image](https://github.com/protocolbuffers/protobuf/assets/303201/d4aedf19-22cc-49b5-8717-9299e00abc85)

**TypeRegistry after:**

![image](https://github.com/protocolbuffers/protobuf/assets/303201/4a473595-74b9-40e2-96b8-2b103d5f44b0)

Closes #14097

COPYBARA_INTEGRATE_REVIEW=https://github.com/protocolbuffers/protobuf/pull/14097 from JamesNK:jamesnk/debugging f0dea3464d471042a59e9c0000a5e65e8c9bc46b
PiperOrigin-RevId: 568444117
diff --git a/csharp/src/Google.Protobuf/ByteString.cs b/csharp/src/Google.Protobuf/ByteString.cs
index e361dd1..fc5d925 100644
--- a/csharp/src/Google.Protobuf/ByteString.cs
+++ b/csharp/src/Google.Protobuf/ByteString.cs
@@ -10,6 +10,7 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Runtime.InteropServices;
 using System.Security;
@@ -23,6 +24,8 @@
     /// Immutable array of bytes.
     /// </summary>
     [SecuritySafeCritical]
+    [DebuggerDisplay("Length = {Length}")]
+    [DebuggerTypeProxy(typeof(ByteStringDebugView))]
     public sealed class ByteString : IEnumerable<byte>, IEquatable<ByteString>
     {
         private static readonly ByteString empty = new ByteString(new byte[0]);
@@ -400,5 +403,18 @@
                 outputStream.Write(array, 0, array.Length);
             }
         }
+
+        private sealed class ByteStringDebugView
+        {
+            private readonly ByteString data;
+
+            public ByteStringDebugView(ByteString data)
+            {
+                this.data = data;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public byte[] Items => data.bytes.ToArray();
+        }
     }
 }
\ No newline at end of file
diff --git a/csharp/src/Google.Protobuf/Collections/MapField.cs b/csharp/src/Google.Protobuf/Collections/MapField.cs
index 2e18976..f0be958 100644
--- a/csharp/src/Google.Protobuf/Collections/MapField.cs
+++ b/csharp/src/Google.Protobuf/Collections/MapField.cs
@@ -11,6 +11,7 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Security;
@@ -43,6 +44,8 @@
     /// in future versions.
     /// </para>
     /// </remarks>
+    [DebuggerDisplay("Count = {Count}")]
+    [DebuggerTypeProxy(typeof(MapField<,>.MapFieldDebugView))]
     public sealed class MapField<TKey, TValue> : IDeepCloneable<MapField<TKey, TValue>>, IDictionary<TKey, TValue>, IEquatable<MapField<TKey, TValue>>, IDictionary, IReadOnlyDictionary<TKey, TValue>
     {
         private static readonly EqualityComparer<TValue> ValueEqualityComparer = ProtobufEqualityComparers.GetEqualityComparer<TValue>();
@@ -683,5 +686,18 @@
                 }
             }
         }
+
+        private sealed class MapFieldDebugView
+        {
+            private readonly MapField<TKey, TValue> map;
+
+            public MapFieldDebugView(MapField<TKey, TValue> map)
+            {
+                this.map = map;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public KeyValuePair<TKey, TValue>[] Items => map.list.ToArray();
+        }
     }
 }
diff --git a/csharp/src/Google.Protobuf/Collections/RepeatedField.cs b/csharp/src/Google.Protobuf/Collections/RepeatedField.cs
index 38e7015..8bf410a 100644
--- a/csharp/src/Google.Protobuf/Collections/RepeatedField.cs
+++ b/csharp/src/Google.Protobuf/Collections/RepeatedField.cs
@@ -10,7 +10,9 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
+using System.Linq;
 using System.Security;
 
 namespace Google.Protobuf.Collections
@@ -24,6 +26,8 @@
     /// supported by Protocol Buffers but nor does it guarantee that all operations will work in such cases.
     /// </remarks>
     /// <typeparam name="T">The element type of the repeated field.</typeparam>
+    [DebuggerDisplay("Count = {Count}")]
+    [DebuggerTypeProxy(typeof(RepeatedField<>.RepeatedFieldDebugView))]
     public sealed class RepeatedField<T> : IList<T>, IList, IDeepCloneable<RepeatedField<T>>, IEquatable<RepeatedField<T>>, IReadOnlyList<T>
     {
         private static readonly EqualityComparer<T> EqualityComparer = ProtobufEqualityComparers.GetEqualityComparer<T>();
@@ -642,5 +646,18 @@
             }
         }
         #endregion        
+
+        private sealed class RepeatedFieldDebugView
+        {
+            private readonly RepeatedField<T> list;
+
+            public RepeatedFieldDebugView(RepeatedField<T> list)
+            {
+                this.list = list;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public T[] Items => list.ToArray();
+        }
     }
 }
diff --git a/csharp/src/Google.Protobuf/Reflection/CustomOptions.cs b/csharp/src/Google.Protobuf/Reflection/CustomOptions.cs
index 21ec57a..43b5a4c 100644
--- a/csharp/src/Google.Protobuf/Reflection/CustomOptions.cs
+++ b/csharp/src/Google.Protobuf/Reflection/CustomOptions.cs
@@ -8,8 +8,10 @@
 #endregion
 
 using Google.Protobuf.Collections;
+using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Reflection;
@@ -38,6 +40,8 @@
     /// all the set values are merged together.
     /// </para>
     /// </remarks>
+    [DebuggerDisplay("Count = {DebugCount}")]
+    [DebuggerTypeProxy(typeof(CustomOptionsDebugView))]
     public sealed class CustomOptions
     {
         private const string UnreferencedCodeMessage = "CustomOptions is incompatible with trimming.";
@@ -290,5 +294,20 @@
             value = default;
             return false;
         }
+
+        private int DebugCount => values?.Count ?? 0;
+
+        private sealed class CustomOptionsDebugView
+        {
+            private readonly CustomOptions customOptions;
+
+            public CustomOptionsDebugView(CustomOptions customOptions)
+            {
+                this.customOptions = customOptions;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public KeyValuePair<int, IExtensionValue>[] Items => customOptions.values?.ToArray() ?? new KeyValuePair<int, IExtensionValue>[0];
+        }
     }
 }
diff --git a/csharp/src/Google.Protobuf/Reflection/DescriptorBase.cs b/csharp/src/Google.Protobuf/Reflection/DescriptorBase.cs
index 34a73d6..e70b23a 100644
--- a/csharp/src/Google.Protobuf/Reflection/DescriptorBase.cs
+++ b/csharp/src/Google.Protobuf/Reflection/DescriptorBase.cs
@@ -8,12 +8,14 @@
 #endregion
 
 using System.Collections.Generic;
+using System.Diagnostics;
 
 namespace Google.Protobuf.Reflection
 {
     /// <summary>
     /// Base class for nearly all descriptors, providing common functionality.
     /// </summary>
+    [DebuggerDisplay("Type = {GetType().Name,nq}, FullName = {FullName}")]
     public abstract class DescriptorBase : IDescriptor
     {
         internal DescriptorBase(FileDescriptor file, string fullName, int index)
diff --git a/csharp/src/Google.Protobuf/Reflection/ExtensionCollection.cs b/csharp/src/Google.Protobuf/Reflection/ExtensionCollection.cs
index 9fcad89..5ce2cfe 100644
--- a/csharp/src/Google.Protobuf/Reflection/ExtensionCollection.cs
+++ b/csharp/src/Google.Protobuf/Reflection/ExtensionCollection.cs
@@ -9,6 +9,7 @@
 
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.Linq;
 
 namespace Google.Protobuf.Reflection
@@ -16,6 +17,8 @@
     /// <summary>
     /// A collection to simplify retrieving the descriptors of extensions in a descriptor for a message
     /// </summary>
+    [DebuggerDisplay("Count = {UnorderedExtensions.Count}")]
+    [DebuggerTypeProxy(typeof(ExtensionCollectionDebugView))]
     public sealed class ExtensionCollection
     {
         private IDictionary<MessageDescriptor, IList<FieldDescriptor>> extensionsByTypeInDeclarationOrder;
@@ -98,5 +101,18 @@
             extensionsByTypeInNumberOrder = declarationOrder
                 .ToDictionary(kvp => kvp.Key, kvp => (IList<FieldDescriptor>)new ReadOnlyCollection<FieldDescriptor>(kvp.Value.OrderBy(field => field.FieldNumber).ToArray()));
         }
+
+        private sealed class ExtensionCollectionDebugView
+        {
+            private readonly ExtensionCollection list;
+
+            public ExtensionCollectionDebugView(ExtensionCollection list)
+            {
+                this.list = list;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public FieldDescriptor[] Items => list.UnorderedExtensions.ToArray();
+        }
     }
 }
diff --git a/csharp/src/Google.Protobuf/Reflection/GeneratedClrTypeInfo.cs b/csharp/src/Google.Protobuf/Reflection/GeneratedClrTypeInfo.cs
index f5ef62c..a4748c5 100644
--- a/csharp/src/Google.Protobuf/Reflection/GeneratedClrTypeInfo.cs
+++ b/csharp/src/Google.Protobuf/Reflection/GeneratedClrTypeInfo.cs
@@ -8,6 +8,7 @@
 #endregion
 
 using System;
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 
 namespace Google.Protobuf.Reflection
@@ -17,6 +18,7 @@
     /// These are constructed as required, and are not long-lived. Hand-written code should
     /// never need to use this type.
     /// </summary>
+    [DebuggerDisplay("ClrType = {ClrType}")]
     public sealed class GeneratedClrTypeInfo
     {
         private static readonly string[] EmptyNames = new string[0];
diff --git a/csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs b/csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs
index dfa63e4..37ca0f1 100644
--- a/csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs
+++ b/csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs
@@ -10,6 +10,7 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Reflection;
@@ -326,6 +327,8 @@
         /// <summary>
         /// A collection to simplify retrieving the field accessor for a particular field.
         /// </summary>
+        [DebuggerDisplay("Count = {InFieldNumberOrder().Count}")]
+        [DebuggerTypeProxy(typeof(FieldCollectionDebugView))]
         public sealed class FieldCollection
         {
             private readonly MessageDescriptor messageDescriptor;
@@ -398,6 +401,19 @@
                     return fieldDescriptor;
                 }
             }
+
+            private sealed class FieldCollectionDebugView
+            {
+                private readonly FieldCollection collection;
+
+                public FieldCollectionDebugView(FieldCollection collection)
+                {
+                    this.collection = collection;
+                }
+
+                [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+                public FieldDescriptor[] Items => collection.InFieldNumberOrder().ToArray();
+            }
         }
     }
 }
diff --git a/csharp/src/Google.Protobuf/Reflection/TypeRegistry.cs b/csharp/src/Google.Protobuf/Reflection/TypeRegistry.cs
index b1bf30b..f0ab81c 100644
--- a/csharp/src/Google.Protobuf/Reflection/TypeRegistry.cs
+++ b/csharp/src/Google.Protobuf/Reflection/TypeRegistry.cs
@@ -8,6 +8,7 @@
 #endregion
 
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 
 namespace Google.Protobuf.Reflection
@@ -15,6 +16,8 @@
     /// <summary>
     /// An immutable registry of types which can be looked up by their full name.
     /// </summary>
+    [DebuggerDisplay("Count = {fullNameToMessageMap.Count}")]
+    [DebuggerTypeProxy(typeof(TypeRegistryDebugView))]
     public sealed class TypeRegistry
     {
         /// <summary>
@@ -156,5 +159,18 @@
                 return new TypeRegistry(types);
             }
         }
+
+        private sealed class TypeRegistryDebugView
+        {
+            private readonly TypeRegistry list;
+
+            public TypeRegistryDebugView(TypeRegistry list)
+            {
+                this.list = list;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public KeyValuePair<string, MessageDescriptor>[] Items => list.fullNameToMessageMap.ToArray();
+        }
     }
 }
diff --git a/csharp/src/Google.Protobuf/UnknownFieldSet.cs b/csharp/src/Google.Protobuf/UnknownFieldSet.cs
index 4fab615..d0963d2 100644
--- a/csharp/src/Google.Protobuf/UnknownFieldSet.cs
+++ b/csharp/src/Google.Protobuf/UnknownFieldSet.cs
@@ -9,6 +9,8 @@
 
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
 using System.Security;
 
 namespace Google.Protobuf
@@ -22,6 +24,8 @@
     ///
     /// Most users will never need to use this class directly.
     /// </summary>
+    [DebuggerDisplay("Count = {fields.Count}")]
+    [DebuggerTypeProxy(typeof(UnknownFieldSetDebugView))]
     public sealed partial class UnknownFieldSet
     {
         private readonly IDictionary<int, UnknownField> fields = new Dictionary<int, UnknownField>();
@@ -93,7 +97,7 @@
             }
             UnknownFieldSet otherSet = other as UnknownFieldSet;
             IDictionary<int, UnknownField> otherFields = otherSet.fields;
-            if (fields.Count  != otherFields.Count)
+            if (fields.Count != otherFields.Count)
             {
                 return false;
             }
@@ -360,6 +364,19 @@
             unknownFields.MergeFrom(other);
             return unknownFields;
         }
+
+        private sealed class UnknownFieldSetDebugView
+        {
+            private readonly UnknownFieldSet set;
+
+            public UnknownFieldSetDebugView(UnknownFieldSet set)
+            {
+                this.set = set;
+            }
+
+            [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+            public KeyValuePair<int, UnknownField>[] Items => set.fields.ToArray();
+        }
     }
 }