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 f0dea3464d
PiperOrigin-RevId: 568444117
pull/14167/head
James Newton-King 1 year ago committed by Copybara-Service
parent 34bbf3162d
commit 67b9c76752
  1. 16
      csharp/src/Google.Protobuf/ByteString.cs
  2. 16
      csharp/src/Google.Protobuf/Collections/MapField.cs
  3. 17
      csharp/src/Google.Protobuf/Collections/RepeatedField.cs
  4. 19
      csharp/src/Google.Protobuf/Reflection/CustomOptions.cs
  5. 2
      csharp/src/Google.Protobuf/Reflection/DescriptorBase.cs
  6. 16
      csharp/src/Google.Protobuf/Reflection/ExtensionCollection.cs
  7. 2
      csharp/src/Google.Protobuf/Reflection/GeneratedClrTypeInfo.cs
  8. 16
      csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs
  9. 16
      csharp/src/Google.Protobuf/Reflection/TypeRegistry.cs
  10. 19
      csharp/src/Google.Protobuf/UnknownFieldSet.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 @@ namespace Google.Protobuf
/// 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 @@ namespace Google.Protobuf
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();
}
}
}

@ -11,6 +11,7 @@ using Google.Protobuf.Compatibility;
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 @@ namespace Google.Protobuf.Collections
/// 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 @@ namespace Google.Protobuf.Collections
}
}
}
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();
}
}
}

@ -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 @@ namespace Google.Protobuf.Collections
/// 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 @@ namespace Google.Protobuf.Collections
}
}
#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();
}
}
}

@ -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 @@ namespace Google.Protobuf.Reflection
/// 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 @@ namespace Google.Protobuf.Reflection
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];
}
}
}

@ -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)

@ -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 @@ namespace Google.Protobuf.Reflection
/// <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 @@ namespace Google.Protobuf.Reflection
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();
}
}
}

@ -8,6 +8,7 @@
#endregion
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace Google.Protobuf.Reflection
@ -17,6 +18,7 @@ namespace Google.Protobuf.Reflection
/// 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];

@ -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 @@ namespace Google.Protobuf.Reflection
/// <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 @@ namespace Google.Protobuf.Reflection
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();
}
}
}
}

@ -8,6 +8,7 @@
#endregion
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Google.Protobuf.Reflection
@ -15,6 +16,8 @@ namespace Google.Protobuf.Reflection
/// <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 @@ namespace Google.Protobuf.Reflection
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();
}
}
}

@ -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 @@ namespace Google.Protobuf
///
/// 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 @@ namespace Google.Protobuf
}
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 @@ namespace Google.Protobuf
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();
}
}
}

Loading…
Cancel
Save