You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by co...@apache.org on 2026/06/25 19:47:30 UTC
(tinkerpop) 13/17: Add PrimitivePDT support to gremlin-dotnet GLV
This is an automated email from the ASF dual-hosted git repository.
Cole-Greer pushed a commit to branch simplePDT
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit fdff7b20a3e7849e93fcd1811e542455c28df1c3
Author: Cole Greer <co...@colegreer.ca>
AuthorDate: Wed Jun 24 20:59:47 2026 -0700
Add PrimitivePDT support to gremlin-dotnet GLV
Implements PrimitivePDT in the .NET GLV, mirroring composite support and
applying the review lessons from the Python GLV.
- PrimitiveProviderDefinedType (Name, Value) + IPrimitivePdtAdapter<T>
(TypeName/FromString/ToString) in Structure/.
- DataType.PrimitivePDT (0xF1) enabled; PrimitivePDTSerializer writes/reads
two fully-qualified Strings; registered in TypeSerializerRegistry.
- ProviderDefinedTypeRegistry gains an explicit primitive adapter path
(register + GetPrimitiveAdapterByType + HydratePrimitive), mirroring the
composite/primitive naming split used across the other GLVs.
- GraphBinaryReader hydrates PrimitiveProviderDefinedType via the registry.
- GremlinLang text translation emits PDT("name","value") for a
PrimitiveProviderDefinedType and auto-dehydrates registered types — the
client-side text path that was the Python gap.
- ADAPTER-OVER-ATTRIBUTE precedence: a registered adapter takes priority over
the [ProviderDefined] attribute on dehydration (matching the Java/Python fix
in ef194e358f), applied in the text path.
- Client wiring reuses the existing SetPdtRegistry / GremlinClient /
DriverRemoteConnection path.
No GraphSON g:PrimitivePdt read path added (consistent with the .NET driver's
GraphBinary-based V4 response handling).
Tests: 57 unit tests pass (serializer round-trip incl. opaque-value fidelity,
registry hydration, gremlin-lang text emission, adapter-over-attribute
precedence). Integration tests (raw, opaque value, in-collection,
nested-in-composite, registered) pass against the test server: 6/6.
tinkerpop-2gy.11
Assisted-by: Kiro:claude-opus-4.8
---
.../Gremlin.Net/Process/Traversal/GremlinLang.cs | 15 +-
.../Structure/IO/GraphBinary4/DataType.cs | 3 +-
.../Structure/IO/GraphBinary4/GraphBinaryReader.cs | 12 +-
.../IO/GraphBinary4/TypeSerializerRegistry.cs | 2 +
.../GraphBinary4/Types/PrimitivePDTSerializer.cs | 66 +++++++
.../Gremlin.Net/Structure/IPrimitivePdtAdapter.cs | 47 +++++
.../Structure/PrimitiveProviderDefinedType.cs | 65 +++++++
.../Structure/ProviderDefinedTypeRegistry.cs | 138 +++++++++++---
.../Driver/DriverRemoteConnectionTests.cs | 51 +++++-
.../Driver/GremlinClientTests.cs | 72 ++++++++
.../Process/Traversal/GremlinLangTests.cs | 73 ++++++++
.../PrimitiveProviderDefinedTypeTests.cs | 204 +++++++++++++++++++++
.../Structure/PrimitivePdtRegistryTests.cs | 126 +++++++++++++
13 files changed, 844 insertions(+), 30 deletions(-)
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
index 684a76c2f6..ce9bb1e1d5 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/GremlinLang.cs
@@ -356,6 +356,11 @@ namespace Gremlin.Net.Process.Traversal
sb2.Append(']');
return $"PDT(\"{EscapeJava(pdt.Name)}\",{sb2})";
}
+
+ if (arg is PrimitiveProviderDefinedType primitivePdt)
+ {
+ return $"PDT(\"{EscapeJava(primitivePdt.Name)}\",\"{EscapeJava(primitivePdt.Value)}\")";
+ }
if (arg is IDictionary dict)
return AsString(dict);
@@ -377,9 +382,17 @@ namespace Gremlin.Net.Process.Traversal
// Precedence: a registered adapter intentionally takes priority over the [ProviderDefined]
// attribute so that explicit adapters can override attribute-derived dehydration behavior.
+ // Check primitive adapter first, then composite.
if (PdtRegistry != null)
{
- var adapterInfo = PdtRegistry.GetAdapterByType(arg.GetType());
+ var primitiveInfo = PdtRegistry.GetPrimitiveAdapterByType(arg.GetType());
+ if (primitiveInfo != null)
+ {
+ var (adapterTypeName, toStr) = primitiveInfo.Value;
+ return ArgAsString(new PrimitiveProviderDefinedType(adapterTypeName, toStr(arg)));
+ }
+
+ var adapterInfo = PdtRegistry.GetCompositeAdapterByType(arg.GetType());
if (adapterInfo != null)
{
var (adapterTypeName, toFields) = adapterInfo.Value;
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs
index a8844bbb47..bc48d9edf4 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs
@@ -59,8 +59,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
// public static readonly DataType Tree = new DataType(0x2B);
public static readonly DataType Merge = new DataType(0x2E);
public static readonly DataType CompositePDT = new DataType(0xF0);
- // Not yet implemented
- // public static readonly DataType PrimitivePDT = new DataType(0xF1);
+ public static readonly DataType PrimitivePDT = new DataType(0xF1);
public static readonly DataType Char = new DataType(0x80);
public static readonly DataType Duration = new DataType(0x81);
public static readonly DataType Marker = new DataType(0xFD);
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs
index 97f0dd76fe..76d4b40999 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs
@@ -99,12 +99,22 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
{
if (_pdtRegistry != null)
{
- var hydrated = _pdtRegistry.Hydrate(pdt);
+ var hydrated = _pdtRegistry.HydrateComposite(pdt);
if (hydrated is not ProviderDefinedType)
return hydrated;
}
return ProviderDefinedAttribute.HydrateIfRegistered(pdt);
}
+ if (result is PrimitiveProviderDefinedType primitivePdt)
+ {
+ if (_pdtRegistry != null)
+ {
+ var hydrated = _pdtRegistry.HydratePrimitive(primitivePdt);
+ if (hydrated is not PrimitiveProviderDefinedType)
+ return hydrated;
+ }
+ return primitivePdt;
+ }
return result;
}
}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs
index 8c48079668..e0f02fea61 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs
@@ -66,6 +66,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
{typeof(TimeSpan), new DurationSerializer()},
{typeof(Marker), SingleTypeSerializers.MarkerSerializer},
{typeof(ProviderDefinedType), new CompositePDTSerializer()},
+ {typeof(PrimitiveProviderDefinedType), new PrimitivePDTSerializer()},
};
private readonly Dictionary<DataType, ITypeSerializer> _serializerByDataType =
@@ -100,6 +101,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary4
{DataType.Duration, new DurationSerializer()},
{DataType.Marker, SingleTypeSerializers.MarkerSerializer},
{DataType.CompositePDT, new CompositePDTSerializer()},
+ {DataType.PrimitivePDT, new PrimitivePDTSerializer()},
};
/// <summary>
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PrimitivePDTSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PrimitivePDTSerializer.cs
new file mode 100644
index 0000000000..9d84f8c191
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PrimitivePDTSerializer.cs
@@ -0,0 +1,66 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#endregion
+
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ /// <summary>
+ /// A <see cref="PrimitiveProviderDefinedType"/> serializer for the PrimitivePDT data type.
+ /// Wire format: two fully-qualified Strings {name}{value}.
+ /// </summary>
+ public class PrimitivePDTSerializer : SimpleTypeSerializer<PrimitiveProviderDefinedType>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PrimitivePDTSerializer"/> class.
+ /// </summary>
+ public PrimitivePDTSerializer() : base(DataType.PrimitivePDT)
+ {
+ }
+
+ /// <inheritdoc />
+ protected override async Task WriteValueAsync(PrimitiveProviderDefinedType value, Stream stream,
+ GraphBinaryWriter writer, CancellationToken cancellationToken = default)
+ {
+ await writer.WriteAsync(value.Name, stream, cancellationToken).ConfigureAwait(false);
+ await writer.WriteAsync(value.Value, stream, cancellationToken).ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ protected override async Task<PrimitiveProviderDefinedType> ReadValueAsync(Stream stream,
+ GraphBinaryReader reader, CancellationToken cancellationToken = default)
+ {
+ var name = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false) as string;
+ if (string.IsNullOrEmpty(name))
+ throw new IOException("PrimitivePDT name cannot be null or empty.");
+
+ var value = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false) as string;
+ if (value == null)
+ throw new IOException("PrimitivePDT value cannot be null.");
+
+ return new PrimitiveProviderDefinedType(name!, value);
+ }
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IPrimitivePdtAdapter.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IPrimitivePdtAdapter.cs
new file mode 100644
index 0000000000..78240a0e15
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IPrimitivePdtAdapter.cs
@@ -0,0 +1,47 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#endregion
+
+namespace Gremlin.Net.Structure
+{
+ /// <summary>
+ /// Adapter for hydrating a <see cref="PrimitiveProviderDefinedType"/> into a strongly-typed object.
+ /// </summary>
+ /// <typeparam name="T">The target type to hydrate into.</typeparam>
+ public interface IPrimitivePdtAdapter<T>
+ {
+ /// <summary>
+ /// Gets the fully-qualified type name this adapter handles.
+ /// </summary>
+ string TypeName { get; }
+
+ /// <summary>
+ /// Creates a typed instance from the opaque string value.
+ /// </summary>
+ T FromString(string value);
+
+ /// <summary>
+ /// Converts a typed instance to its opaque string representation.
+ /// </summary>
+ string ToString(T obj);
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/PrimitiveProviderDefinedType.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/PrimitiveProviderDefinedType.cs
new file mode 100644
index 0000000000..83f8932666
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/PrimitiveProviderDefinedType.cs
@@ -0,0 +1,65 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#endregion
+
+using System;
+
+namespace Gremlin.Net.Structure
+{
+ /// <summary>
+ /// Represents a primitive provider-defined type (PDT) with a name and an opaque string value.
+ /// </summary>
+ public class PrimitiveProviderDefinedType
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PrimitiveProviderDefinedType"/> class.
+ /// </summary>
+ /// <param name="name">The fully-qualified name of the provider-defined type.</param>
+ /// <param name="value">The opaque string value.</param>
+ public PrimitiveProviderDefinedType(string name, string value)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ if (string.IsNullOrEmpty(name)) throw new ArgumentException("name cannot be empty", nameof(name));
+ Value = value ?? throw new ArgumentNullException(nameof(value));
+ }
+
+ /// <summary>
+ /// Gets the fully-qualified name of this primitive provider-defined type.
+ /// </summary>
+ public string Name { get; }
+
+ /// <summary>
+ /// Gets the opaque string value of this primitive provider-defined type.
+ /// </summary>
+ public string Value { get; }
+
+ /// <inheritdoc />
+ public override string ToString() => $"pdt[{Name}]{{{Value}}}";
+
+ /// <inheritdoc />
+ public override bool Equals(object? obj) =>
+ obj is PrimitiveProviderDefinedType other && Name == other.Name && Value == other.Value;
+
+ /// <inheritdoc />
+ public override int GetHashCode() => HashCode.Combine(Name, Value);
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedTypeRegistry.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedTypeRegistry.cs
index 427a35ca44..46843d74b0 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedTypeRegistry.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/ProviderDefinedTypeRegistry.cs
@@ -30,27 +30,39 @@ using System.Reflection;
namespace Gremlin.Net.Structure
{
/// <summary>
- /// Registry for <see cref="IProviderDefinedTypeAdapter{T}"/> instances that hydrate
- /// <see cref="ProviderDefinedType"/> values into strongly-typed objects.
+ /// Registry for <see cref="IProviderDefinedTypeAdapter{T}"/> and <see cref="IPrimitivePdtAdapter{T}"/>
+ /// instances that hydrate provider-defined types into strongly-typed objects.
/// </summary>
public class ProviderDefinedTypeRegistry
{
- private readonly Dictionary<string, object> _adaptersByName = new();
- private readonly Dictionary<Type, (string typeName, object adapter)> _adaptersByType = new();
+ private readonly Dictionary<string, object> _compositeAdaptersByName = new();
+ private readonly Dictionary<Type, (string typeName, object adapter)> _compositeAdaptersByType = new();
+ private readonly Dictionary<string, object> _primitiveAdaptersByName = new();
+ private readonly Dictionary<Type, (string typeName, object adapter)> _primitiveAdaptersByType = new();
/// <summary>
- /// Registers an adapter for a specific provider-defined type name.
+ /// Registers a composite adapter for a specific provider-defined type name.
/// </summary>
public void Register<T>(IProviderDefinedTypeAdapter<T> adapter)
{
- _adaptersByName[adapter.TypeName] = adapter;
- _adaptersByType[typeof(T)] = (adapter.TypeName, adapter);
+ _compositeAdaptersByName[adapter.TypeName] = adapter;
+ _compositeAdaptersByType[typeof(T)] = (adapter.TypeName, adapter);
+ }
+
+ /// <summary>
+ /// Registers a primitive adapter for a specific provider-defined type name.
+ /// </summary>
+ public void RegisterPrimitive<T>(IPrimitivePdtAdapter<T> adapter)
+ {
+ _primitiveAdaptersByName[adapter.TypeName] = adapter;
+ _primitiveAdaptersByType[typeof(T)] = (adapter.TypeName, adapter);
}
/// <summary>
/// Creates a registry populated by scanning loaded assemblies for:
/// <list type="bullet">
- /// <item>Types implementing <see cref="IProviderDefinedTypeAdapter{T}"/> (adapter-based hydration)</item>
+ /// <item>Types implementing <see cref="IProviderDefinedTypeAdapter{T}"/> (composite adapter-based hydration)</item>
+ /// <item>Types implementing <see cref="IPrimitivePdtAdapter{T}"/> (primitive adapter-based hydration)</item>
/// <item>Types annotated with <see cref="ProviderDefinedAttribute"/> (annotation-based round-trip)</item>
/// </list>
/// </summary>
@@ -64,18 +76,38 @@ namespace Gremlin.Net.Structure
{
foreach (var type in assembly.GetTypes())
{
- // Register adapter implementations
- var adapterInterface = type.GetInterfaces()
+ // Register composite adapter implementations
+ var compositeInterface = type.GetInterfaces()
.FirstOrDefault(i => i.IsGenericType &&
i.GetGenericTypeDefinition() == typeof(IProviderDefinedTypeAdapter<>));
- if (adapterInterface != null && !type.IsAbstract && !type.IsInterface)
+ if (compositeInterface != null && !type.IsAbstract && !type.IsInterface)
{
try
{
var adapter = Activator.CreateInstance(type);
var registerMethod = typeof(ProviderDefinedTypeRegistry)
.GetMethod(nameof(Register))!
- .MakeGenericMethod(adapterInterface.GetGenericArguments()[0]);
+ .MakeGenericMethod(compositeInterface.GetGenericArguments()[0]);
+ registerMethod.Invoke(registry, new[] { adapter });
+ }
+ catch
+ {
+ // skip types that can't be instantiated
+ }
+ }
+
+ // Register primitive adapter implementations
+ var primitiveInterface = type.GetInterfaces()
+ .FirstOrDefault(i => i.IsGenericType &&
+ i.GetGenericTypeDefinition() == typeof(IPrimitivePdtAdapter<>));
+ if (primitiveInterface != null && !type.IsAbstract && !type.IsInterface)
+ {
+ try
+ {
+ var adapter = Activator.CreateInstance(type);
+ var registerMethod = typeof(ProviderDefinedTypeRegistry)
+ .GetMethod(nameof(RegisterPrimitive))!
+ .MakeGenericMethod(primitiveInterface.GetGenericArguments()[0]);
registerMethod.Invoke(registry, new[] { adapter });
}
catch
@@ -103,11 +135,11 @@ namespace Gremlin.Net.Structure
}
/// <summary>
- /// Returns the type name and ToFields method for the given CLR type, or null if not registered.
+ /// Returns the type name and ToFields method for the given CLR type, or null if not registered as composite.
/// </summary>
- internal (string typeName, Func<object, IReadOnlyDictionary<string, object?>>)? GetAdapterByType(Type type)
+ internal (string typeName, Func<object, IReadOnlyDictionary<string, object?>>)? GetCompositeAdapterByType(Type type)
{
- if (!_adaptersByType.TryGetValue(type, out var entry))
+ if (!_compositeAdaptersByType.TryGetValue(type, out var entry))
return null;
var method = entry.adapter.GetType().GetMethod("ToFields");
if (method == null) return null;
@@ -115,25 +147,50 @@ namespace Gremlin.Net.Structure
}
/// <summary>
- /// Hydrates a <see cref="ProviderDefinedType"/> into a typed object using a registered adapter.
+ /// Returns the type name and ToString method for the given CLR type, or null if not registered as primitive.
+ /// </summary>
+ internal (string typeName, Func<object, string>)? GetPrimitiveAdapterByType(Type type)
+ {
+ if (!_primitiveAdaptersByType.TryGetValue(type, out var entry))
+ return null;
+ var method = entry.adapter.GetType().GetMethod("ToString", new[] { type });
+ if (method == null) return null;
+ return (entry.typeName, obj => (string)method.Invoke(entry.adapter, new[] { obj })!);
+ }
+
+ /// <summary>
+ /// Returns the type name and ToFields method for the given CLR type (composite),
+ /// or type name and ToString method (primitive). Checks primitive first, then composite.
+ /// Returns null if not registered.
+ /// </summary>
+ internal (string typeName, Func<object, IReadOnlyDictionary<string, object?>>)? GetAdapterByType(Type type)
+ {
+ // Check composite adapters (backward compatibility with existing callers)
+ return GetCompositeAdapterByType(type);
+ }
+
+ /// <summary>
+ /// Hydrates a <see cref="ProviderDefinedType"/> into a typed object using a registered composite adapter.
/// Returns the original PDT if no adapter is registered or if hydration fails.
/// </summary>
- public object Hydrate(ProviderDefinedType pdt)
+ public object HydrateComposite(ProviderDefinedType pdt)
{
- if (!_adaptersByName.TryGetValue(pdt.Name, out var adapterObj))
+ if (!_compositeAdaptersByName.TryGetValue(pdt.Name, out var adapterObj))
{
// No adapter for outer — still recurse into nested PDT fields
Dictionary<string, object?>? resolved = null;
foreach (var (key, value) in pdt.Fields)
{
+ object? hydrated = value;
if (value is ProviderDefinedType nested)
+ hydrated = HydrateComposite(nested);
+ else if (value is PrimitiveProviderDefinedType nestedPrim)
+ hydrated = HydratePrimitive(nestedPrim);
+
+ if (!ReferenceEquals(hydrated, value))
{
- var hydrated = Hydrate(nested);
- if (!ReferenceEquals(hydrated, nested))
- {
- resolved ??= new Dictionary<string, object?>(pdt.Fields);
- resolved[key] = hydrated;
- }
+ resolved ??= new Dictionary<string, object?>(pdt.Fields);
+ resolved[key] = hydrated;
}
}
return resolved != null ? new ProviderDefinedType(pdt.Name, resolved) : pdt;
@@ -143,7 +200,12 @@ namespace Gremlin.Net.Structure
var hydratedFields = new Dictionary<string, object?>();
foreach (var (key, value) in pdt.Fields)
{
- hydratedFields[key] = value is ProviderDefinedType nested ? Hydrate(nested) : value;
+ if (value is ProviderDefinedType nested)
+ hydratedFields[key] = HydrateComposite(nested);
+ else if (value is PrimitiveProviderDefinedType nestedPrim)
+ hydratedFields[key] = HydratePrimitive(nestedPrim);
+ else
+ hydratedFields[key] = value;
}
var readOnlyFields = new ReadOnlyDictionary<string, object?>(hydratedFields);
@@ -155,5 +217,31 @@ namespace Gremlin.Net.Structure
return pdt;
}
}
+
+ /// <summary>
+ /// Hydrates a <see cref="PrimitiveProviderDefinedType"/> into a typed object using a registered primitive adapter.
+ /// Returns the original primitive PDT if no adapter is registered or if hydration fails.
+ /// </summary>
+ public object HydratePrimitive(PrimitiveProviderDefinedType pdt)
+ {
+ if (!_primitiveAdaptersByName.TryGetValue(pdt.Name, out var adapterObj))
+ return pdt;
+ try
+ {
+ var method = adapterObj.GetType().GetMethod("FromString");
+ return method!.Invoke(adapterObj, new object[] { pdt.Value })!;
+ }
+ catch (Exception)
+ {
+ return pdt;
+ }
+ }
+
+ /// <summary>
+ /// Hydrates a <see cref="ProviderDefinedType"/> into a typed object using a registered composite adapter.
+ /// Returns the original PDT if no adapter is registered or if hydration fails.
+ /// </summary>
+ [Obsolete("Use HydrateComposite instead.")]
+ public object Hydrate(ProviderDefinedType pdt) => HydrateComposite(pdt);
}
}
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/DriverRemoteConnectionTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/DriverRemoteConnectionTests.cs
index 3aaedb218e..e28742bef2 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/DriverRemoteConnectionTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/DriverRemoteConnectionTests.cs
@@ -192,4 +192,53 @@ public class DriverRemoteConnectionTests
}
#endregion
-}
\ No newline at end of file
+
+ [Fact]
+ public void ShouldRoundTripPrimitivePdtViaTraversalApi()
+ {
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using var gremlinClient = new GremlinClient(gremlinServer);
+ using var connection = new DriverRemoteConnection(gremlinClient, "gmodern");
+ var g = AnonymousTraversalSource.Traversal().With(connection);
+
+ var pdt = new PrimitiveProviderDefinedType("TestToken", "abc123");
+
+ var results = g.Inject<object>(pdt).ToList();
+
+ Assert.Single(results);
+ var result = Assert.IsType<PrimitiveProviderDefinedType>(results[0]);
+ Assert.Equal("TestToken", result.Name);
+ Assert.Equal("abc123", result.Value);
+ }
+
+ [Fact]
+ public void ShouldRoundTripPrimitiveTypedObjectViaRegistry()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ registry.RegisterPrimitive(new TestUint32Adapter());
+
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using var gremlinClient = new GremlinClient(gremlinServer, pdtRegistry: registry);
+ using var connection = new DriverRemoteConnection(gremlinClient, "gmodern", pdtRegistry: registry);
+ var g = AnonymousTraversalSource.Traversal().With(connection);
+
+ var val = 42u;
+
+ var results = g.Inject<object>(val).ToList();
+
+ Assert.Single(results);
+ Assert.IsType<uint>(results[0]);
+ Assert.Equal(42u, (uint)results[0]);
+ }
+
+ #region Test helpers (primitive)
+
+ private class TestUint32Adapter : IPrimitivePdtAdapter<uint>
+ {
+ public string TypeName => "TestToken";
+ public uint FromString(string value) => uint.Parse(value);
+ public string ToString(uint obj) => obj.ToString();
+ }
+
+ #endregion
+}
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs
index a5fdac7c8e..91507a517d 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/GremlinClientTests.cs
@@ -272,5 +272,77 @@ namespace Gremlin.Net.IntegrationTest.Driver
Assert.Equal(3, p2.Fields["x"]);
Assert.Equal(4, p2.Fields["y"]);
}
+
+ [Fact]
+ public async Task ShouldRoundTripSimplePrimitivePdt()
+ {
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using var gremlinClient = new GremlinClient(gremlinServer);
+
+ var response = await gremlinClient.SubmitAsync<object>(
+ "g.inject(PDT(\"Uint32\", \"42\"))");
+ var results = await response.ToListAsync();
+
+ Assert.Single(results);
+ var pdt = Assert.IsType<PrimitiveProviderDefinedType>(results[0]);
+ Assert.Equal("Uint32", pdt.Name);
+ Assert.Equal("42", pdt.Value);
+ }
+
+ [Fact]
+ public async Task ShouldRoundTripPrimitivePdtWithOpaqueValue()
+ {
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using var gremlinClient = new GremlinClient(gremlinServer);
+
+ var response = await gremlinClient.SubmitAsync<object>(
+ "g.inject(PDT(\"Token\", \"007-abc\"))");
+ var results = await response.ToListAsync();
+
+ Assert.Single(results);
+ var pdt = Assert.IsType<PrimitiveProviderDefinedType>(results[0]);
+ Assert.Equal("Token", pdt.Name);
+ Assert.Equal("007-abc", pdt.Value);
+ }
+
+ [Fact]
+ public async Task ShouldHandlePrimitivePdtInCollection()
+ {
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using var gremlinClient = new GremlinClient(gremlinServer);
+
+ var response = await gremlinClient.SubmitAsync<object>(
+ "g.inject([PDT(\"Uint32\", \"1\"), PDT(\"Uint32\", \"2\")])");
+ var results = await response.ToListAsync();
+
+ Assert.Single(results);
+ var list = Assert.IsType<List<object>>(results[0]);
+ Assert.Equal(2, list.Count);
+
+ var p1 = Assert.IsType<PrimitiveProviderDefinedType>(list[0]);
+ Assert.Equal("1", p1.Value);
+
+ var p2 = Assert.IsType<PrimitiveProviderDefinedType>(list[1]);
+ Assert.Equal("2", p2.Value);
+ }
+
+ [Fact]
+ public async Task ShouldRoundTripPrimitivePdtNestedInComposite()
+ {
+ var gremlinServer = new GremlinServer(TestHost, TestPort);
+ using var gremlinClient = new GremlinClient(gremlinServer);
+
+ var response = await gremlinClient.SubmitAsync<object>(
+ "g.inject(PDT(\"Measurement\", [\"unit\":\"kg\", \"value\":PDT(\"Uint32\", \"100\")]))");
+ var results = await response.ToListAsync();
+
+ Assert.Single(results);
+ var pdt = Assert.IsType<ProviderDefinedType>(results[0]);
+ Assert.Equal("Measurement", pdt.Name);
+ Assert.Equal("kg", pdt.Fields["unit"]);
+ var inner = Assert.IsType<PrimitiveProviderDefinedType>(pdt.Fields["value"]);
+ Assert.Equal("Uint32", inner.Name);
+ Assert.Equal("100", inner.Value);
+ }
}
}
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs
index daca1acc07..bae2258f4a 100644
--- a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Process/Traversal/GremlinLangTests.cs
@@ -1231,5 +1231,78 @@ namespace Gremlin.Net.UnitTest.Process.Traversal
{
public string Tag { get; set; } = "";
}
+
+ [Fact]
+ public void g_Inject_PrimitivePDT_basic()
+ {
+ var pdt = new PrimitiveProviderDefinedType("Uint32", "42");
+ var result = _g.Inject((object)pdt).GremlinLang.GetGremlin();
+ Assert.Equal("g.inject(PDT(\"Uint32\",\"42\"))", result);
+ }
+
+ [Fact]
+ public void g_Inject_PrimitivePDT_special_chars_in_value()
+ {
+ var pdt = new PrimitiveProviderDefinedType("Token", "hello\"world");
+ var result = _g.Inject((object)pdt).GremlinLang.GetGremlin();
+ Assert.Equal("g.inject(PDT(\"Token\",\"hello\\\"world\"))", result);
+ }
+
+ [Fact]
+ public void g_Inject_PrimitivePDT_leading_zeros()
+ {
+ var pdt = new PrimitiveProviderDefinedType("Padded", "007");
+ var result = _g.Inject((object)pdt).GremlinLang.GetGremlin();
+ Assert.Equal("g.inject(PDT(\"Padded\",\"007\"))", result);
+ }
+
+ [Fact]
+ public void g_Inject_PrimitivePDT_auto_dehydration_via_primitive_adapter()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ registry.RegisterPrimitive(new TestUint32Adapter());
+
+ var g = new GraphTraversalSource();
+ g.GremlinLang.PdtRegistry = registry;
+
+ var result = g.Inject((object)99u).GremlinLang.GetGremlin();
+ Assert.Equal("g.inject(PDT(\"test:Uint32\",\"99\"))", result);
+ }
+
+ [Fact]
+ public void g_Inject_PrimitivePDT_adapter_takes_precedence_over_attribute()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ registry.RegisterPrimitive(new PrimitiveAdapterForAnnotatedType());
+
+ var g = new GraphTraversalSource();
+ g.GremlinLang.PdtRegistry = registry;
+
+ var obj = new AnnotatedButPrimitiveAdapted { Data = "hello" };
+ var result = g.Inject((object)obj).GremlinLang.GetGremlin();
+
+ // The primitive adapter should win over [ProviderDefined] attribute
+ Assert.Equal("g.inject(PDT(\"prim:Adapted\",\"hello\"))", result);
+ }
+
+ [ProviderDefined(Name = "attr.Annotated")]
+ private class AnnotatedButPrimitiveAdapted
+ {
+ public string Data { get; set; } = "";
+ }
+
+ private class PrimitiveAdapterForAnnotatedType : IPrimitivePdtAdapter<AnnotatedButPrimitiveAdapted>
+ {
+ public string TypeName => "prim:Adapted";
+ public AnnotatedButPrimitiveAdapted FromString(string value) => new() { Data = value };
+ public string ToString(AnnotatedButPrimitiveAdapted obj) => obj.Data;
+ }
+
+ private class TestUint32Adapter : IPrimitivePdtAdapter<uint>
+ {
+ public string TypeName => "test:Uint32";
+ public uint FromString(string value) => uint.Parse(value);
+ public string ToString(uint obj) => obj.ToString();
+ }
}
}
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/PrimitiveProviderDefinedTypeTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/PrimitiveProviderDefinedTypeTests.cs
new file mode 100644
index 0000000000..e96d03e451
--- /dev/null
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/PrimitiveProviderDefinedTypeTests.cs
@@ -0,0 +1,204 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#endregion
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Gremlin.Net.Structure;
+using Gremlin.Net.Structure.IO.GraphBinary4;
+using Xunit;
+
+namespace Gremlin.Net.UnitTest.Structure.IO.GraphBinary4
+{
+ public class PrimitiveProviderDefinedTypeTests
+ {
+ private static readonly GraphBinaryWriter Writer = new();
+ private static readonly GraphBinaryReader Reader = new();
+
+ [Fact]
+ public async Task TestRoundTripBasic()
+ {
+ var expected = new PrimitiveProviderDefinedType("com.example.Uint32", "42");
+
+ using var stream = new MemoryStream();
+ await Writer.WriteAsync(expected, stream);
+ stream.Position = 0;
+ var actual = await Reader.ReadAsync(stream) as PrimitiveProviderDefinedType;
+
+ Assert.NotNull(actual);
+ Assert.Equal(expected.Name, actual!.Name);
+ Assert.Equal(expected.Value, actual.Value);
+ }
+
+ [Fact]
+ public async Task TestRoundTripWithLeadingZeros()
+ {
+ var expected = new PrimitiveProviderDefinedType("com.example.Padded", "007");
+
+ using var stream = new MemoryStream();
+ await Writer.WriteAsync(expected, stream);
+ stream.Position = 0;
+ var actual = await Reader.ReadAsync(stream) as PrimitiveProviderDefinedType;
+
+ Assert.NotNull(actual);
+ Assert.Equal("007", actual!.Value);
+ }
+
+ [Fact]
+ public async Task TestRoundTripWithLargeNumber()
+ {
+ var expected = new PrimitiveProviderDefinedType("com.example.BigNum",
+ "99999999999999999999999999999999");
+
+ using var stream = new MemoryStream();
+ await Writer.WriteAsync(expected, stream);
+ stream.Position = 0;
+ var actual = await Reader.ReadAsync(stream) as PrimitiveProviderDefinedType;
+
+ Assert.NotNull(actual);
+ Assert.Equal("99999999999999999999999999999999", actual!.Value);
+ }
+
+ [Fact]
+ public async Task TestRoundTripNonNumericValue()
+ {
+ var expected = new PrimitiveProviderDefinedType("com.example.Token", "abc-def-123");
+
+ using var stream = new MemoryStream();
+ await Writer.WriteAsync(expected, stream);
+ stream.Position = 0;
+ var actual = await Reader.ReadAsync(stream) as PrimitiveProviderDefinedType;
+
+ Assert.NotNull(actual);
+ Assert.Equal("abc-def-123", actual!.Value);
+ }
+
+ [Fact]
+ public async Task TestRoundTripEmptyValue()
+ {
+ var expected = new PrimitiveProviderDefinedType("com.example.Empty", "");
+
+ using var stream = new MemoryStream();
+ await Writer.WriteAsync(expected, stream);
+ stream.Position = 0;
+ var actual = await Reader.ReadAsync(stream) as PrimitiveProviderDefinedType;
+
+ Assert.NotNull(actual);
+ Assert.Equal("", actual!.Value);
+ }
+
+ [Fact]
+ public async Task TestDataTypeCode()
+ {
+ var pdt = new PrimitiveProviderDefinedType("com.example.Test", "val");
+
+ using var stream = new MemoryStream();
+ await Writer.WriteAsync(pdt, stream);
+
+ Assert.Equal(0xF1, stream.ToArray()[0]);
+ }
+
+ [Fact]
+ public void TestConstructorThrowsOnNullName()
+ {
+ Assert.Throws<ArgumentNullException>(() =>
+ new PrimitiveProviderDefinedType(null!, "val"));
+ }
+
+ [Fact]
+ public void TestConstructorThrowsOnEmptyName()
+ {
+ Assert.Throws<ArgumentException>(() =>
+ new PrimitiveProviderDefinedType("", "val"));
+ }
+
+ [Fact]
+ public void TestConstructorThrowsOnNullValue()
+ {
+ Assert.Throws<ArgumentNullException>(() =>
+ new PrimitiveProviderDefinedType("com.example.T", null!));
+ }
+
+ [Fact]
+ public void TestEquality()
+ {
+ var a = new PrimitiveProviderDefinedType("com.example.T", "42");
+ var b = new PrimitiveProviderDefinedType("com.example.T", "42");
+ Assert.Equal(a, b);
+ Assert.Equal(a.GetHashCode(), b.GetHashCode());
+ }
+
+ [Fact]
+ public void TestInequality()
+ {
+ var a = new PrimitiveProviderDefinedType("com.example.A", "1");
+ var b = new PrimitiveProviderDefinedType("com.example.B", "1");
+ Assert.NotEqual(a, b);
+ }
+
+ [Fact]
+ public void TestToString()
+ {
+ var pdt = new PrimitiveProviderDefinedType("com.example.T", "42");
+ Assert.Contains("com.example.T", pdt.ToString());
+ Assert.Contains("42", pdt.ToString());
+ }
+
+ [Fact]
+ public async Task TestHydrationWithRegistry()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ registry.RegisterPrimitive(new TestUint32Adapter());
+ var reader = new GraphBinaryReader(pdtRegistry: registry);
+
+ var pdt = new PrimitiveProviderDefinedType("test:Uint32", "123");
+ using var stream = new MemoryStream();
+ await Writer.WriteAsync(pdt, stream);
+ stream.Position = 0;
+ var result = await reader.ReadAsync(stream);
+
+ Assert.IsType<uint>(result);
+ Assert.Equal(123u, (uint)result);
+ }
+
+ [Fact]
+ public async Task TestNoHydrationWithoutRegistry()
+ {
+ var pdt = new PrimitiveProviderDefinedType("test:Uint32", "456");
+ using var stream = new MemoryStream();
+ await Writer.WriteAsync(pdt, stream);
+ stream.Position = 0;
+ var result = await Reader.ReadAsync(stream);
+
+ Assert.IsType<PrimitiveProviderDefinedType>(result);
+ Assert.Equal("456", ((PrimitiveProviderDefinedType)result).Value);
+ }
+
+ private class TestUint32Adapter : IPrimitivePdtAdapter<uint>
+ {
+ public string TypeName => "test:Uint32";
+ public uint FromString(string value) => uint.Parse(value);
+ public string ToString(uint obj) => obj.ToString();
+ }
+ }
+}
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/PrimitivePdtRegistryTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/PrimitivePdtRegistryTests.cs
new file mode 100644
index 0000000000..ad33728399
--- /dev/null
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/PrimitivePdtRegistryTests.cs
@@ -0,0 +1,126 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#endregion
+
+using System;
+using System.Collections.Generic;
+using Gremlin.Net.Structure;
+using Xunit;
+
+namespace Gremlin.Net.UnitTest.Structure
+{
+ public class PrimitivePdtRegistryTests
+ {
+ [Fact]
+ public void ShouldHydratePrimitiveWhenAdapterRegistered()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ registry.RegisterPrimitive(new Uint32Adapter());
+ var pdt = new PrimitiveProviderDefinedType("test:Uint32", "42");
+
+ var result = registry.HydratePrimitive(pdt);
+
+ Assert.IsType<uint>(result);
+ Assert.Equal(42u, (uint)result);
+ }
+
+ [Fact]
+ public void ShouldReturnRawPrimitivePdtWhenNoAdapterRegistered()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ var pdt = new PrimitiveProviderDefinedType("unknown:Type", "hello");
+
+ var result = registry.HydratePrimitive(pdt);
+
+ Assert.Same(pdt, result);
+ }
+
+ [Fact]
+ public void ShouldReturnRawPrimitivePdtWhenAdapterThrows()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ registry.RegisterPrimitive(new ThrowingPrimitiveAdapter());
+ var pdt = new PrimitiveProviderDefinedType("bad:Type", "oops");
+
+ var result = registry.HydratePrimitive(pdt);
+
+ Assert.Same(pdt, result);
+ }
+
+ [Fact]
+ public void ShouldHydratePrimitiveNestedInComposite()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ registry.RegisterPrimitive(new Uint32Adapter());
+ var inner = new PrimitiveProviderDefinedType("test:Uint32", "99");
+ var outer = new ProviderDefinedType("unregistered:Wrapper",
+ new Dictionary<string, object?> { ["val"] = inner, ["label"] = "test" });
+
+ var result = registry.HydrateComposite(outer);
+
+ var rawOuter = Assert.IsType<ProviderDefinedType>(result);
+ Assert.Equal(99u, (uint)rawOuter.Fields["val"]!);
+ Assert.Equal("test", rawOuter.Fields["label"]);
+ }
+
+ [Fact]
+ public void ShouldGetPrimitiveAdapterByType()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+ registry.RegisterPrimitive(new Uint32Adapter());
+
+ var info = registry.GetPrimitiveAdapterByType(typeof(uint));
+
+ Assert.NotNull(info);
+ Assert.Equal("test:Uint32", info!.Value.typeName);
+ Assert.Equal("123", info.Value.Item2(123u));
+ }
+
+ [Fact]
+ public void ShouldReturnNullForUnregisteredPrimitiveType()
+ {
+ var registry = new ProviderDefinedTypeRegistry();
+
+ var info = registry.GetPrimitiveAdapterByType(typeof(uint));
+
+ Assert.Null(info);
+ }
+
+ #region Test helpers
+
+ private class Uint32Adapter : IPrimitivePdtAdapter<uint>
+ {
+ public string TypeName => "test:Uint32";
+ public uint FromString(string value) => uint.Parse(value);
+ public string ToString(uint obj) => obj.ToString();
+ }
+
+ private class ThrowingPrimitiveAdapter : IPrimitivePdtAdapter<object>
+ {
+ public string TypeName => "bad:Type";
+ public object FromString(string value) => throw new InvalidOperationException("intentional");
+ public string ToString(object obj) => throw new InvalidOperationException("intentional");
+ }
+
+ #endregion
+ }
+}