From a7c495c3faaca22672dea5dbdf15d1eb5dce00ab Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:14:33 +0800
Subject: [PATCH 001/399] feat(identity): introduce fixed-width RPC semantic
hash
---
.../RpcGeneratedCodecRegistry.cs | 7 ++-
src/SharpLink.Abstractions/RpcHash128.cs | 43 +++++++++++++++++++
2 files changed, 48 insertions(+), 2 deletions(-)
create mode 100644 src/SharpLink.Abstractions/RpcHash128.cs
diff --git a/src/SharpLink.Abstractions/RpcGeneratedCodecRegistry.cs b/src/SharpLink.Abstractions/RpcGeneratedCodecRegistry.cs
index 22c9909cc..3df993274 100644
--- a/src/SharpLink.Abstractions/RpcGeneratedCodecRegistry.cs
+++ b/src/SharpLink.Abstractions/RpcGeneratedCodecRegistry.cs
@@ -6,10 +6,13 @@ public interface IRpcGeneratedCodecFactory
/// Gets the closed DTO or collection type handled by the factory.
Type TargetType { get; }
- /// Gets the deterministic schema identifier used for idempotent registration.
+ /// Gets the deterministic identity of the finalized Codec semantics.
+ RpcHash128 CodecHash => default;
+
+ /// Gets the legacy deterministic schema identifier.
string SchemaId { get; }
- /// Gets the stable binary wire-format identity.
+ /// Gets the legacy binary wire-format identity.
string WireFormatId { get; }
/// Gets the adapter lifecycle identity, or null for adapter-free Codecs.
diff --git a/src/SharpLink.Abstractions/RpcHash128.cs b/src/SharpLink.Abstractions/RpcHash128.cs
new file mode 100644
index 000000000..529ab87f2
--- /dev/null
+++ b/src/SharpLink.Abstractions/RpcHash128.cs
@@ -0,0 +1,43 @@
+using System.Globalization;
+
+namespace SharpLink.Abstractions;
+
+/// Represents a deterministic fixed-width RPC semantic identity.
+public readonly struct RpcHash128 : IEquatable
+{
+ /// Creates a 128-bit identity from its high and low 64-bit words.
+ public RpcHash128(ulong high, ulong low)
+ {
+ High = high;
+ Low = low;
+ }
+
+ /// Gets the high 64 bits.
+ public ulong High { get; }
+
+ /// Gets the low 64 bits.
+ public ulong Low { get; }
+
+ /// Gets whether all bits are zero.
+ public bool IsEmpty => (High | Low) == 0;
+
+ ///
+ public bool Equals(RpcHash128 other) => High == other.High && Low == other.Low;
+
+ ///
+ public override bool Equals(object? obj) => obj is RpcHash128 other && Equals(other);
+
+ ///
+ public override int GetHashCode() => HashCode.Combine(High, Low);
+
+ ///
+ public override string ToString()
+ => High.ToString("x16", CultureInfo.InvariantCulture) +
+ Low.ToString("x16", CultureInfo.InvariantCulture);
+
+ /// Compares two RPC identities for exact equality.
+ public static bool operator ==(RpcHash128 left, RpcHash128 right) => left.Equals(right);
+
+ /// Compares two RPC identities for inequality.
+ public static bool operator !=(RpcHash128 left, RpcHash128 right) => !left.Equals(right);
+}
From fbc144d8adf22378220f600313076fdd2a561c17 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:22:25 +0800
Subject: [PATCH 002/399] refactor(identity): carry finalized codec hashes in
generator model
---
.../RpcGenerator.Models.cs | 48 ++++++++++++++++++-
1 file changed, 46 insertions(+), 2 deletions(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.Models.cs b/src/SharpLink.Generator/RpcGenerator.Models.cs
index 5fee88c31..91d065d0b 100644
--- a/src/SharpLink.Generator/RpcGenerator.Models.cs
+++ b/src/SharpLink.Generator/RpcGenerator.Models.cs
@@ -206,6 +206,17 @@ internal sealed record GeneratedCodecModel(
public bool ElementIsString { get; init; }
}
+internal readonly record struct GeneratedCodecHashModel(
+ string TypeName,
+ ulong High,
+ ulong Low);
+
+internal readonly record struct RpcHashValue(ulong High, ulong Low)
+{
+ public string ToHex()
+ => High.ToString("x16", InvariantCulture) + Low.ToString("x16", InvariantCulture);
+}
+
internal enum DtoDiagnosticKind
{
Unsupported,
@@ -240,7 +251,11 @@ internal sealed record DtoGenerationResult(
ImmutableArray ContractCodecs,
ImmutableArray FinalCodecBoundTypes,
ImmutableArray Diagnostics,
- ImmutableArray Enums);
+ ImmutableArray Enums)
+{
+ public ImmutableArray CodecHashes { get; init; } =
+ ImmutableArray.Empty;
+}
internal sealed record GeneratedEnumModel(
string TypeName,
@@ -258,6 +273,7 @@ public bool Equals(DtoGenerationResult? x, DtoGenerationResult? y)
if (x is null || y is null || x.Codecs.Length != y.Codecs.Length ||
x.ContractCodecs.Length != y.ContractCodecs.Length ||
x.FinalCodecBoundTypes.Length != y.FinalCodecBoundTypes.Length ||
+ x.CodecHashes.Length != y.CodecHashes.Length ||
x.Diagnostics.Length != y.Diagnostics.Length || x.Enums.Length != y.Enums.Length)
{
return false;
@@ -274,6 +290,11 @@ public bool Equals(DtoGenerationResult? x, DtoGenerationResult? y)
}
if (!x.FinalCodecBoundTypes.SequenceEqual(y.FinalCodecBoundTypes, StringComparer.Ordinal))
return false;
+ for (var index = 0; index < x.CodecHashes.Length; index++)
+ {
+ if (x.CodecHashes[index] != y.CodecHashes[index])
+ return false;
+ }
for (var index = 0; index < x.Diagnostics.Length; index++)
{
var left = x.Diagnostics[index];
@@ -313,6 +334,12 @@ public int GetHashCode(DtoGenerationResult obj)
}
foreach (var type in obj.FinalCodecBoundTypes)
hash = unchecked(hash * 31 + StringComparer.Ordinal.GetHashCode(type));
+ foreach (var codecHash in obj.CodecHashes)
+ {
+ hash = unchecked(hash * 31 + StringComparer.Ordinal.GetHashCode(codecHash.TypeName));
+ hash = unchecked(hash * 31 + codecHash.High.GetHashCode());
+ hash = unchecked(hash * 31 + codecHash.Low.GetHashCode());
+ }
foreach (var diagnostic in obj.Diagnostics)
hash = unchecked(hash * 31 + StringComparer.Ordinal.GetHashCode(diagnostic.Detail));
foreach (var item in obj.Enums)
@@ -372,6 +399,23 @@ public static long GetInterfaceHash(string iName)
public static string GetIdentifierHash(string value)
=> Hash(value).ToString("x16", CultureInfo.InvariantCulture);
+ public static RpcHashValue GetSemanticHash(params string[] parts)
+ {
+ var canonical = new StringBuilder();
+ foreach (var part in parts)
+ {
+ var value = part ?? string.Empty;
+ canonical.Append(value.Length.ToString(CultureInfo.InvariantCulture))
+ .Append(':')
+ .Append(value);
+ }
+
+ var hex = GetSha256(canonical.ToString());
+ return new RpcHashValue(
+ ulong.Parse(hex.Substring(0, 16), NumberStyles.HexNumber, CultureInfo.InvariantCulture),
+ ulong.Parse(hex.Substring(16, 16), NumberStyles.HexNumber, CultureInfo.InvariantCulture));
+ }
+
public static string GetSha256(string value)
{
using (var sha = System.Security.Cryptography.SHA256.Create())
@@ -395,4 +439,4 @@ private static ulong Hash(string s)
}
return hash;
}
-}
+}
\ No newline at end of file
From aa462016249163f0d7f3ce673b47f6e342b2835e Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:23:55 +0800
Subject: [PATCH 003/399] feat(identity): compute deterministic final codec
hashes
---
.../RpcGenerator.CodecIdentity.cs | 442 ++++++++++++++++++
1 file changed, 442 insertions(+)
create mode 100644 src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
diff --git a/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs b/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
new file mode 100644
index 000000000..7b4db7399
--- /dev/null
+++ b/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
@@ -0,0 +1,442 @@
+namespace SharpLink.Generator;
+
+public partial class RpcGenerator
+{
+ private sealed partial class DtoAnalysisState
+ {
+ internal ImmutableArray BuildFinalCodecHashes(
+ bool includeSerializable,
+ bool includeContracts)
+ {
+ var roots = new Dictionary(StringComparer.Ordinal);
+ CollectCurrentAssemblyRoots(
+ _compilation.Assembly.GlobalNamespace,
+ roots,
+ includeSerializable,
+ includeContracts);
+
+ var reachable = new Dictionary(StringComparer.Ordinal);
+ var seen = new HashSet(SymbolEqualityComparer.Default);
+ foreach (var root in roots.Values)
+ CollectFinalBindingTypes(root, reachable, seen, 0);
+
+ var cache = new Dictionary(StringComparer.Ordinal);
+ return reachable
+ .OrderBy(static pair => pair.Key, StringComparer.Ordinal)
+ .Select(pair =>
+ {
+ var hash = GetFinalCodecHash(pair.Value, cache, new HashSet(StringComparer.Ordinal));
+ return new GeneratedCodecHashModel(pair.Key, hash.High, hash.Low);
+ })
+ .ToImmutableArray();
+ }
+
+ private RpcHashValue GetFinalCodecHash(
+ ITypeSymbol type,
+ Dictionary cache,
+ HashSet stack)
+ {
+ var typeName = GetTypeName(type);
+ if (cache.TryGetValue(typeName, out var cached))
+ return cached;
+ if (!stack.Add(typeName))
+ return Hashing.GetSemanticHash("codec/v1", "recursive", typeName);
+
+ RpcHashValue result;
+ if (TryGetFrameworkPrimitiveCodecHash(type, cache, stack, out result))
+ {
+ stack.Remove(typeName);
+ cache[typeName] = result;
+ return result;
+ }
+
+ if (_models.TryGetValue(typeName, out var model))
+ {
+ result = GetGeneratedCodecHash(model, cache, stack);
+ stack.Remove(typeName);
+ cache[typeName] = result;
+ return result;
+ }
+
+ if (TryGetCollection(type, out var collectionKind, out var elementType, out var keyType, out var valueType))
+ {
+ var parts = new List
+ {
+ "codec/v1",
+ "collection",
+ collectionKind.ToString()
+ };
+ if (elementType is not null)
+ parts.Add(GetFinalCodecHash(elementType, cache, stack).ToHex());
+ if (keyType is not null)
+ parts.Add(GetFinalCodecHash(keyType, cache, stack).ToHex());
+ if (valueType is not null)
+ parts.Add(GetFinalCodecHash(valueType, cache, stack).ToHex());
+ result = Hashing.GetSemanticHash(parts.ToArray());
+ }
+ else if (type.IsUnmanagedType)
+ {
+ var layout = new StringBuilder("unsafe-blit/v1");
+ AppendUnsafeBlitPhysicalLayout(
+ type,
+ layout,
+ new HashSet(SymbolEqualityComparer.Default));
+ result = Hashing.GetSemanticHash("codec/v1", layout.ToString());
+ }
+ else
+ {
+ // A non-local generated dependency has no behavior that can be inferred from its CLR
+ // name. This fallback keeps the identity deterministic until the referenced manifest
+ // hash table is consumed by the downstream-assembly path.
+ result = Hashing.GetSemanticHash("codec/v1", "external-generated", typeName);
+ }
+
+ stack.Remove(typeName);
+ cache[typeName] = result;
+ return result;
+ }
+
+ private RpcHashValue GetGeneratedCodecHash(
+ GeneratedCodecModel model,
+ Dictionary cache,
+ HashSet stack)
+ {
+ switch (model.Kind)
+ {
+ case GeneratedCodecKind.Custom:
+ return Hashing.GetSemanticHash(
+ "codec/v1",
+ "custom-opaque",
+ model.WireFormatId,
+ model.SchemaId);
+ case GeneratedCodecKind.Adapter:
+ return Hashing.GetSemanticHash(
+ "codec/v1",
+ "adapter-opaque",
+ model.AdapterId ?? string.Empty,
+ model.WireFormatId);
+ case GeneratedCodecKind.Dto:
+ {
+ var parts = new List
+ {
+ "codec/v1",
+ "dto",
+ model.IsReferenceType ? "ref" : "value"
+ };
+ foreach (var member in model.Members.OrderBy(static member => member.FieldId))
+ {
+ parts.Add(member.FieldId.ToString(InvariantCulture));
+ parts.Add(member.Kind.ToString());
+ parts.Add(member.Required ? "required" : "optional");
+ parts.Add(member.Nullable ? "nullable" : "non-nullable");
+ parts.Add(member.NonNullableReference ? "non-null-ref" : "other-null-semantics");
+ switch (member.Kind)
+ {
+ case GeneratedMemberKind.String:
+ parts.Add("string/utf8/v1");
+ break;
+ case GeneratedMemberKind.Fixed:
+ case GeneratedMemberKind.NullableFixed:
+ parts.Add(GetFixedMemberSemanticIdentity(member));
+ break;
+ case GeneratedMemberKind.Complex:
+ if (!TryResolveReachableType(member.TypeName, out var memberType))
+ parts.Add(Hashing.GetSemanticHash("codec/v1", "external-generated", member.TypeName).ToHex());
+ else
+ parts.Add(GetFinalCodecHash(memberType, cache, stack).ToHex());
+ break;
+ }
+ }
+ return Hashing.GetSemanticHash(parts.ToArray());
+ }
+ default:
+ {
+ var parts = new List
+ {
+ "codec/v1",
+ "collection",
+ model.Kind.ToString()
+ };
+ AppendChild(model.ElementType);
+ AppendChild(model.KeyType);
+ AppendChild(model.ValueType);
+ return Hashing.GetSemanticHash(parts.ToArray());
+
+ void AppendChild(string? childTypeName)
+ {
+ if (childTypeName is null)
+ return;
+ if (TryResolveReachableType(childTypeName, out var childType))
+ parts.Add(GetFinalCodecHash(childType, cache, stack).ToHex());
+ else
+ parts.Add(Hashing.GetSemanticHash("codec/v1", "external-generated", childTypeName).ToHex());
+ }
+ }
+ }
+ }
+
+ private bool TryResolveReachableType(string typeName, out ITypeSymbol type)
+ {
+ var roots = new Dictionary(StringComparer.Ordinal);
+ CollectCurrentAssemblyRoots(
+ _compilation.Assembly.GlobalNamespace,
+ roots,
+ includeSerializable: !_contractMode,
+ includeContracts: _contractMode);
+ var reachable = new Dictionary(StringComparer.Ordinal);
+ var seen = new HashSet(SymbolEqualityComparer.Default);
+ foreach (var root in roots.Values)
+ CollectFinalBindingTypes(root, reachable, seen, 0);
+ return reachable.TryGetValue(typeName, out type!);
+ }
+
+ private static string GetFixedMemberSemanticIdentity(GeneratedMemberModel member)
+ {
+ var typeName = member.FixedTypeName ?? member.TypeName;
+ return string.Join(
+ ":",
+ "fixed/v1",
+ member.FixedSize.ToString(InvariantCulture),
+ member.EnumUnderlyingType ?? typeName);
+ }
+
+ private bool TryGetFrameworkPrimitiveCodecHash(
+ ITypeSymbol type,
+ Dictionary cache,
+ HashSet stack,
+ out RpcHashValue hash)
+ {
+ if (type.TypeKind == TypeKind.Enum &&
+ type is INamedTypeSymbol { EnumUnderlyingType: { } enumUnderlying })
+ {
+ hash = Hashing.GetSemanticHash(
+ "codec/v1",
+ "enum",
+ GetFinalCodecHash(enumUnderlying, cache, stack).ToHex());
+ return true;
+ }
+
+ if (type is INamedTypeSymbol nullable &&
+ nullable.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T &&
+ nullable.TypeArguments.Length == 1 &&
+ IsFrameworkWirePrimitive(nullable.TypeArguments[0]))
+ {
+ hash = Hashing.GetSemanticHash(
+ "codec/v1",
+ "nullable",
+ GetFinalCodecHash(nullable.TypeArguments[0], cache, stack).ToHex());
+ return true;
+ }
+
+ string? token = type.SpecialType switch
+ {
+ SpecialType.System_String => "string/utf8/v1",
+ SpecialType.System_Boolean => "bool/fixed1/v1",
+ SpecialType.System_Byte => "u8/fixed1/v1",
+ SpecialType.System_SByte => "i8/fixed1/v1",
+ SpecialType.System_Int16 => "i16/fixed2/v1",
+ SpecialType.System_UInt16 => "u16/fixed2/v1",
+ SpecialType.System_Char => "char/fixed2/v1",
+ SpecialType.System_Int32 => "i32/fixed4/v1",
+ SpecialType.System_UInt32 => "u32/fixed4/v1",
+ SpecialType.System_Single => "f32/fixed4/v1",
+ SpecialType.System_Int64 => "i64/fixed8/v1",
+ SpecialType.System_UInt64 => "u64/fixed8/v1",
+ SpecialType.System_Double => "f64/fixed8/v1",
+ SpecialType.System_Decimal => "decimal/fixed16/v1",
+ _ => null
+ };
+
+ if (token is null && type is IArrayTypeSymbol { Rank: 1, ElementType.SpecialType: SpecialType.System_Byte })
+ token = "bytes/v1";
+ if (token is null)
+ {
+ token = type.ToDisplayString() switch
+ {
+ "System.Half" => "half/fixed2/v1",
+ "System.Text.Rune" => "rune/fixed4/v1",
+ "System.Guid" => "guid/fixed16/v1",
+ "System.DateTimeOffset" => "datetime-offset/fixed16/v1",
+ "System.DateTime" => "datetime/fixed8/v1",
+ "System.DateOnly" => "date-only/fixed4/v1",
+ "System.TimeOnly" => "time-only/fixed8/v1",
+ "System.TimeSpan" => "timespan/fixed8/v1",
+ "System.Int128" => "i128/fixed16/v1",
+ "System.UInt128" => "u128/fixed16/v1",
+ "System.Index" => "index/fixed4/v1",
+ "System.Range" => "range/fixed8/v1",
+ _ => null
+ };
+ }
+
+ if (token is null)
+ {
+ hash = default;
+ return false;
+ }
+
+ hash = Hashing.GetSemanticHash("codec/v1", "framework", token);
+ return true;
+ }
+
+ private void AppendUnsafeBlitPhysicalLayout(
+ ITypeSymbol type,
+ StringBuilder builder,
+ HashSet stack)
+ {
+ if (TryAppendPhysicalPrimitive(type, builder))
+ return;
+
+ if (type.TypeKind == TypeKind.Enum &&
+ type is INamedTypeSymbol { EnumUnderlyingType: { } enumUnderlying })
+ {
+ builder.Append("|enum");
+ AppendUnsafeBlitPhysicalLayout(enumUnderlying, builder, stack);
+ return;
+ }
+
+ if (type is IPointerTypeSymbol pointer)
+ {
+ builder.Append("|pointer|");
+ AppendUnsafeBlitPhysicalLayout(pointer.PointedAtType, builder, stack);
+ return;
+ }
+ if (type is IFunctionPointerTypeSymbol)
+ {
+ builder.Append("|function-pointer");
+ return;
+ }
+ if (type is not INamedTypeSymbol named)
+ {
+ builder.Append("|unknown-unmanaged");
+ return;
+ }
+
+ AppendPhysicalLayoutAttribute(builder, named, "System.Runtime.InteropServices.StructLayoutAttribute", stack);
+ AppendPhysicalLayoutAttribute(builder, named, "System.Runtime.CompilerServices.InlineArrayAttribute", stack);
+
+ if (!stack.Add(type))
+ {
+ builder.Append("|recursive");
+ return;
+ }
+
+ if (named.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T &&
+ named.TypeArguments.Length == 1)
+ {
+ builder.Append("|nullable-underlying");
+ AppendUnsafeBlitPhysicalLayout(named.TypeArguments[0], builder, stack);
+ stack.Remove(type);
+ return;
+ }
+
+ var fields = named.GetMembers()
+ .OfType()
+ .Where(static field => !field.IsStatic && !field.IsConst)
+ .ToArray();
+ builder.Append("|fields:").Append(fields.Length.ToString(InvariantCulture));
+ for (var index = 0; index < fields.Length; index++)
+ {
+ var field = fields[index];
+ builder.Append("|field:").Append(index.ToString(InvariantCulture));
+ if (field.IsFixedSizeBuffer)
+ builder.Append("|fixed-buffer:").Append(field.FixedSize.ToString(InvariantCulture));
+ AppendPhysicalLayoutAttribute(builder, field, "System.Runtime.InteropServices.FieldOffsetAttribute", stack);
+ AppendPhysicalLayoutAttribute(builder, field, "System.Runtime.CompilerServices.FixedBufferAttribute", stack);
+ AppendUnsafeBlitPhysicalLayout(field.Type, builder, stack);
+ }
+
+ stack.Remove(type);
+ }
+
+ private static bool TryAppendPhysicalPrimitive(ITypeSymbol type, StringBuilder builder)
+ {
+ var token = type.SpecialType switch
+ {
+ SpecialType.System_Boolean => "bool1",
+ SpecialType.System_Byte => "u8",
+ SpecialType.System_SByte => "i8",
+ SpecialType.System_Int16 => "i16",
+ SpecialType.System_UInt16 => "u16",
+ SpecialType.System_Char => "char16",
+ SpecialType.System_Int32 => "i32",
+ SpecialType.System_UInt32 => "u32",
+ SpecialType.System_Single => "f32",
+ SpecialType.System_Int64 => "i64",
+ SpecialType.System_UInt64 => "u64",
+ SpecialType.System_Double => "f64",
+ SpecialType.System_Decimal => "decimal128",
+ _ => null
+ };
+ if (token is null)
+ {
+ token = type.ToDisplayString() switch
+ {
+ "System.Half" => "half16",
+ "System.Text.Rune" => "rune32",
+ "System.Guid" => "guid128",
+ "System.DateTimeOffset" => "datetimeoffset128",
+ "System.DateTime" => "datetime64",
+ "System.DateOnly" => "dateonly32",
+ "System.TimeOnly" => "timeonly64",
+ "System.TimeSpan" => "timespan64",
+ "System.Int128" => "i128",
+ "System.UInt128" => "u128",
+ "System.Index" => "index32",
+ "System.Range" => "range64",
+ _ => null
+ };
+ }
+ if (token is null)
+ return false;
+ builder.Append('|').Append(token);
+ return true;
+ }
+
+ private static void AppendPhysicalLayoutAttribute(
+ StringBuilder builder,
+ ISymbol symbol,
+ string attributeName,
+ HashSet stack)
+ {
+ var attribute = symbol.GetAttributes().FirstOrDefault(item =>
+ string.Equals(item.AttributeClass?.ToDisplayString(), attributeName, StringComparison.Ordinal));
+ if (attribute is null)
+ return;
+
+ builder.Append("|attr:").Append(attributeName);
+ foreach (var argument in attribute.ConstructorArguments)
+ AppendPhysicalLayoutConstant(builder, argument, stack);
+ foreach (var argument in attribute.NamedArguments.OrderBy(static item => item.Key, StringComparer.Ordinal))
+ {
+ builder.Append('|').Append(argument.Key).Append('=');
+ AppendPhysicalLayoutConstant(builder, argument.Value, stack);
+ }
+ }
+
+ private static void AppendPhysicalLayoutConstant(
+ StringBuilder builder,
+ TypedConstant constant,
+ HashSet stack)
+ {
+ builder.Append(':').Append(constant.Kind.ToString()).Append('=');
+ if (constant.Kind == TypedConstantKind.Array)
+ {
+ builder.Append('[');
+ foreach (var item in constant.Values)
+ AppendPhysicalLayoutConstant(builder, item, stack);
+ builder.Append(']');
+ return;
+ }
+
+ if (constant.Value is ITypeSymbol type)
+ {
+ if (!TryAppendPhysicalPrimitive(type, builder))
+ builder.Append("layout-type");
+ return;
+ }
+
+ builder.Append(Convert.ToString(constant.Value, InvariantCulture) ?? "null");
+ }
+ }
+}
\ No newline at end of file
From a5a80e7a13da833757165b1fb4786cfee2408aae Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:24:42 +0800
Subject: [PATCH 004/399] fix(identity): use explicit invariant culture in hash
model
---
src/SharpLink.Generator/RpcGenerator.Models.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.Models.cs b/src/SharpLink.Generator/RpcGenerator.Models.cs
index 91d065d0b..ab3f90e63 100644
--- a/src/SharpLink.Generator/RpcGenerator.Models.cs
+++ b/src/SharpLink.Generator/RpcGenerator.Models.cs
@@ -214,7 +214,8 @@ internal readonly record struct GeneratedCodecHashModel(
internal readonly record struct RpcHashValue(ulong High, ulong Low)
{
public string ToHex()
- => High.ToString("x16", InvariantCulture) + Low.ToString("x16", InvariantCulture);
+ => High.ToString("x16", CultureInfo.InvariantCulture) +
+ Low.ToString("x16", CultureInfo.InvariantCulture);
}
internal enum DtoDiagnosticKind
From 1c06241853f98d106ebf2c3a3b275a124d9473c0 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:40:44 +0800
Subject: [PATCH 005/399] style(generator): format codec identity
---
.../RpcGenerator.CodecIdentity.cs | 100 +++++++++---------
1 file changed, 50 insertions(+), 50 deletions(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs b/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
index 7b4db7399..4d9790eff 100644
--- a/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
+++ b/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
@@ -116,62 +116,62 @@ private RpcHashValue GetGeneratedCodecHash(
model.AdapterId ?? string.Empty,
model.WireFormatId);
case GeneratedCodecKind.Dto:
- {
- var parts = new List
- {
- "codec/v1",
- "dto",
- model.IsReferenceType ? "ref" : "value"
- };
- foreach (var member in model.Members.OrderBy(static member => member.FieldId))
{
- parts.Add(member.FieldId.ToString(InvariantCulture));
- parts.Add(member.Kind.ToString());
- parts.Add(member.Required ? "required" : "optional");
- parts.Add(member.Nullable ? "nullable" : "non-nullable");
- parts.Add(member.NonNullableReference ? "non-null-ref" : "other-null-semantics");
- switch (member.Kind)
+ var parts = new List
{
- case GeneratedMemberKind.String:
- parts.Add("string/utf8/v1");
- break;
- case GeneratedMemberKind.Fixed:
- case GeneratedMemberKind.NullableFixed:
- parts.Add(GetFixedMemberSemanticIdentity(member));
- break;
- case GeneratedMemberKind.Complex:
- if (!TryResolveReachableType(member.TypeName, out var memberType))
- parts.Add(Hashing.GetSemanticHash("codec/v1", "external-generated", member.TypeName).ToHex());
- else
- parts.Add(GetFinalCodecHash(memberType, cache, stack).ToHex());
- break;
+ "codec/v1",
+ "dto",
+ model.IsReferenceType ? "ref" : "value"
+ };
+ foreach (var member in model.Members.OrderBy(static member => member.FieldId))
+ {
+ parts.Add(member.FieldId.ToString(InvariantCulture));
+ parts.Add(member.Kind.ToString());
+ parts.Add(member.Required ? "required" : "optional");
+ parts.Add(member.Nullable ? "nullable" : "non-nullable");
+ parts.Add(member.NonNullableReference ? "non-null-ref" : "other-null-semantics");
+ switch (member.Kind)
+ {
+ case GeneratedMemberKind.String:
+ parts.Add("string/utf8/v1");
+ break;
+ case GeneratedMemberKind.Fixed:
+ case GeneratedMemberKind.NullableFixed:
+ parts.Add(GetFixedMemberSemanticIdentity(member));
+ break;
+ case GeneratedMemberKind.Complex:
+ if (!TryResolveReachableType(member.TypeName, out var memberType))
+ parts.Add(Hashing.GetSemanticHash("codec/v1", "external-generated", member.TypeName).ToHex());
+ else
+ parts.Add(GetFinalCodecHash(memberType, cache, stack).ToHex());
+ break;
+ }
}
+ return Hashing.GetSemanticHash(parts.ToArray());
}
- return Hashing.GetSemanticHash(parts.ToArray());
- }
default:
- {
- var parts = new List
{
- "codec/v1",
- "collection",
- model.Kind.ToString()
- };
- AppendChild(model.ElementType);
- AppendChild(model.KeyType);
- AppendChild(model.ValueType);
- return Hashing.GetSemanticHash(parts.ToArray());
-
- void AppendChild(string? childTypeName)
- {
- if (childTypeName is null)
- return;
- if (TryResolveReachableType(childTypeName, out var childType))
- parts.Add(GetFinalCodecHash(childType, cache, stack).ToHex());
- else
- parts.Add(Hashing.GetSemanticHash("codec/v1", "external-generated", childTypeName).ToHex());
+ var parts = new List
+ {
+ "codec/v1",
+ "collection",
+ model.Kind.ToString()
+ };
+ AppendChild(model.ElementType);
+ AppendChild(model.KeyType);
+ AppendChild(model.ValueType);
+ return Hashing.GetSemanticHash(parts.ToArray());
+
+ void AppendChild(string? childTypeName)
+ {
+ if (childTypeName is null)
+ return;
+ if (TryResolveReachableType(childTypeName, out var childType))
+ parts.Add(GetFinalCodecHash(childType, cache, stack).ToHex());
+ else
+ parts.Add(Hashing.GetSemanticHash("codec/v1", "external-generated", childTypeName).ToHex());
+ }
}
- }
}
}
@@ -439,4 +439,4 @@ private static void AppendPhysicalLayoutConstant(
builder.Append(Convert.ToString(constant.Value, InvariantCulture) ?? "null");
}
}
-}
\ No newline at end of file
+}
From 49faaf53c450b2d46ce41bae548f8b1af161590f Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:41:24 +0800
Subject: [PATCH 006/399] style(generator): terminate identity model file
---
src/SharpLink.Generator/RpcGenerator.Models.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.Models.cs b/src/SharpLink.Generator/RpcGenerator.Models.cs
index ab3f90e63..ab8d68574 100644
--- a/src/SharpLink.Generator/RpcGenerator.Models.cs
+++ b/src/SharpLink.Generator/RpcGenerator.Models.cs
@@ -440,4 +440,4 @@ private static ulong Hash(string s)
}
return hash;
}
-}
\ No newline at end of file
+}
From 19372472195448ae5c67cca365c808b336c30ed0 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:44:30 +0800
Subject: [PATCH 007/399] feat(generator): materialize final codec hashes
---
.../RpcGenerator.CodecPolicyOwnership.cs | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.CodecPolicyOwnership.cs b/src/SharpLink.Generator/RpcGenerator.CodecPolicyOwnership.cs
index d95cb4256..2dcf2c01e 100644
--- a/src/SharpLink.Generator/RpcGenerator.CodecPolicyOwnership.cs
+++ b/src/SharpLink.Generator/RpcGenerator.CodecPolicyOwnership.cs
@@ -27,6 +27,9 @@ private static DtoGenerationResult AnalyzeGeneratedCodecsWithPolicyOwnership(
applyCodecPolicy: true,
selectorOnlyContractDefault: false);
var contractPolicy = contractPolicyState.AnalyzeWithFinalCodecBindings();
+ var codecHashes = contractPolicyState.BuildFinalCodecHashes(
+ includeSerializable: false,
+ includeContracts: true);
var currentContractTypes = contractPolicyState.GetCurrentContractReachableTypeNames();
var currentContractDefaultCodecs = contractDefault.Codecs
@@ -97,7 +100,10 @@ private static DtoGenerationResult AnalyzeGeneratedCodecsWithPolicyOwnership(
contractCodecs,
finalCodecBoundTypes,
diagnostics,
- enums);
+ enums)
+ {
+ CodecHashes = codecHashes
+ };
}
private static bool ContainsRpcContract(INamespaceSymbol namespaceSymbol)
From ce3b3b3783ea455273c5156bb46c581483603139 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:46:22 +0800
Subject: [PATCH 008/399] feat(generator): compose RPC semantic hashes
---
.../RpcGenerator.RpcIdentity.cs | 133 ++++++++++++++++++
1 file changed, 133 insertions(+)
create mode 100644 src/SharpLink.Generator/RpcGenerator.RpcIdentity.cs
diff --git a/src/SharpLink.Generator/RpcGenerator.RpcIdentity.cs b/src/SharpLink.Generator/RpcGenerator.RpcIdentity.cs
new file mode 100644
index 000000000..147814f76
--- /dev/null
+++ b/src/SharpLink.Generator/RpcGenerator.RpcIdentity.cs
@@ -0,0 +1,133 @@
+namespace SharpLink.Generator;
+
+public partial class RpcGenerator
+{
+ private sealed record RpcAssemblyIdentityModel(
+ RpcHashValue AssemblyHash,
+ ImmutableArray Contracts);
+
+ private sealed record RpcContractIdentityModel(
+ long ContractId,
+ RpcHashValue ContractHash,
+ ImmutableArray Methods);
+
+ private sealed record RpcMethodIdentityModel(
+ long MethodId,
+ RpcHashValue MethodHash);
+
+ private static RpcAssemblyIdentityModel BuildRpcAssemblyIdentity(
+ RpcInterfaceModel[] contracts,
+ ImmutableArray codecHashes)
+ {
+ var codecHashByType = codecHashes.ToDictionary(
+ static item => item.TypeName,
+ static item => new RpcHashValue(item.High, item.Low),
+ StringComparer.Ordinal);
+ var contractIdentities = contracts
+ .OrderBy(static contract => contract.Hash)
+ .Select(contract => BuildContractIdentity(contract, codecHashByType))
+ .ToImmutableArray();
+ var assemblyParts = new List
+ {
+ "rpc-assembly/v1",
+ contractIdentities.Length.ToString(InvariantCulture)
+ };
+ foreach (var contract in contractIdentities)
+ {
+ assemblyParts.Add(contract.ContractId.ToString(InvariantCulture));
+ assemblyParts.Add(contract.ContractHash.ToHex());
+ }
+
+ return new RpcAssemblyIdentityModel(
+ Hashing.GetSemanticHash(assemblyParts.ToArray()),
+ contractIdentities);
+ }
+
+ private static RpcContractIdentityModel BuildContractIdentity(
+ RpcInterfaceModel contract,
+ IReadOnlyDictionary codecHashes)
+ {
+ var methods = contract.Methods
+ .OrderBy(static method => method.Hash)
+ .Select(method => new RpcMethodIdentityModel(
+ method.Hash,
+ BuildMethodHash(method, codecHashes)))
+ .ToImmutableArray();
+ var parts = new List
+ {
+ "contract/v1",
+ contract.Hash.ToString(InvariantCulture),
+ methods.Length.ToString(InvariantCulture)
+ };
+ foreach (var method in methods)
+ {
+ parts.Add(method.MethodId.ToString(InvariantCulture));
+ parts.Add(method.MethodHash.ToHex());
+ }
+
+ return new RpcContractIdentityModel(
+ contract.Hash,
+ Hashing.GetSemanticHash(parts.ToArray()),
+ methods);
+ }
+
+ private static RpcHashValue BuildMethodHash(
+ RpcMethodModel method,
+ IReadOnlyDictionary codecHashes)
+ {
+ var payloadParameters = method.Parameters
+ .Where(static parameter => !parameter.IsCancellationToken)
+ .ToArray();
+ var parts = new List
+ {
+ "method/v1",
+ method.Hash.ToString(InvariantCulture),
+ GetMethodKind(method),
+ method.HasCancellationToken ? "cancellable" : "non-cancellable",
+ method.IsIdempotent ? "idempotent" : "non-idempotent",
+ method.HasTimeoutAttribute ? "timeout" : "no-timeout",
+ method.TimeoutSeconds?.ToString("R", InvariantCulture) ?? string.Empty,
+ payloadParameters.Length.ToString(InvariantCulture)
+ };
+ for (var index = 0; index < payloadParameters.Length; index++)
+ {
+ var parameter = payloadParameters[index];
+ var payloadType = parameter.IsStream
+ ? parameter.StreamItemType ?? throw new InvalidOperationException(
+ $"Streaming RPC parameter '{parameter.Name}' has no item type.")
+ : parameter.Type;
+ parts.Add(index.ToString(InvariantCulture));
+ parts.Add(parameter.IsStream ? "stream" : "unary");
+ parts.Add(parameter.PayloadNullable ? "nullable" : "non-nullable");
+ parts.Add(GetRequiredCodecHash(payloadType, codecHashes).ToHex());
+ }
+
+ if (method.IsVoid)
+ {
+ parts.Add("response:void");
+ }
+ else
+ {
+ var responseType = method.IsStreamReturn
+ ? method.StreamItemType ?? throw new InvalidOperationException(
+ $"Streaming RPC method '{method.Name}' has no item type.")
+ : method.GenericArgumentType ?? throw new InvalidOperationException(
+ $"RPC method '{method.Name}' has no response payload type.");
+ parts.Add(method.IsStreamReturn ? "response:stream" : "response:unary");
+ parts.Add(method.ResponseNullable ? "nullable" : "non-nullable");
+ parts.Add(GetRequiredCodecHash(responseType, codecHashes).ToHex());
+ }
+
+ return Hashing.GetSemanticHash(parts.ToArray());
+ }
+
+ private static RpcHashValue GetRequiredCodecHash(
+ string typeName,
+ IReadOnlyDictionary codecHashes)
+ {
+ if (codecHashes.TryGetValue(typeName, out var hash))
+ return hash;
+ throw new InvalidOperationException(
+ $"Final RPC Codec graph is missing deterministic identity for '{typeName}'.");
+ }
+}
From 7e1ecd9ab8973d0044b6e2e17cef3e902111c138 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:47:13 +0800
Subject: [PATCH 009/399] feat(abstractions): expose RPC assembly hash
---
.../SharpLinkGeneratedAssemblyManifest.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/SharpLink.Abstractions/SharpLinkGeneratedAssemblyManifest.cs b/src/SharpLink.Abstractions/SharpLinkGeneratedAssemblyManifest.cs
index 2aa6f5311..a7fd1b16e 100644
--- a/src/SharpLink.Abstractions/SharpLinkGeneratedAssemblyManifest.cs
+++ b/src/SharpLink.Abstractions/SharpLinkGeneratedAssemblyManifest.cs
@@ -138,6 +138,9 @@ public interface ISharpLinkGeneratedAssemblyManifest
/// Gets the assembly that owns this manifest.
Assembly OwnerAssembly { get; }
+ /// Gets the deterministic identity of the complete RPC-visible semantic graph.
+ RpcHash128 RpcAssemblyHash => default;
+
/// Gets the canonical compile-time descriptor used by downstream analyzers.
string CompileTimeDescriptor { get; }
From 721028e2051f590cb4699d24392ce704830e1f16 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:48:07 +0800
Subject: [PATCH 010/399] feat(generator): emit RPC assembly hash
---
src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs b/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs
index a2b866db3..b9942a640 100644
--- a/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs
+++ b/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs
@@ -7,7 +7,8 @@ private static string GenerateAssemblyManifest(
ImmutableArray interfaces,
ImmutableArray services,
ImmutableArray codecs,
- ImmutableArray contractCodecs)
+ ImmutableArray contractCodecs,
+ ImmutableArray codecHashes)
{
var contracts = GetContractModels(interfaces);
var serviceModels = GetServiceModels(services);
@@ -15,6 +16,7 @@ private static string GenerateAssemblyManifest(
return string.Empty;
var manifestTypeName = GetManifestTypeName(contracts, serviceModels, codecs, contractCodecs);
+ var rpcIdentity = BuildRpcAssemblyIdentity(contracts, codecHashes);
// Module dependencies come from generated artifacts and the finalized Codec graph. Contract
// signature CLR references alone are not evidence that the referenced assembly publishes a
// SharpLink generated manifest and therefore must not become dynamic-module dependencies.
@@ -58,6 +60,7 @@ private static string GenerateAssemblyManifest(
sb.AppendLine(" public int ProtocolVersion => 2;");
sb.AppendLine($" public string GeneratorVersion => \"{EscapeString(ExecutingGeneratorVersion)}\";");
sb.AppendLine($" public Assembly OwnerAssembly => typeof({manifestTypeName}).Assembly;");
+ sb.AppendLine($" public RpcHash128 RpcAssemblyHash => new RpcHash128({rpcIdentity.AssemblyHash.High.ToString(InvariantCulture)}UL, {rpcIdentity.AssemblyHash.Low.ToString(InvariantCulture)}UL);");
sb.AppendLine(" string ISharpLinkGeneratedAssemblyManifest.CompileTimeDescriptor => CompileTimeDescriptor;");
sb.AppendLine();
AppendContractArtifactFactories(sb, contracts);
From ecdfe3c640435786f694d7344950b5571383e6e3 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:48:57 +0800
Subject: [PATCH 011/399] feat(generator): pass final codec hashes to manifest
---
src/SharpLink.Generator/RpcGenerator.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.cs b/src/SharpLink.Generator/RpcGenerator.cs
index 24a3d118e..7f4d91a82 100644
--- a/src/SharpLink.Generator/RpcGenerator.cs
+++ b/src/SharpLink.Generator/RpcGenerator.cs
@@ -317,10 +317,11 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var services = value.Left.Right;
var codecs = value.Right.Codecs;
var contractCodecs = value.Right.ContractCodecs;
+ var codecHashes = value.Right.CodecHashes;
var contracts = GetContractModels(interfaces);
var serviceModels = GetServiceModels(services);
- var code = GenerateAssemblyManifest(interfaces, services, codecs, contractCodecs);
+ var code = GenerateAssemblyManifest(interfaces, services, codecs, contractCodecs, codecHashes);
if (!string.IsNullOrEmpty(code))
{
var manifestTypeName = GetManifestTypeName(contracts, serviceModels, codecs, contractCodecs);
From f4364e29b4776a584a8069bff1bca3f2bfd4aa44 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:56:13 +0800
Subject: [PATCH 012/399] feat(generator): attach codec hashes to models
---
src/SharpLink.Generator/RpcGenerator.Models.cs | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/SharpLink.Generator/RpcGenerator.Models.cs b/src/SharpLink.Generator/RpcGenerator.Models.cs
index ab8d68574..3623cce45 100644
--- a/src/SharpLink.Generator/RpcGenerator.Models.cs
+++ b/src/SharpLink.Generator/RpcGenerator.Models.cs
@@ -204,6 +204,8 @@ internal sealed record GeneratedCodecModel(
Location? Location)
{
public bool ElementIsString { get; init; }
+ public ulong CodecHashHigh { get; init; }
+ public ulong CodecHashLow { get; init; }
}
internal readonly record struct GeneratedCodecHashModel(
@@ -327,11 +329,15 @@ public int GetHashCode(DtoGenerationResult obj)
{
hash = unchecked(hash * 31 + StringComparer.Ordinal.GetHashCode(codec.TypeName));
hash = unchecked(hash * 31 + StringComparer.Ordinal.GetHashCode(codec.SchemaId));
+ hash = unchecked(hash * 31 + codec.CodecHashHigh.GetHashCode());
+ hash = unchecked(hash * 31 + codec.CodecHashLow.GetHashCode());
}
foreach (var codec in obj.ContractCodecs)
{
hash = unchecked(hash * 31 + StringComparer.Ordinal.GetHashCode(codec.TypeName));
hash = unchecked(hash * 31 + StringComparer.Ordinal.GetHashCode(codec.SchemaId));
+ hash = unchecked(hash * 31 + codec.CodecHashHigh.GetHashCode());
+ hash = unchecked(hash * 31 + codec.CodecHashLow.GetHashCode());
}
foreach (var type in obj.FinalCodecBoundTypes)
hash = unchecked(hash * 31 + StringComparer.Ordinal.GetHashCode(type));
@@ -356,6 +362,8 @@ private static bool CodecEquals(GeneratedCodecModel left, GeneratedCodecModel ri
if (!string.Equals(left.TypeName, right.TypeName, StringComparison.Ordinal) ||
!string.Equals(left.CodecName, right.CodecName, StringComparison.Ordinal) ||
!string.Equals(left.SchemaId, right.SchemaId, StringComparison.Ordinal) ||
+ left.CodecHashHigh != right.CodecHashHigh ||
+ left.CodecHashLow != right.CodecHashLow ||
left.Kind != right.Kind || left.IsReferenceType != right.IsReferenceType ||
!string.Equals(left.ElementType, right.ElementType, StringComparison.Ordinal) ||
!string.Equals(left.KeyType, right.KeyType, StringComparison.Ordinal) ||
From df4a4f0d009e549c769fefed089b8c8bc25f8d4b Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:57:28 +0800
Subject: [PATCH 013/399] feat(generator): bind hashes to final codec
registrations
---
.../RpcGenerator.CodecPolicyOwnership.cs | 41 +++++++++++++++++--
1 file changed, 37 insertions(+), 4 deletions(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.CodecPolicyOwnership.cs b/src/SharpLink.Generator/RpcGenerator.CodecPolicyOwnership.cs
index 2dcf2c01e..fa493449f 100644
--- a/src/SharpLink.Generator/RpcGenerator.CodecPolicyOwnership.cs
+++ b/src/SharpLink.Generator/RpcGenerator.CodecPolicyOwnership.cs
@@ -13,6 +13,11 @@ private static DtoGenerationResult AnalyzeGeneratedCodecsWithPolicyOwnership(
applyCodecPolicy: true,
selectorOnlyContractDefault: false);
var standalone = standaloneState.AnalyzeWithFinalCodecBindings();
+ var standaloneHashes = standaloneState.BuildFinalCodecHashes(
+ includeSerializable: true,
+ includeContracts: false);
+ var standaloneCodecs = AttachCodecHashes(standalone.Codecs, standaloneHashes);
+
var contractDefaultState = new DtoAnalysisState(
compilation,
cancellationToken,
@@ -20,6 +25,11 @@ private static DtoGenerationResult AnalyzeGeneratedCodecsWithPolicyOwnership(
applyCodecPolicy: true,
selectorOnlyContractDefault: true);
var contractDefault = contractDefaultState.AnalyzeWithFinalCodecBindings();
+ var contractDefaultHashes = contractDefaultState.BuildFinalCodecHashes(
+ includeSerializable: false,
+ includeContracts: true);
+ var contractDefaultCodecs = AttachCodecHashes(contractDefault.Codecs, contractDefaultHashes);
+
var contractPolicyState = new DtoAnalysisState(
compilation,
cancellationToken,
@@ -30,12 +40,13 @@ private static DtoGenerationResult AnalyzeGeneratedCodecsWithPolicyOwnership(
var codecHashes = contractPolicyState.BuildFinalCodecHashes(
includeSerializable: false,
includeContracts: true);
+ var contractPolicyCodecs = AttachCodecHashes(contractPolicy.Codecs, codecHashes);
var currentContractTypes = contractPolicyState.GetCurrentContractReachableTypeNames();
- var currentContractDefaultCodecs = contractDefault.Codecs
+ var currentContractDefaultCodecs = contractDefaultCodecs
.Where(codec => currentContractTypes.Contains(codec.TypeName))
.ToImmutableArray();
- var currentContractPolicyCodecs = contractPolicy.Codecs
+ var currentContractPolicyCodecs = contractPolicyCodecs
.Where(codec => currentContractTypes.Contains(codec.TypeName))
.ToImmutableArray();
var contractOwnedPolicyRoots = new HashSet(
@@ -43,7 +54,7 @@ private static DtoGenerationResult AnalyzeGeneratedCodecsWithPolicyOwnership(
StringComparer.Ordinal);
var standaloneTypes = new HashSet(
- standalone.Codecs.Select(static codec => codec.TypeName),
+ standaloneCodecs.Select(static codec => codec.TypeName),
StringComparer.Ordinal);
var defaultByType = currentContractDefaultCodecs
.ToDictionary(static codec => codec.TypeName, StringComparer.Ordinal);
@@ -60,7 +71,7 @@ private static DtoGenerationResult AnalyzeGeneratedCodecsWithPolicyOwnership(
var globalByType = currentContractDefaultCodecs
.Where(codec => !globalExcludedTypes.Contains(codec.TypeName))
.ToDictionary(static codec => codec.TypeName, StringComparer.Ordinal);
- foreach (var codec in standalone.Codecs)
+ foreach (var codec in standaloneCodecs)
globalByType[codec.TypeName] = codec;
var globalCodecs = globalByType.Values
.OrderBy(static codec => codec.TypeName, StringComparer.Ordinal)
@@ -106,6 +117,28 @@ private static DtoGenerationResult AnalyzeGeneratedCodecsWithPolicyOwnership(
};
}
+ private static ImmutableArray AttachCodecHashes(
+ ImmutableArray codecs,
+ ImmutableArray hashes)
+ {
+ var hashByType = hashes.ToDictionary(static item => item.TypeName, StringComparer.Ordinal);
+ return codecs
+ .Select(codec =>
+ {
+ if (!hashByType.TryGetValue(codec.TypeName, out var hash))
+ {
+ throw new InvalidOperationException(
+ $"Final Codec graph is missing deterministic identity for generated Codec '{codec.TypeName}'.");
+ }
+ return codec with
+ {
+ CodecHashHigh = hash.High,
+ CodecHashLow = hash.Low
+ };
+ })
+ .ToImmutableArray();
+ }
+
private static bool ContainsRpcContract(INamespaceSymbol namespaceSymbol)
{
foreach (var type in namespaceSymbol.GetTypeMembers())
From d27a544ce5fc213220ee133bb13385397f4bcf60 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 12:58:34 +0800
Subject: [PATCH 014/399] feat(generator): bind codec hash at manifest
registration
---
.../RpcGenerator.ManifestEmitter.cs | 34 +++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs b/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs
index b9942a640..e545c8756 100644
--- a/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs
+++ b/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs
@@ -64,6 +64,7 @@ private static string GenerateAssemblyManifest(
sb.AppendLine(" string ISharpLinkGeneratedAssemblyManifest.CompileTimeDescriptor => CompileTimeDescriptor;");
sb.AppendLine();
AppendContractArtifactFactories(sb, contracts);
+ AppendIdentifiedCodecFactory(sb);
AppendContractManifestArray(sb, contracts);
AppendServiceManifestArray(sb, serviceModels);
AppendCodecManifestArray(sb, codecs);
@@ -115,6 +116,31 @@ private static void AppendContractArtifactFactories(StringBuilder sb, RpcInterfa
}
}
+ private static void AppendIdentifiedCodecFactory(StringBuilder sb)
+ {
+ sb.AppendLine(" private sealed class __SharpLinkIdentifiedCodecFactory : IRpcGeneratedCodecFactory");
+ sb.AppendLine(" {");
+ sb.AppendLine(" private readonly IRpcGeneratedCodecFactory __inner;");
+ sb.AppendLine();
+ sb.AppendLine(" internal __SharpLinkIdentifiedCodecFactory(IRpcGeneratedCodecFactory inner, ulong hashHigh, ulong hashLow)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" __inner = inner ?? throw new ArgumentNullException(nameof(inner));");
+ sb.AppendLine(" CodecHash = new RpcHash128(hashHigh, hashLow);");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" public Type TargetType => __inner.TargetType;");
+ sb.AppendLine(" public RpcHash128 CodecHash { get; }");
+ sb.AppendLine(" public string SchemaId => __inner.SchemaId;");
+ sb.AppendLine(" public string WireFormatId => __inner.WireFormatId;");
+ sb.AppendLine(" public string? AdapterId => __inner.AdapterId;");
+ sb.AppendLine(" public IRpcCodecAdapter? Adapter => __inner.Adapter;");
+ sb.AppendLine(" public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapterScope)");
+ sb.AppendLine(" => __inner.Create(provider, adapterScope);");
+ sb.AppendLine(" public bool IsCompatibleCodec(IRpcCodec codec) => __inner.IsCompatibleCodec(codec);");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ }
+
private static void AppendContractManifestArray(StringBuilder sb, RpcInterfaceModel[] contracts)
{
sb.AppendLine(" private static readonly SharpLinkGeneratedContractDescriptor[] __contracts = new SharpLinkGeneratedContractDescriptor[]");
@@ -177,7 +203,7 @@ private static void AppendCodecManifestArray(StringBuilder sb, ImmutableArray codec.TypeName, StringComparer.Ordinal))
- sb.AppendLine($" new {codec.CodecName}.Factory(),");
+ AppendIdentifiedCodecFactoryRegistration(sb, codec);
sb.AppendLine(" };");
}
@@ -188,10 +214,14 @@ private static void AppendContractCodecManifestArray(
sb.AppendLine(" private static readonly IRpcGeneratedCodecFactory[] __contractCodecs = new IRpcGeneratedCodecFactory[]");
sb.AppendLine(" {");
foreach (var codec in codecs.OrderBy(static codec => codec.TypeName, StringComparer.Ordinal))
- sb.AppendLine($" new {codec.CodecName}.Factory(),");
+ AppendIdentifiedCodecFactoryRegistration(sb, codec);
sb.AppendLine(" };");
}
+ private static void AppendIdentifiedCodecFactoryRegistration(StringBuilder sb, GeneratedCodecModel codec)
+ => sb.AppendLine(
+ $" new __SharpLinkIdentifiedCodecFactory(new {codec.CodecName}.Factory(), {codec.CodecHashHigh.ToString(InvariantCulture)}UL, {codec.CodecHashLow.ToString(InvariantCulture)}UL),");
+
private static string BuildCompileTimeDescriptor(
RpcInterfaceModel[] contracts,
RpcServiceModel[] services,
From 72c97cc14bf797a16d977e5b1e8e85e7a5d8b8c8 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:00:06 +0800
Subject: [PATCH 015/399] refactor(runtime): compare generated codec hashes
---
.../SharpLinkRuntimeContext.cs | 22 +++++++++++++++----
1 file changed, 18 insertions(+), 4 deletions(-)
diff --git a/src/SharpLink.Runtime/SharpLinkRuntimeContext.cs b/src/SharpLink.Runtime/SharpLinkRuntimeContext.cs
index afe50b803..fe4ea19b9 100644
--- a/src/SharpLink.Runtime/SharpLinkRuntimeContext.cs
+++ b/src/SharpLink.Runtime/SharpLinkRuntimeContext.cs
@@ -35,13 +35,12 @@ internal SharpLinkRuntimeContext(
foreach (var pair in owner.Codecs)
{
if (generatedRegistrations.TryGetValue(pair.Key, out var existing) &&
- (!string.Equals(existing.Factory.SchemaId, pair.Value.Factory.SchemaId, StringComparison.Ordinal) ||
- !string.Equals(existing.Factory.WireFormatId, pair.Value.Factory.WireFormatId, StringComparison.Ordinal)))
+ !HasSameGeneratedCodecIdentity(existing.Factory, pair.Value.Factory))
{
throw new InvalidOperationException(
$"Generated Codec conflict for '{pair.Key.FullName}': " +
- $"schema/wire '{existing.Factory.SchemaId}'/'{existing.Factory.WireFormatId}' and " +
- $"'{pair.Value.Factory.SchemaId}'/'{pair.Value.Factory.WireFormatId}'.");
+ $"identity '{DescribeGeneratedCodecIdentity(existing.Factory)}' and " +
+ $"'{DescribeGeneratedCodecIdentity(pair.Value.Factory)}'.");
}
generatedRegistrations[pair.Key] = pair.Value;
}
@@ -57,6 +56,21 @@ internal SharpLinkRuntimeContext(
Buffers = new SharpLinkBufferWriterPool(bufferPool);
}
+ private static bool HasSameGeneratedCodecIdentity(
+ IRpcGeneratedCodecFactory left,
+ IRpcGeneratedCodecFactory right)
+ {
+ if (!left.CodecHash.IsEmpty || !right.CodecHash.IsEmpty)
+ return left.CodecHash == right.CodecHash;
+ return string.Equals(left.SchemaId, right.SchemaId, StringComparison.Ordinal) &&
+ string.Equals(left.WireFormatId, right.WireFormatId, StringComparison.Ordinal);
+ }
+
+ private static string DescribeGeneratedCodecIdentity(IRpcGeneratedCodecFactory factory)
+ => factory.CodecHash.IsEmpty
+ ? $"legacy:{factory.SchemaId}/{factory.WireFormatId}"
+ : $"codec:{factory.CodecHash}";
+
[System.Diagnostics.CodeAnalysis.DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowAfterConstructionRollback(
From 3b1ce2a229976f65285ec32ec9f0fecc501488b6 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:01:12 +0800
Subject: [PATCH 016/399] feat(abstractions): publish generated codec identity
metadata
---
...harpLinkGeneratedCodecIdentityAttribute.cs | 23 +++++++++++++++++++
1 file changed, 23 insertions(+)
create mode 100644 src/SharpLink.Abstractions/SharpLinkGeneratedCodecIdentityAttribute.cs
diff --git a/src/SharpLink.Abstractions/SharpLinkGeneratedCodecIdentityAttribute.cs b/src/SharpLink.Abstractions/SharpLinkGeneratedCodecIdentityAttribute.cs
new file mode 100644
index 000000000..ede5f97f9
--- /dev/null
+++ b/src/SharpLink.Abstractions/SharpLinkGeneratedCodecIdentityAttribute.cs
@@ -0,0 +1,23 @@
+namespace SharpLink.Abstractions;
+
+///
+/// Publishes the deterministic default Codec identity of one closed payload type for downstream
+/// source-generation. The target is a metadata lookup key and is not part of the hash.
+///
+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
+[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
+public sealed class SharpLinkGeneratedCodecIdentityAttribute : Attribute
+{
+ /// Creates one generated Codec identity entry.
+ public SharpLinkGeneratedCodecIdentityAttribute(Type targetType, ulong hashHigh, ulong hashLow)
+ {
+ TargetType = targetType ?? throw new ArgumentNullException(nameof(targetType));
+ CodecHash = new RpcHash128(hashHigh, hashLow);
+ }
+
+ /// Gets the closed payload type used to locate this generated identity.
+ public Type TargetType { get; }
+
+ /// Gets the deterministic default Codec identity.
+ public RpcHash128 CodecHash { get; }
+}
From 67d3509de342162785f387197aa2ff5e55843be0 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:02:08 +0800
Subject: [PATCH 017/399] feat(generator): publish default codec hashes
---
src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs b/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs
index e545c8756..2bd7fea90 100644
--- a/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs
+++ b/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs
@@ -43,6 +43,13 @@ private static string GenerateAssemblyManifest(
sb.AppendLine("using SharpLink.Abstractions;");
sb.AppendLine("using SharpLink.Sdk;");
sb.AppendLine();
+ foreach (var codec in codecs.OrderBy(static codec => codec.TypeName, StringComparer.Ordinal))
+ {
+ sb.AppendLine(
+ $"[assembly: SharpLinkGeneratedCodecIdentityAttribute(typeof({codec.TypeName}), {codec.CodecHashHigh.ToString(InvariantCulture)}UL, {codec.CodecHashLow.ToString(InvariantCulture)}UL)]");
+ }
+ if (!codecs.IsDefaultOrEmpty)
+ sb.AppendLine();
sb.AppendLine($"[assembly: SharpLinkGeneratedAssemblyManifestAttribute(typeof(SharpLink.Generated.{manifestTypeName}), 4, 2, \"{EscapeString(ExecutingGeneratorVersion)}\", \"{GeneratedAbiIdentity}\")]");
sb.AppendLine();
sb.AppendLine("namespace SharpLink.Generated;");
From f08bef089a3c521355cfce84c456750b7499af22 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:24:10 +0800
Subject: [PATCH 018/399] feat(generator): consume referenced codec hashes
---
.../RpcGenerator.CodecIdentity.cs | 51 ++++++++++++++++---
1 file changed, 45 insertions(+), 6 deletions(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs b/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
index 4d9790eff..f813b17d0 100644
--- a/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
+++ b/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
@@ -58,6 +58,13 @@ private RpcHashValue GetFinalCodecHash(
return result;
}
+ if (TryGetReferencedGeneratedCodecHash(type, out result))
+ {
+ stack.Remove(typeName);
+ cache[typeName] = result;
+ return result;
+ }
+
if (TryGetCollection(type, out var collectionKind, out var elementType, out var keyType, out var valueType))
{
var parts = new List
@@ -85,10 +92,10 @@ private RpcHashValue GetFinalCodecHash(
}
else
{
- // A non-local generated dependency has no behavior that can be inferred from its CLR
- // name. This fallback keeps the identity deterministic until the referenced manifest
- // hash table is consumed by the downstream-assembly path.
- result = Hashing.GetSemanticHash("codec/v1", "external-generated", typeName);
+ // Compatibility only for referenced assemblies produced before deterministic CodecHash
+ // metadata existed. Current SharpLink-generated dependencies take the exact published
+ // hash path above; this fallback is removed with the remaining legacy identity surface.
+ result = Hashing.GetSemanticHash("codec/v1", "legacy-external-generated", typeName);
}
stack.Remove(typeName);
@@ -96,6 +103,38 @@ private RpcHashValue GetFinalCodecHash(
return result;
}
+ private bool TryGetReferencedGeneratedCodecHash(ITypeSymbol type, out RpcHashValue hash)
+ {
+ var assembly = type.ContainingAssembly;
+ if (assembly is null || SymbolEqualityComparer.Default.Equals(assembly, _compilation.Assembly))
+ {
+ hash = default;
+ return false;
+ }
+
+ foreach (var attribute in assembly.GetAttributes())
+ {
+ if (!IsAttribute(
+ attribute,
+ "SharpLink.Abstractions",
+ "SharpLinkGeneratedCodecIdentityAttribute") ||
+ attribute.ConstructorArguments.Length != 3 ||
+ attribute.ConstructorArguments[0].Value is not ITypeSymbol targetType ||
+ !SymbolEqualityComparer.Default.Equals(targetType, type) ||
+ attribute.ConstructorArguments[1].Value is not ulong high ||
+ attribute.ConstructorArguments[2].Value is not ulong low)
+ {
+ continue;
+ }
+
+ hash = new RpcHashValue(high, low);
+ return true;
+ }
+
+ hash = default;
+ return false;
+ }
+
private RpcHashValue GetGeneratedCodecHash(
GeneratedCodecModel model,
Dictionary cache,
@@ -141,7 +180,7 @@ private RpcHashValue GetGeneratedCodecHash(
break;
case GeneratedMemberKind.Complex:
if (!TryResolveReachableType(member.TypeName, out var memberType))
- parts.Add(Hashing.GetSemanticHash("codec/v1", "external-generated", member.TypeName).ToHex());
+ parts.Add(Hashing.GetSemanticHash("codec/v1", "legacy-external-generated", member.TypeName).ToHex());
else
parts.Add(GetFinalCodecHash(memberType, cache, stack).ToHex());
break;
@@ -169,7 +208,7 @@ void AppendChild(string? childTypeName)
if (TryResolveReachableType(childTypeName, out var childType))
parts.Add(GetFinalCodecHash(childType, cache, stack).ToHex());
else
- parts.Add(Hashing.GetSemanticHash("codec/v1", "external-generated", childTypeName).ToHex());
+ parts.Add(Hashing.GetSemanticHash("codec/v1", "legacy-external-generated", childTypeName).ToHex());
}
}
}
From 905a3055069274bfed6bb73f5d924986c873c4ff Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:26:52 +0800
Subject: [PATCH 019/399] feat(codec): add fixed semantic identity attribute
---
.../Sdk/RpcCodecSemanticIdentityAttribute.cs | 22 +++++++++++++++++++
1 file changed, 22 insertions(+)
create mode 100644 src/SharpLink.Abstractions/Sdk/RpcCodecSemanticIdentityAttribute.cs
diff --git a/src/SharpLink.Abstractions/Sdk/RpcCodecSemanticIdentityAttribute.cs b/src/SharpLink.Abstractions/Sdk/RpcCodecSemanticIdentityAttribute.cs
new file mode 100644
index 000000000..7b857f9aa
--- /dev/null
+++ b/src/SharpLink.Abstractions/Sdk/RpcCodecSemanticIdentityAttribute.cs
@@ -0,0 +1,22 @@
+namespace SharpLink.Sdk;
+
+///
+/// Declares the fixed-width semantic identity of an opaque hand-written Codec or Codec Adapter.
+/// Change this value whenever the implementation's RPC-visible wire semantics change.
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
+public sealed class RpcCodecSemanticIdentityAttribute : Attribute
+{
+ /// Creates one opaque serializer semantic identity.
+ public RpcCodecSemanticIdentityAttribute(ulong high, ulong low)
+ {
+ High = high;
+ Low = low;
+ }
+
+ /// Gets the high 64 bits of the semantic identity.
+ public ulong High { get; }
+
+ /// Gets the low 64 bits of the semantic identity.
+ public ulong Low { get; }
+}
From d2b8c3e2db8e61a5f1896cb027686e5bff2db101 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:27:46 +0800
Subject: [PATCH 020/399] feat(generator): fold opaque semantic hashes into
codec identity
---
.../RpcGenerator.CodecIdentity.cs | 112 +++++++++++++++++-
1 file changed, 110 insertions(+), 2 deletions(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs b/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
index f813b17d0..67d401c87 100644
--- a/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
+++ b/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
@@ -4,6 +4,9 @@ public partial class RpcGenerator
{
private sealed partial class DtoAnalysisState
{
+ private readonly Dictionary _opaqueSemanticIdentityCache =
+ new(StringComparer.Ordinal);
+
internal ImmutableArray BuildFinalCodecHashes(
bool includeSerializable,
bool includeContracts)
@@ -143,15 +146,29 @@ private RpcHashValue GetGeneratedCodecHash(
switch (model.Kind)
{
case GeneratedCodecKind.Custom:
+ if (TryGetOpaqueSemanticIdentity(model.CustomCodecType, out var customIdentity))
+ {
+ return Hashing.GetSemanticHash(
+ "codec/v1",
+ "custom-opaque",
+ customIdentity.ToHex());
+ }
return Hashing.GetSemanticHash(
"codec/v1",
- "custom-opaque",
+ "custom-opaque-legacy",
model.WireFormatId,
model.SchemaId);
case GeneratedCodecKind.Adapter:
+ if (TryGetOpaqueSemanticIdentity(model.AdapterType, out var adapterIdentity))
+ {
+ return Hashing.GetSemanticHash(
+ "codec/v1",
+ "adapter-opaque",
+ adapterIdentity.ToHex());
+ }
return Hashing.GetSemanticHash(
"codec/v1",
- "adapter-opaque",
+ "adapter-opaque-legacy",
model.AdapterId ?? string.Empty,
model.WireFormatId);
case GeneratedCodecKind.Dto:
@@ -214,6 +231,97 @@ void AppendChild(string? childTypeName)
}
}
+ private bool TryGetOpaqueSemanticIdentity(string? implementationTypeName, out RpcHashValue hash)
+ {
+ if (implementationTypeName is null)
+ {
+ hash = default;
+ return false;
+ }
+
+ if (_opaqueSemanticIdentityCache.TryGetValue(implementationTypeName, out var cached))
+ {
+ hash = cached ?? default;
+ return cached.HasValue;
+ }
+
+ var assemblies = new Dictionary(StringComparer.Ordinal)
+ {
+ [_compilation.Assembly.Identity.ToString()] = _compilation.Assembly
+ };
+ var pending = new Queue();
+ pending.Enqueue(_compilation.Assembly);
+ while (pending.Count != 0)
+ {
+ var assembly = pending.Dequeue();
+ if (TryFindNamedType(assembly.GlobalNamespace, implementationTypeName, out var implementationType))
+ {
+ var attribute = implementationType.GetAttributes().FirstOrDefault(static item =>
+ IsAttribute(item, "SharpLink.Sdk", "RpcCodecSemanticIdentityAttribute"));
+ if (attribute is not null &&
+ attribute.ConstructorArguments.Length == 2 &&
+ attribute.ConstructorArguments[0].Value is ulong high &&
+ attribute.ConstructorArguments[1].Value is ulong low)
+ {
+ hash = new RpcHashValue(high, low);
+ _opaqueSemanticIdentityCache[implementationTypeName] = hash;
+ return true;
+ }
+ }
+
+ foreach (var referenced in assembly.Modules.SelectMany(static module => module.ReferencedAssemblySymbols))
+ {
+ var identity = referenced.Identity.ToString();
+ if (assemblies.ContainsKey(identity))
+ continue;
+ assemblies.Add(identity, referenced);
+ pending.Enqueue(referenced);
+ }
+ }
+
+ _opaqueSemanticIdentityCache[implementationTypeName] = null;
+ hash = default;
+ return false;
+ }
+
+ private static bool TryFindNamedType(
+ INamespaceSymbol namespaceSymbol,
+ string typeName,
+ out INamedTypeSymbol type)
+ {
+ foreach (var candidate in namespaceSymbol.GetTypeMembers())
+ {
+ if (TryFindNamedType(candidate, typeName, out type))
+ return true;
+ }
+ foreach (var nestedNamespace in namespaceSymbol.GetNamespaceMembers())
+ {
+ if (TryFindNamedType(nestedNamespace, typeName, out type))
+ return true;
+ }
+ type = null!;
+ return false;
+ }
+
+ private static bool TryFindNamedType(
+ INamedTypeSymbol candidate,
+ string typeName,
+ out INamedTypeSymbol type)
+ {
+ if (string.Equals(GetTypeName(candidate), typeName, StringComparison.Ordinal))
+ {
+ type = candidate;
+ return true;
+ }
+ foreach (var nested in candidate.GetTypeMembers())
+ {
+ if (TryFindNamedType(nested, typeName, out type))
+ return true;
+ }
+ type = null!;
+ return false;
+ }
+
private bool TryResolveReachableType(string typeName, out ITypeSymbol type)
{
var roots = new Dictionary(StringComparer.Ordinal);
From 87143bcd17245c26a236a305fd325536425510b0 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:29:35 +0800
Subject: [PATCH 021/399] test(generator): cover opaque semantic codec identity
---
.../RpcDeterministicIdentityTests.cs | 108 ++++++++++++++++++
1 file changed, 108 insertions(+)
create mode 100644 test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
diff --git a/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs b/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
new file mode 100644
index 000000000..986d13f56
--- /dev/null
+++ b/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace SharpLink.Generator.Tests;
+
+public partial class RpcAnalyzerTests
+{
+ [Test]
+ public Task OpaqueSemanticIdentityShouldIgnoreLegacyStringChanges()
+ {
+ var first = GenerateOpaqueIdentityManifest(
+ wireFormatId: "legacy-wire-a/v1",
+ schemaId: "legacy-schema-a/v1",
+ semanticHigh: 0x0102030405060708UL,
+ semanticLow: 0x1112131415161718UL);
+ var second = GenerateOpaqueIdentityManifest(
+ wireFormatId: "legacy-wire-b/v9",
+ schemaId: "legacy-schema-b/v9",
+ semanticHigh: 0x0102030405060708UL,
+ semanticLow: 0x1112131415161718UL);
+
+ Ensure(
+ ExtractGeneratedCodecIdentity(first) == ExtractGeneratedCodecIdentity(second),
+ "fixed opaque semantic identity must replace legacy WireFormatId/SchemaId as the CodecHash input");
+ Ensure(
+ ExtractGeneratedRpcAssemblyHash(first) == ExtractGeneratedRpcAssemblyHash(second),
+ "legacy custom-codec strings must not perturb RpcAssemblyHash once fixed semantic identity is present");
+ return Task.CompletedTask;
+ }
+
+ [Test]
+ public Task OpaqueSemanticIdentityChangeShouldChangeFinalRpcIdentity()
+ {
+ var first = GenerateOpaqueIdentityManifest(
+ wireFormatId: "same-wire/v1",
+ schemaId: "same-schema/v1",
+ semanticHigh: 0x0102030405060708UL,
+ semanticLow: 0x1112131415161718UL);
+ var second = GenerateOpaqueIdentityManifest(
+ wireFormatId: "same-wire/v1",
+ schemaId: "same-schema/v1",
+ semanticHigh: 0x0102030405060708UL,
+ semanticLow: 0x2112131415161718UL);
+
+ Ensure(
+ ExtractGeneratedCodecIdentity(first) != ExtractGeneratedCodecIdentity(second),
+ "changing opaque serializer semantics must change CodecHash");
+ Ensure(
+ ExtractGeneratedRpcAssemblyHash(first) != ExtractGeneratedRpcAssemblyHash(second),
+ "changing a payload CodecHash must flow through MethodHash/ContractHash into RpcAssemblyHash");
+ return Task.CompletedTask;
+ }
+
+ private static string GenerateOpaqueIdentityManifest(
+ string wireFormatId,
+ string schemaId,
+ ulong semanticHigh,
+ ulong semanticLow)
+ {
+ var source = $$"""
+using System;
+using System.Buffers;
+using System.Threading;
+using System.Threading.Tasks;
+using SharpLink.Abstractions;
+using SharpLink.Sdk;
+
+[RpcSerializable]
+[RpcCodec(typeof(OpaquePayloadCodec))]
+public sealed class OpaquePayload
+{
+ public int Value { get; set; }
+}
+
+[RpcCodecImplementation("{{wireFormatId}}", "{{schemaId}}")]
+[RpcCodecSemanticIdentity({{semanticHigh}}UL, {{semanticLow}}UL)]
+public sealed class OpaquePayloadCodec : IRpcCodec
+{
+ public void Serialize(in OpaquePayload value, IBufferWriter buffer) { }
+ public OpaquePayload Deserialize(in ReadOnlySequence buffer) => new();
+}
+
+[RpcContract]
+public interface IOpaqueIdentityContract : IService
+{
+ ValueTask Echo(OpaquePayload value, CancellationToken cancellationToken);
+}
+""";
+
+ return RunGeneratorAndGetSources(source)
+ .Single(static generated =>
+ generated.Contains("ISharpLinkGeneratedAssemblyManifest", StringComparison.Ordinal));
+ }
+
+ private static string ExtractGeneratedCodecIdentity(string manifest)
+ => manifest.Split('\n')
+ .Single(static line =>
+ line.Contains(
+ "SharpLinkGeneratedCodecIdentityAttribute(typeof(global::OpaquePayload)",
+ StringComparison.Ordinal))
+ .Trim();
+
+ private static string ExtractGeneratedRpcAssemblyHash(string manifest)
+ => manifest.Split('\n')
+ .Single(static line => line.Contains("public RpcHash128 RpcAssemblyHash =>", StringComparison.Ordinal))
+ .Trim();
+}
From 26d8697ff6fc6f0defb5cd69b54623143093b416 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:33:42 +0800
Subject: [PATCH 022/399] refactor(runtime): require deterministic codec hashes
---
.../SharpLinkGeneratedManifestStructureValidator.cs | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/SharpLink.Runtime/SharpLinkGeneratedManifestStructureValidator.cs b/src/SharpLink.Runtime/SharpLinkGeneratedManifestStructureValidator.cs
index e6d13f0c1..89ebf289d 100644
--- a/src/SharpLink.Runtime/SharpLinkGeneratedManifestStructureValidator.cs
+++ b/src/SharpLink.Runtime/SharpLinkGeneratedManifestStructureValidator.cs
@@ -10,6 +10,11 @@ internal static void Validate(ISharpLinkGeneratedAssemblyManifest manifest)
var ownerAssembly = manifest.OwnerAssembly ??
throw new InvalidOperationException("Generated manifest has no owner assembly.");
+ if (manifest.RpcAssemblyHash.IsEmpty)
+ {
+ throw new InvalidOperationException(
+ $"Generated manifest '{ownerAssembly.FullName}' has no deterministic RPC assembly identity.");
+ }
var contracts = manifest.Contracts ??
throw new InvalidOperationException($"Generated manifest '{ownerAssembly.FullName}' has a null Contract table.");
var codecs = manifest.Codecs ??
@@ -60,10 +65,10 @@ private static void ValidateFactories(
var targetType = factory.TargetType ??
throw new InvalidOperationException(
$"Generated manifest '{ownerAssembly.FullName}' contains a Codec factory without a target Type in the {scope} graph.");
- if (string.IsNullOrWhiteSpace(factory.SchemaId) || string.IsNullOrWhiteSpace(factory.WireFormatId))
+ if (factory.CodecHash.IsEmpty)
{
throw new InvalidOperationException(
- $"Generated manifest '{ownerAssembly.FullName}' contains incomplete Codec identity for '{targetType.FullName}' in the {scope} graph.");
+ $"Generated manifest '{ownerAssembly.FullName}' contains no deterministic CodecHash for '{targetType.FullName}' in the {scope} graph.");
}
if (!targets.Add(targetType))
{
From 580dd87035f0c9a9c7a1caea1f9564dd94a7a2ac Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:36:08 +0800
Subject: [PATCH 023/399] refactor(generator): remove codec identity fallbacks
---
.../RpcGenerator.CodecIdentity.cs | 59 +++++++++----------
1 file changed, 28 insertions(+), 31 deletions(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs b/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
index 67d401c87..7b9cecd91 100644
--- a/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
+++ b/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
@@ -95,10 +95,8 @@ private RpcHashValue GetFinalCodecHash(
}
else
{
- // Compatibility only for referenced assemblies produced before deterministic CodecHash
- // metadata existed. Current SharpLink-generated dependencies take the exact published
- // hash path above; this fallback is removed with the remaining legacy identity surface.
- result = Hashing.GetSemanticHash("codec/v1", "legacy-external-generated", typeName);
+ throw new InvalidOperationException(
+ $"Final RPC Codec graph cannot resolve deterministic CodecHash metadata for referenced payload '{typeName}'. Rebuild the referenced SharpLink assembly with deterministic identity generation enabled.");
}
stack.Remove(typeName);
@@ -146,31 +144,15 @@ private RpcHashValue GetGeneratedCodecHash(
switch (model.Kind)
{
case GeneratedCodecKind.Custom:
- if (TryGetOpaqueSemanticIdentity(model.CustomCodecType, out var customIdentity))
- {
- return Hashing.GetSemanticHash(
- "codec/v1",
- "custom-opaque",
- customIdentity.ToHex());
- }
return Hashing.GetSemanticHash(
"codec/v1",
- "custom-opaque-legacy",
- model.WireFormatId,
- model.SchemaId);
+ "custom-opaque",
+ GetRequiredOpaqueSemanticIdentity(model.CustomCodecType, "custom Codec").ToHex());
case GeneratedCodecKind.Adapter:
- if (TryGetOpaqueSemanticIdentity(model.AdapterType, out var adapterIdentity))
- {
- return Hashing.GetSemanticHash(
- "codec/v1",
- "adapter-opaque",
- adapterIdentity.ToHex());
- }
return Hashing.GetSemanticHash(
"codec/v1",
- "adapter-opaque-legacy",
- model.AdapterId ?? string.Empty,
- model.WireFormatId);
+ "adapter-opaque",
+ GetRequiredOpaqueSemanticIdentity(model.AdapterType, "Codec Adapter").ToHex());
case GeneratedCodecKind.Dto:
{
var parts = new List
@@ -197,9 +179,11 @@ private RpcHashValue GetGeneratedCodecHash(
break;
case GeneratedMemberKind.Complex:
if (!TryResolveReachableType(member.TypeName, out var memberType))
- parts.Add(Hashing.GetSemanticHash("codec/v1", "legacy-external-generated", member.TypeName).ToHex());
- else
- parts.Add(GetFinalCodecHash(memberType, cache, stack).ToHex());
+ {
+ throw new InvalidOperationException(
+ $"Final RPC Codec graph cannot resolve child payload '{member.TypeName}' while hashing '{model.TypeName}'.");
+ }
+ parts.Add(GetFinalCodecHash(memberType, cache, stack).ToHex());
break;
}
}
@@ -222,15 +206,28 @@ void AppendChild(string? childTypeName)
{
if (childTypeName is null)
return;
- if (TryResolveReachableType(childTypeName, out var childType))
- parts.Add(GetFinalCodecHash(childType, cache, stack).ToHex());
- else
- parts.Add(Hashing.GetSemanticHash("codec/v1", "legacy-external-generated", childTypeName).ToHex());
+ if (!TryResolveReachableType(childTypeName, out var childType))
+ {
+ throw new InvalidOperationException(
+ $"Final RPC Codec graph cannot resolve child payload '{childTypeName}' while hashing '{model.TypeName}'.");
+ }
+ parts.Add(GetFinalCodecHash(childType, cache, stack).ToHex());
}
}
}
}
+ private RpcHashValue GetRequiredOpaqueSemanticIdentity(
+ string? implementationTypeName,
+ string implementationKind)
+ {
+ if (TryGetOpaqueSemanticIdentity(implementationTypeName, out var hash))
+ return hash;
+
+ throw new InvalidOperationException(
+ $"Opaque {implementationKind} '{implementationTypeName ?? ""}' must declare [RpcCodecSemanticIdentity(high, low)].");
+ }
+
private bool TryGetOpaqueSemanticIdentity(string? implementationTypeName, out RpcHashValue hash)
{
if (implementationTypeName is null)
From 16aa1a24f37e0e00094e4851abd6d2fcbdfb8353 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:37:10 +0800
Subject: [PATCH 024/399] test(generator): make opaque identity tests
semantic-only
---
.../RpcDeterministicIdentityTests.cs | 27 +++++++++----------
1 file changed, 12 insertions(+), 15 deletions(-)
diff --git a/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs b/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
index 986d13f56..b29000514 100644
--- a/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
+++ b/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
@@ -7,25 +7,23 @@ namespace SharpLink.Generator.Tests;
public partial class RpcAnalyzerTests
{
[Test]
- public Task OpaqueSemanticIdentityShouldIgnoreLegacyStringChanges()
+ public Task OpaqueSemanticIdentityShouldIgnoreUnrelatedImplementationChanges()
{
var first = GenerateOpaqueIdentityManifest(
- wireFormatId: "legacy-wire-a/v1",
- schemaId: "legacy-schema-a/v1",
+ implementationMarker: "first-build",
semanticHigh: 0x0102030405060708UL,
semanticLow: 0x1112131415161718UL);
var second = GenerateOpaqueIdentityManifest(
- wireFormatId: "legacy-wire-b/v9",
- schemaId: "legacy-schema-b/v9",
+ implementationMarker: "second-build",
semanticHigh: 0x0102030405060708UL,
semanticLow: 0x1112131415161718UL);
Ensure(
ExtractGeneratedCodecIdentity(first) == ExtractGeneratedCodecIdentity(second),
- "fixed opaque semantic identity must replace legacy WireFormatId/SchemaId as the CodecHash input");
+ "opaque CodecHash must be controlled by its fixed semantic identity rather than unrelated implementation details");
Ensure(
ExtractGeneratedRpcAssemblyHash(first) == ExtractGeneratedRpcAssemblyHash(second),
- "legacy custom-codec strings must not perturb RpcAssemblyHash once fixed semantic identity is present");
+ "unrelated implementation changes must not perturb RpcAssemblyHash when RPC semantics are unchanged");
return Task.CompletedTask;
}
@@ -33,13 +31,11 @@ public Task OpaqueSemanticIdentityShouldIgnoreLegacyStringChanges()
public Task OpaqueSemanticIdentityChangeShouldChangeFinalRpcIdentity()
{
var first = GenerateOpaqueIdentityManifest(
- wireFormatId: "same-wire/v1",
- schemaId: "same-schema/v1",
+ implementationMarker: "same-implementation",
semanticHigh: 0x0102030405060708UL,
semanticLow: 0x1112131415161718UL);
var second = GenerateOpaqueIdentityManifest(
- wireFormatId: "same-wire/v1",
- schemaId: "same-schema/v1",
+ implementationMarker: "same-implementation",
semanticHigh: 0x0102030405060708UL,
semanticLow: 0x2112131415161718UL);
@@ -53,8 +49,7 @@ public Task OpaqueSemanticIdentityChangeShouldChangeFinalRpcIdentity()
}
private static string GenerateOpaqueIdentityManifest(
- string wireFormatId,
- string schemaId,
+ string implementationMarker,
ulong semanticHigh,
ulong semanticLow)
{
@@ -73,11 +68,13 @@ public sealed class OpaquePayload
public int Value { get; set; }
}
-[RpcCodecImplementation("{{wireFormatId}}", "{{schemaId}}")]
+[RpcCodecImplementation("opaque-test-wire/v1", "opaque-test-schema/v1")]
[RpcCodecSemanticIdentity({{semanticHigh}}UL, {{semanticLow}}UL)]
public sealed class OpaquePayloadCodec : IRpcCodec
{
- public void Serialize(in OpaquePayload value, IBufferWriter buffer) { }
+ private const string ImplementationMarker = "{{implementationMarker}}";
+
+ public void Serialize(in OpaquePayload value, IBufferWriter buffer) { _ = ImplementationMarker; }
public OpaquePayload Deserialize(in ReadOnlySequence buffer) => new();
}
From 48d52f2d7627d1f9f1c61ec55cdcde1c6bf58914 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:38:17 +0800
Subject: [PATCH 025/399] feat(sharppack): declare opaque codec semantic
identity
---
src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs b/src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs
index 93a2d60be..893091ef6 100644
--- a/src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs
+++ b/src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs
@@ -30,6 +30,7 @@ public static IRpcCodec Create<
/// SharpPack integration selected by generated Manifest metadata.
[EditorBrowsable(EditorBrowsableState.Never)]
+[RpcCodecSemanticIdentity(0x3fd7540d55dfa977UL, 0xbb67b4932c1a5249UL)]
public sealed class SharpPackRpcCodecAdapter : IRpcCodecAdapter
{
/// The stable Adapter implementation identity.
From ed740076bd1d448142f9e7d50172dbc3b6797c68 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:39:50 +0800
Subject: [PATCH 026/399] refactor(runtime): remove adapter wire identity
checks
---
.../Codec/RpcCodecProvider.cs | 21 +++++++++----------
1 file changed, 10 insertions(+), 11 deletions(-)
diff --git a/src/SharpLink.Runtime/Codec/RpcCodecProvider.cs b/src/SharpLink.Runtime/Codec/RpcCodecProvider.cs
index 1b8b8cc4d..d120c2de1 100644
--- a/src/SharpLink.Runtime/Codec/RpcCodecProvider.cs
+++ b/src/SharpLink.Runtime/Codec/RpcCodecProvider.cs
@@ -346,7 +346,6 @@ internal static RpcGeneratedManifestRegistration Create(
var scopeByAdapterId = new Dictionary(StringComparer.Ordinal);
var allFactories = manifest.Codecs.Concat(manifest.ContractCodecs).ToArray();
foreach (var factory in allFactories.OrderBy(static factory => factory.AdapterId, StringComparer.Ordinal)
- .ThenBy(static factory => factory.WireFormatId, StringComparer.Ordinal)
.ThenBy(static factory => factory.TargetType.FullName, StringComparer.Ordinal))
{
ValidateFactory(factory);
@@ -357,11 +356,10 @@ internal static RpcGeneratedManifestRegistration Create(
ValidateAdapter(factory, adapter);
if (scopeByAdapterId.TryGetValue(factory.AdapterId, out var existing))
{
- if (existing.Adapter.GetType() != adapter.GetType() ||
- !string.Equals(existing.WireFormatId, factory.WireFormatId, StringComparison.Ordinal))
+ if (existing.Adapter.GetType() != adapter.GetType())
{
throw new InvalidOperationException(
- $"Adapter '{factory.AdapterId}' has inconsistent implementation or wire-format metadata in manifest '{manifest.OwnerAssembly.FullName}'.");
+ $"Adapter '{factory.AdapterId}' has inconsistent implementations in manifest '{manifest.OwnerAssembly.FullName}'.");
}
continue;
}
@@ -370,7 +368,7 @@ internal static RpcGeneratedManifestRegistration Create(
$"Adapter '{factory.AdapterId}' returned a null scope.");
scopes.Add(scope);
scopeByAdapterId.Add(factory.AdapterId,
- new AdapterScopeRegistration(adapter, factory.WireFormatId, scope));
+ new AdapterScopeRegistration(adapter, scope));
}
var ownerBox = new OwnerBox();
@@ -435,8 +433,11 @@ private static void ValidateFactory(IRpcGeneratedCodecFactory factory)
{
ArgumentNullException.ThrowIfNull(factory);
ArgumentNullException.ThrowIfNull(factory.TargetType);
- ArgumentException.ThrowIfNullOrWhiteSpace(factory.SchemaId);
- ArgumentException.ThrowIfNullOrWhiteSpace(factory.WireFormatId);
+ if (factory.CodecHash.IsEmpty)
+ {
+ throw new InvalidOperationException(
+ $"Generated Codec factory for '{factory.TargetType.FullName}' has no deterministic CodecHash.");
+ }
var hasAdapterId = factory.AdapterId is not null;
var hasAdapter = factory.Adapter is not null;
@@ -450,11 +451,10 @@ private static void ValidateFactory(IRpcGeneratedCodecFactory factory)
private static void ValidateAdapter(IRpcGeneratedCodecFactory factory, IRpcCodecAdapter adapter)
{
- if (!string.Equals(adapter.AdapterId, factory.AdapterId, StringComparison.Ordinal) ||
- !string.Equals(adapter.WireFormatId, factory.WireFormatId, StringComparison.Ordinal))
+ if (!string.Equals(adapter.AdapterId, factory.AdapterId, StringComparison.Ordinal))
{
throw new InvalidOperationException(
- $"Codec adapter '{adapter.GetType().FullName}' runtime identity does not match its generated registration metadata.");
+ $"Codec adapter '{adapter.GetType().FullName}' lifecycle identity does not match its generated registration metadata.");
}
}
@@ -489,7 +489,6 @@ public void Dispose()
private sealed record AdapterScopeRegistration(
IRpcCodecAdapter Adapter,
- string WireFormatId,
IRpcCodecAdapterScope Scope);
internal sealed class OwnerBox
From e29144bcfffccd790ff20c177f250face705a441 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:40:01 +0800
Subject: [PATCH 027/399] refactor(codec): remove adapter wire identity API
---
src/SharpLink.Abstractions/IRpcCodecAdapter.cs | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/SharpLink.Abstractions/IRpcCodecAdapter.cs b/src/SharpLink.Abstractions/IRpcCodecAdapter.cs
index d9410c301..a0c110030 100644
--- a/src/SharpLink.Abstractions/IRpcCodecAdapter.cs
+++ b/src/SharpLink.Abstractions/IRpcCodecAdapter.cs
@@ -6,9 +6,6 @@ public interface IRpcCodecAdapter
/// Gets the implementation and lifecycle identity.
string AdapterId { get; }
- /// Gets the stable binary wire-format identity.
- string WireFormatId { get; }
-
/// Creates isolated state for one runtime Context and generated manifest.
IRpcCodecAdapterScope CreateScope();
}
From 507a53b11c62e953a820338e65f21fce9554a59f Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:42:58 +0800
Subject: [PATCH 028/399] refactor(generator): require fixed opaque semantic
identity
---
.../RpcGenerator.DtoAnalysis.cs | 70 +++++++++----------
1 file changed, 32 insertions(+), 38 deletions(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.DtoAnalysis.cs b/src/SharpLink.Generator/RpcGenerator.DtoAnalysis.cs
index e12c11594..aaa01fac7 100644
--- a/src/SharpLink.Generator/RpcGenerator.DtoAnalysis.cs
+++ b/src/SharpLink.Generator/RpcGenerator.DtoAnalysis.cs
@@ -20,14 +20,13 @@ private static IEnumerable GetCodecDependencies(GeneratedCodecModel code
private static bool HasSameCodecDefinition(GeneratedCodecModel left, GeneratedCodecModel right)
{
if (!string.Equals(left.TypeName, right.TypeName, StringComparison.Ordinal) ||
- !string.Equals(left.SchemaId, right.SchemaId, StringComparison.Ordinal) ||
left.Kind != right.Kind || left.IsReferenceType != right.IsReferenceType ||
!string.Equals(left.ElementType, right.ElementType, StringComparison.Ordinal) ||
!string.Equals(left.KeyType, right.KeyType, StringComparison.Ordinal) ||
!string.Equals(left.ValueType, right.ValueType, StringComparison.Ordinal) ||
+ !string.Equals(left.CustomCodecType, right.CustomCodecType, StringComparison.Ordinal) ||
!string.Equals(left.AdapterType, right.AdapterType, StringComparison.Ordinal) ||
!string.Equals(left.AdapterId, right.AdapterId, StringComparison.Ordinal) ||
- !string.Equals(left.WireFormatId, right.WireFormatId, StringComparison.Ordinal) ||
!left.ConstructorMembers.SequenceEqual(right.ConstructorMembers, StringComparer.Ordinal) ||
!left.AssemblyDependencies.SequenceEqual(right.AssemblyDependencies, StringComparer.Ordinal) ||
left.Members.Length != right.Members.Length)
@@ -235,14 +234,13 @@ private void CollectAdapterRegistrations()
.OrderBy(static attribute => attribute.ToString(), StringComparer.Ordinal))
{
var location = attribute.ApplicationSyntaxReference?.GetSyntax(_cancellationToken).GetLocation() ?? Location.None;
- if (attribute.ConstructorArguments.Length != 3 ||
+ if (attribute.ConstructorArguments.Length != 2 ||
attribute.ConstructorArguments[0].Value is not INamedTypeSymbol adapterType ||
attribute.ConstructorArguments[1].Value is not string adapterId ||
- attribute.ConstructorArguments[2].Value is not string wireFormatId ||
- !IsStableIdentity(adapterId) || !IsStableIdentity(wireFormatId))
+ !IsStableIdentity(adapterId))
{
Report(DtoDiagnosticKind.AdapterRegistrationInvalid, assembly,
- "registration requires a concrete Adapter type and non-empty stable ASCII Adapter/Wire Format IDs", location);
+ "registration requires a concrete Adapter type and non-empty stable ASCII AdapterId", location);
continue;
}
@@ -258,6 +256,12 @@ attribute.ConstructorArguments[2].Value is not string wireFormatId ||
"Adapter must implement IRpcCodecAdapter, be public sealed, and expose a public parameterless constructor", location);
continue;
}
+ if (!HasValidOpaqueSemanticIdentity(adapterType))
+ {
+ Report(DtoDiagnosticKind.AdapterRegistrationInvalid, adapterType,
+ "Adapter must declare a non-zero fixed semantic identity via [RpcCodecSemanticIdentity(high, low)]", location);
+ continue;
+ }
if (selector is not null && !InheritsFromAttribute(selector))
{
Report(DtoDiagnosticKind.AdapterRegistrationInvalid, selector,
@@ -268,23 +272,20 @@ attribute.ConstructorArguments[2].Value is not string wireFormatId ||
var registration = new AdapterRegistration(
adapterType,
adapterId,
- wireFormatId,
selector,
location);
if (_adaptersByType.TryGetValue(adapterType, out var existingType) &&
- (!string.Equals(existingType.AdapterId, adapterId, StringComparison.Ordinal) ||
- !string.Equals(existingType.WireFormatId, wireFormatId, StringComparison.Ordinal)))
+ !string.Equals(existingType.AdapterId, adapterId, StringComparison.Ordinal))
{
Report(DtoDiagnosticKind.AdapterIdentityConflict, adapterType,
- "the same Adapter type has inconsistent Adapter or Wire Format IDs", location);
+ "the same Adapter type has inconsistent Adapter IDs", location);
continue;
}
if (adapterIds.TryGetValue(adapterId, out var existingId) &&
- (!SymbolEqualityComparer.Default.Equals(existingId.AdapterType, adapterType) ||
- !string.Equals(existingId.WireFormatId, wireFormatId, StringComparison.Ordinal)))
+ !SymbolEqualityComparer.Default.Equals(existingId.AdapterType, adapterType))
{
Report(DtoDiagnosticKind.AdapterIdentityConflict, adapterType,
- $"Adapter ID '{adapterId}' is declared by inconsistent types or Wire Format IDs", location);
+ $"Adapter ID '{adapterId}' is declared by inconsistent implementation types", location);
continue;
}
if (selector is not null && _adaptersBySelector.TryGetValue(selector, out var existingSelector) &&
@@ -341,7 +342,7 @@ private void Visit(ITypeSymbol type, List stack, int depth)
_models[typeName] = new GeneratedCodecModel(
typeName,
GetCodecName(typeName, _contractMode),
- GetSchemaId(typeName, customCodec.SchemaId),
+ GetSchemaId(typeName, "custom|" + GetTypeName(customCodec.CodecType)),
GeneratedCodecKind.Custom,
type.IsReferenceType,
ImmutableArray.Empty,
@@ -352,7 +353,7 @@ private void Visit(ITypeSymbol type, List stack, int depth)
GetTypeName(customCodec.CodecType),
null,
null,
- customCodec.WireFormatId,
+ string.Empty,
GetAssemblyDependencies([type]),
type.Locations.FirstOrDefault());
}
@@ -889,8 +890,7 @@ private bool TryResolveExplicitBinding(
private static bool AdapterRegistrationsEqual(AdapterRegistration left, AdapterRegistration right)
=> SymbolEqualityComparer.Default.Equals(left.AdapterType, right.AdapterType) &&
- string.Equals(left.AdapterId, right.AdapterId, StringComparison.Ordinal) &&
- string.Equals(left.WireFormatId, right.WireFormatId, StringComparison.Ordinal);
+ string.Equals(left.AdapterId, right.AdapterId, StringComparison.Ordinal);
private static bool ImplementsRpcCodecAdapter(INamedTypeSymbol type)
=> type.AllInterfaces.Any(static item =>
@@ -963,14 +963,7 @@ private static bool IsValidCustomCodec(ITypeSymbol codecType, ITypeSymbol target
return false;
}
- var identity = named.GetAttributes().FirstOrDefault(static attribute =>
- IsAttribute(attribute, "SharpLink.Sdk", "RpcCodecImplementationAttribute"));
- return identity is not null &&
- identity.ConstructorArguments.Length == 2 &&
- identity.ConstructorArguments[0].Value is string wireFormatId &&
- identity.ConstructorArguments[1].Value is string schemaId &&
- IsStableIdentity(wireFormatId) &&
- IsStableIdentity(schemaId);
+ return HasValidOpaqueSemanticIdentity(named);
}
private CustomCodecRegistration? ValidateCustomCodec(
@@ -1010,21 +1003,14 @@ identity.ConstructorArguments[1].Value is string schemaId &&
return null;
}
- var identity = named.GetAttributes().FirstOrDefault(static attribute =>
- IsAttribute(attribute, "SharpLink.Sdk", "RpcCodecImplementationAttribute"));
- if (identity is null ||
- identity.ConstructorArguments.Length != 2 ||
- identity.ConstructorArguments[0].Value is not string wireFormatId ||
- identity.ConstructorArguments[1].Value is not string schemaId ||
- !IsStableIdentity(wireFormatId) ||
- !IsStableIdentity(schemaId))
+ if (!HasValidOpaqueSemanticIdentity(named))
{
Report(DtoDiagnosticKind.CustomCodecIdentityInvalid, codecType,
- "custom Codec must declare stable ASCII WireFormatId and SchemaId via [RpcCodecImplementation]", location);
+ "custom Codec must declare a non-zero fixed semantic identity via [RpcCodecSemanticIdentity(high, low)]", location);
return null;
}
- return new CustomCodecRegistration(named, wireFormatId, schemaId, location);
+ return new CustomCodecRegistration(named, location);
}
private bool TrySelectCustomCodec(ITypeSymbol type, out CustomCodecRegistration? selected)
@@ -1098,6 +1084,17 @@ private static bool InheritsFromAttribute(ITypeSymbol type)
return false;
}
+ private static bool HasValidOpaqueSemanticIdentity(INamedTypeSymbol type)
+ {
+ var identity = type.GetAttributes().FirstOrDefault(static attribute =>
+ IsAttribute(attribute, "SharpLink.Sdk", "RpcCodecSemanticIdentityAttribute"));
+ return identity is not null &&
+ identity.ConstructorArguments.Length == 2 &&
+ identity.ConstructorArguments[0].Value is ulong high &&
+ identity.ConstructorArguments[1].Value is ulong low &&
+ (high | low) != 0;
+ }
+
private static bool IsStableIdentity(string value)
{
if (string.IsNullOrWhiteSpace(value))
@@ -1435,14 +1432,11 @@ private sealed record ExplicitBindingCandidate(
private sealed record AdapterRegistration(
INamedTypeSymbol AdapterType,
string AdapterId,
- string WireFormatId,
ITypeSymbol? SelectorType,
Location Location);
private sealed record CustomCodecRegistration(
INamedTypeSymbol CodecType,
- string WireFormatId,
- string SchemaId,
Location Location);
}
}
From 2be8ec91262aa5b16ad50bd9dea34db43a092207 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:43:32 +0800
Subject: [PATCH 029/399] refactor(generator): remove adapter wire metadata
from routing
---
src/SharpLink.Generator/RpcGenerator.CodecRoutes.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.CodecRoutes.cs b/src/SharpLink.Generator/RpcGenerator.CodecRoutes.cs
index 7a77af559..33018f5cd 100644
--- a/src/SharpLink.Generator/RpcGenerator.CodecRoutes.cs
+++ b/src/SharpLink.Generator/RpcGenerator.CodecRoutes.cs
@@ -197,7 +197,7 @@ private bool HasMatchingAssemblyRoute(ITypeSymbol type)
private void AddAdapterModel(ITypeSymbol type, string typeName, AdapterRegistration adapter)
{
- var schema = $"adapter|{adapter.AdapterId}|{GetTypeName(adapter.AdapterType)}|{adapter.WireFormatId}";
+ var schema = $"adapter|{adapter.AdapterId}|{GetTypeName(adapter.AdapterType)}";
_models[typeName] = new GeneratedCodecModel(
typeName,
GetCodecName(typeName, _contractMode),
@@ -212,7 +212,7 @@ private void AddAdapterModel(ITypeSymbol type, string typeName, AdapterRegistrat
null,
GetTypeName(adapter.AdapterType),
adapter.AdapterId,
- adapter.WireFormatId,
+ string.Empty,
GetAssemblyDependencies([type]),
type.Locations.FirstOrDefault());
}
From b78a085d862bb1e29d328e21ec46ed5f2e425f1a Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:44:46 +0800
Subject: [PATCH 030/399] refactor(codec): remove adapter wire identity
registration
---
.../Sdk/RpcCodecAdapterRegistrationAttribute.cs | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/src/SharpLink.Abstractions/Sdk/RpcCodecAdapterRegistrationAttribute.cs b/src/SharpLink.Abstractions/Sdk/RpcCodecAdapterRegistrationAttribute.cs
index a25c0ebb6..99e89e366 100644
--- a/src/SharpLink.Abstractions/Sdk/RpcCodecAdapterRegistrationAttribute.cs
+++ b/src/SharpLink.Abstractions/Sdk/RpcCodecAdapterRegistrationAttribute.cs
@@ -1,15 +1,14 @@
namespace SharpLink.Sdk;
-/// Declares the compile-time identity of a serializer Codec adapter.
+/// Declares one serializer Codec adapter for source-generated selection and lifecycle ownership.
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class RpcCodecAdapterRegistrationAttribute : Attribute
{
/// Creates an adapter registration.
- public RpcCodecAdapterRegistrationAttribute(Type adapterType, string adapterId, string wireFormatId)
+ public RpcCodecAdapterRegistrationAttribute(Type adapterType, string adapterId)
{
AdapterType = adapterType ?? throw new ArgumentNullException(nameof(adapterType));
AdapterId = adapterId ?? throw new ArgumentNullException(nameof(adapterId));
- WireFormatId = wireFormatId ?? throw new ArgumentNullException(nameof(wireFormatId));
}
/// Gets the public adapter implementation type.
@@ -18,9 +17,6 @@ public RpcCodecAdapterRegistrationAttribute(Type adapterType, string adapterId,
/// Gets the implementation and lifecycle identity.
public string AdapterId { get; }
- /// Gets the stable binary wire-format identity.
- public string WireFormatId { get; }
-
/// Gets or initializes the serializer attribute that selects this adapter.
public Type? SelectorAttributeType { get; init; }
}
From 52f2ed5a661b5fc4b38c71c7189d5e57f8f970e8 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:44:56 +0800
Subject: [PATCH 031/399] refactor(codec): remove legacy custom codec identity
attribute
---
.../Sdk/RpcCodecImplementationAttribute.cs | 19 -------------------
1 file changed, 19 deletions(-)
delete mode 100644 src/SharpLink.Abstractions/Sdk/RpcCodecImplementationAttribute.cs
diff --git a/src/SharpLink.Abstractions/Sdk/RpcCodecImplementationAttribute.cs b/src/SharpLink.Abstractions/Sdk/RpcCodecImplementationAttribute.cs
deleted file mode 100644
index 3ad7fa9a4..000000000
--- a/src/SharpLink.Abstractions/Sdk/RpcCodecImplementationAttribute.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-namespace SharpLink.Sdk;
-
-/// Declares the stable wire-format and schema identity of a hand-written RPC Codec implementation.
-[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
-public sealed class RpcCodecImplementationAttribute : Attribute
-{
- /// Creates a custom Codec implementation identity.
- public RpcCodecImplementationAttribute(string wireFormatId, string schemaId)
- {
- WireFormatId = wireFormatId ?? throw new ArgumentNullException(nameof(wireFormatId));
- SchemaId = schemaId ?? throw new ArgumentNullException(nameof(schemaId));
- }
-
- /// Gets the stable binary wire-format identity.
- public string WireFormatId { get; }
-
- /// Gets the deterministic payload schema identity.
- public string SchemaId { get; }
-}
From 34a9befb23ec066de1c2bbed38b25e5040fd966d Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 13:45:29 +0800
Subject: [PATCH 032/399] test(generator): remove legacy codec identity usage
---
test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs b/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
index b29000514..e931d334a 100644
--- a/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
+++ b/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
@@ -68,7 +68,6 @@ public sealed class OpaquePayload
public int Value { get; set; }
}
-[RpcCodecImplementation("opaque-test-wire/v1", "opaque-test-schema/v1")]
[RpcCodecSemanticIdentity({{semanticHigh}}UL, {{semanticLow}}UL)]
public sealed class OpaquePayloadCodec : IRpcCodec
{
From f6b2b9afc187adcbcbc58a7340f1ea039944e159 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:07:25 +0800
Subject: [PATCH 033/399] fix: remove legacy SharpPack wire identity
registration
---
src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs | 9 +--------
1 file changed, 1 insertion(+), 8 deletions(-)
diff --git a/src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs b/src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs
index 893091ef6..099fafdcd 100644
--- a/src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs
+++ b/src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs
@@ -9,7 +9,6 @@
[assembly: RpcCodecAdapterRegistration(
typeof(SharpLink.Serializer.SharpPack.SharpPackRpcCodecAdapter),
SharpLink.Serializer.SharpPack.SharpPackRpcCodecAdapter.AdapterIdentity,
- SharpLink.Serializer.SharpPack.SharpPackRpcCodecAdapter.WireFormatIdentity,
SelectorAttributeType = typeof(SharpPackableAttribute))]
namespace SharpLink.Serializer.SharpPack;
@@ -36,15 +35,9 @@ public sealed class SharpPackRpcCodecAdapter : IRpcCodecAdapter
/// The stable Adapter implementation identity.
public const string AdapterIdentity = "sharplink.serializer.sharppack/v1";
- /// The MemoryPack-compatible wire-format identity.
- public const string WireFormatIdentity = "memorypack-binary/v1";
-
///
public string AdapterId => AdapterIdentity;
- ///
- public string WireFormatId => WireFormatIdentity;
-
///
public IRpcCodecAdapterScope CreateScope() => new SharpPackRpcCodecAdapterScope();
}
@@ -177,4 +170,4 @@ internal readonly struct SharpPackBufferWriter(IBufferWriter writer) : IBu
public void Advance(int count) => writer.Advance(count);
public Memory GetMemory(int sizeHint = 0) => writer.GetMemory(sizeHint);
public Span GetSpan(int sizeHint = 0) => writer.GetSpan(sizeHint);
-}
+}
\ No newline at end of file
From a2f647a877b5ced315275e75b7b28674ba2e1fc2 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:09:11 +0800
Subject: [PATCH 034/399] fix: validate canonical custom codecs by semantic
identity
---
.../RpcGenerator.CodecPolicyOwnership.cs | 13 +++----------
1 file changed, 3 insertions(+), 10 deletions(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.CodecPolicyOwnership.cs b/src/SharpLink.Generator/RpcGenerator.CodecPolicyOwnership.cs
index fa493449f..eb7ae1c15 100644
--- a/src/SharpLink.Generator/RpcGenerator.CodecPolicyOwnership.cs
+++ b/src/SharpLink.Generator/RpcGenerator.CodecPolicyOwnership.cs
@@ -394,21 +394,14 @@ private void AddCanonicalCustomCodecBinding(ITypeSymbol target, ITypeSymbol code
return null;
}
- var codecIdentity = named.GetAttributes().FirstOrDefault(static attribute =>
- IsAttribute(attribute, "SharpLink.Sdk", "RpcCodecImplementationAttribute"));
- if (codecIdentity is null ||
- codecIdentity.ConstructorArguments.Length != 2 ||
- codecIdentity.ConstructorArguments[0].Value is not string wireFormatId ||
- codecIdentity.ConstructorArguments[1].Value is not string schemaId ||
- !IsStableIdentity(wireFormatId) ||
- !IsStableIdentity(schemaId))
+ if (!HasValidOpaqueSemanticIdentity(named))
{
Report(DtoDiagnosticKind.CustomCodecIdentityInvalid, codecType,
- "custom Codec must declare stable ASCII WireFormatId and SchemaId via [RpcCodecImplementation]", location);
+ "custom Codec must declare a non-zero fixed semantic identity via [RpcCodecSemanticIdentity(high, low)]", location);
return null;
}
- return new CustomCodecRegistration(named, wireFormatId, schemaId, location);
+ return new CustomCodecRegistration(named, location);
}
private void CollectCanonicalAssemblyBindings()
From 0100964a0347627fa5ea1185b2281cca455dda34 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:09:34 +0800
Subject: [PATCH 035/399] refactor: remove legacy generated codec identity
strings
---
src/SharpLink.Abstractions/RpcGeneratedCodecRegistry.cs | 6 ------
1 file changed, 6 deletions(-)
diff --git a/src/SharpLink.Abstractions/RpcGeneratedCodecRegistry.cs b/src/SharpLink.Abstractions/RpcGeneratedCodecRegistry.cs
index 3df993274..591df14cc 100644
--- a/src/SharpLink.Abstractions/RpcGeneratedCodecRegistry.cs
+++ b/src/SharpLink.Abstractions/RpcGeneratedCodecRegistry.cs
@@ -9,12 +9,6 @@ public interface IRpcGeneratedCodecFactory
/// Gets the deterministic identity of the finalized Codec semantics.
RpcHash128 CodecHash => default;
- /// Gets the legacy deterministic schema identifier.
- string SchemaId { get; }
-
- /// Gets the legacy binary wire-format identity.
- string WireFormatId { get; }
-
/// Gets the adapter lifecycle identity, or null for adapter-free Codecs.
string? AdapterId { get; }
From bbccc564fd504d804e140db491aec8366bc9dd76 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:10:15 +0800
Subject: [PATCH 036/399] refactor: use CodecHash in generated manifest
identity
---
src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs b/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs
index 2bd7fea90..64c9c407c 100644
--- a/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs
+++ b/src/SharpLink.Generator/RpcGenerator.ManifestEmitter.cs
@@ -137,8 +137,6 @@ private static void AppendIdentifiedCodecFactory(StringBuilder sb)
sb.AppendLine();
sb.AppendLine(" public Type TargetType => __inner.TargetType;");
sb.AppendLine(" public RpcHash128 CodecHash { get; }");
- sb.AppendLine(" public string SchemaId => __inner.SchemaId;");
- sb.AppendLine(" public string WireFormatId => __inner.WireFormatId;");
sb.AppendLine(" public string? AdapterId => __inner.AdapterId;");
sb.AppendLine(" public IRpcCodecAdapter? Adapter => __inner.Adapter;");
sb.AppendLine(" public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapterScope)");
@@ -241,9 +239,9 @@ private static string BuildCompileTimeDescriptor(
foreach (var service in services)
sb.Append("S:").Append(service.Interface.Hash).Append(':').Append(service.ServiceFullName).Append(':').Append(service.Lifetime).Append(';');
foreach (var codec in codecs.OrderBy(static codec => codec.TypeName, StringComparer.Ordinal))
- sb.Append("D:").Append(codec.TypeName).Append(':').Append(codec.SchemaId).Append(';');
+ sb.Append("D:").Append(codec.TypeName).Append(':').Append(new RpcHashValue(codec.CodecHashHigh, codec.CodecHashLow).ToHex()).Append(';');
foreach (var codec in contractCodecs.OrderBy(static codec => codec.TypeName, StringComparer.Ordinal))
- sb.Append("K:").Append(codec.TypeName).Append(':').Append(codec.SchemaId).Append(';');
+ sb.Append("K:").Append(codec.TypeName).Append(':').Append(new RpcHashValue(codec.CodecHashHigh, codec.CodecHashLow).ToHex()).Append(';');
return sb.ToString();
}
From 1f0b33b1a1d42b80251308d53d3037bbe3789f61 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:10:51 +0800
Subject: [PATCH 037/399] refactor: compare generated codecs by CodecHash only
---
src/SharpLink.Runtime/SharpLinkRuntimeContext.cs | 11 ++---------
1 file changed, 2 insertions(+), 9 deletions(-)
diff --git a/src/SharpLink.Runtime/SharpLinkRuntimeContext.cs b/src/SharpLink.Runtime/SharpLinkRuntimeContext.cs
index fe4ea19b9..042ac0ba0 100644
--- a/src/SharpLink.Runtime/SharpLinkRuntimeContext.cs
+++ b/src/SharpLink.Runtime/SharpLinkRuntimeContext.cs
@@ -59,17 +59,10 @@ internal SharpLinkRuntimeContext(
private static bool HasSameGeneratedCodecIdentity(
IRpcGeneratedCodecFactory left,
IRpcGeneratedCodecFactory right)
- {
- if (!left.CodecHash.IsEmpty || !right.CodecHash.IsEmpty)
- return left.CodecHash == right.CodecHash;
- return string.Equals(left.SchemaId, right.SchemaId, StringComparison.Ordinal) &&
- string.Equals(left.WireFormatId, right.WireFormatId, StringComparison.Ordinal);
- }
+ => left.CodecHash == right.CodecHash;
private static string DescribeGeneratedCodecIdentity(IRpcGeneratedCodecFactory factory)
- => factory.CodecHash.IsEmpty
- ? $"legacy:{factory.SchemaId}/{factory.WireFormatId}"
- : $"codec:{factory.CodecHash}";
+ => $"codec:{factory.CodecHash}";
[System.Diagnostics.CodeAnalysis.DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
From d8427e223be045a4c2f031f752252df648dbf48a Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:13:02 +0800
Subject: [PATCH 038/399] refactor: validate dynamic manifest codecs by
CodecHash
---
src/SharpLink.Runtime/SharpLinkDynamicModule.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/SharpLink.Runtime/SharpLinkDynamicModule.cs b/src/SharpLink.Runtime/SharpLinkDynamicModule.cs
index 0cc92212e..9c4d72c33 100644
--- a/src/SharpLink.Runtime/SharpLinkDynamicModule.cs
+++ b/src/SharpLink.Runtime/SharpLinkDynamicModule.cs
@@ -316,7 +316,7 @@ not SharpLinkServiceLifetime.Connection and
for (var codecIndex = 0; codecIndex < manifest.Codecs.Count; codecIndex++)
{
var codec = manifest.Codecs[codecIndex];
- if (codec is null || codec.TargetType is null || string.IsNullOrWhiteSpace(codec.SchemaId))
+ if (codec is null || codec.TargetType is null || codec.CodecHash.IsEmpty)
{
return Error(
SharpLinkAssemblyRegistrationErrorCode.InvalidManifest,
@@ -331,7 +331,7 @@ not SharpLinkServiceLifetime.Connection and
$"Manifest contains more than one Codec for '{codec.TargetType.FullName}'.",
assembly,
"Codec",
- incomingFingerprint: codec.SchemaId);
+ incomingFingerprint: codec.CodecHash.ToString());
}
}
From 0d2e114acc931b6eca681e8c0507fccafb00d6a8 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:13:44 +0800
Subject: [PATCH 039/399] refactor: validate manifest codec identity by
CodecHash
---
.../SharpLinkGeneratedManifestCompatibility.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/SharpLink.Runtime/SharpLinkGeneratedManifestCompatibility.cs b/src/SharpLink.Runtime/SharpLinkGeneratedManifestCompatibility.cs
index c03ee7826..dee6e8dec 100644
--- a/src/SharpLink.Runtime/SharpLinkGeneratedManifestCompatibility.cs
+++ b/src/SharpLink.Runtime/SharpLinkGeneratedManifestCompatibility.cs
@@ -271,7 +271,7 @@ not SharpLinkServiceLifetime.Connection and
for (var codecIndex = 0; codecIndex < manifest.Codecs.Count; codecIndex++)
{
var codec = manifest.Codecs[codecIndex];
- if (codec is null || codec.TargetType is null || string.IsNullOrWhiteSpace(codec.SchemaId))
+ if (codec is null || codec.TargetType is null || codec.CodecHash.IsEmpty)
{
return Error(
SharpLinkAssemblyRegistrationErrorCode.InvalidManifest,
@@ -286,7 +286,7 @@ not SharpLinkServiceLifetime.Connection and
$"Manifest contains more than one Codec for '{codec.TargetType.FullName}'.",
diagnosticAssembly,
"Codec",
- incomingFingerprint: codec.SchemaId);
+ incomingFingerprint: codec.CodecHash.ToString());
}
}
From 4956d78c9a81035eb0238cbd13c9264a0de5ee14 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:14:41 +0800
Subject: [PATCH 040/399] test: cover deterministic identity propagation
---
.../RpcDeterministicIdentityTests.cs | 109 +++++++++++++++++-
1 file changed, 104 insertions(+), 5 deletions(-)
diff --git a/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs b/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
index e931d334a..2dd15b562 100644
--- a/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
+++ b/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
@@ -6,6 +6,57 @@ namespace SharpLink.Generator.Tests;
public partial class RpcAnalyzerTests
{
+ [Test]
+ public Task DeterministicIdentityShouldBeStableAcrossRepeatedGeneration()
+ {
+ var first = GenerateDtoIdentityManifest(includeExtraMember: false, idempotent: false);
+ var second = GenerateDtoIdentityManifest(includeExtraMember: false, idempotent: false);
+
+ Ensure(
+ ExtractGeneratedCodecIdentity(first, "DeterministicPayload") ==
+ ExtractGeneratedCodecIdentity(second, "DeterministicPayload"),
+ "unchanged RPC semantics must produce the same CodecHash across repeated generation");
+ Ensure(
+ ExtractGeneratedRpcAssemblyHash(first) == ExtractGeneratedRpcAssemblyHash(second),
+ "unchanged RPC semantics must produce the same RpcAssemblyHash across repeated generation");
+ return Task.CompletedTask;
+ }
+
+ [Test]
+ public Task DtoWireShapeChangeShouldChangeFinalRpcIdentity()
+ {
+ var first = GenerateDtoIdentityManifest(includeExtraMember: false, idempotent: false);
+ var second = GenerateDtoIdentityManifest(includeExtraMember: true, idempotent: false);
+
+ Ensure(
+ ExtractGeneratedCodecIdentity(first, "DeterministicPayload") !=
+ ExtractGeneratedCodecIdentity(second, "DeterministicPayload"),
+ "changing generated DTO wire shape must change CodecHash");
+ Ensure(
+ ExtractGeneratedRpcAssemblyHash(first) != ExtractGeneratedRpcAssemblyHash(second),
+ "changing a reachable DTO CodecHash must change RpcAssemblyHash");
+ return Task.CompletedTask;
+ }
+
+ [Test]
+ public Task MethodSemanticChangeShouldNotReuseRouteIdentityAsCompatibilityIdentity()
+ {
+ var first = GenerateDtoIdentityManifest(includeExtraMember: false, idempotent: false);
+ var second = GenerateDtoIdentityManifest(includeExtraMember: false, idempotent: true);
+
+ Ensure(
+ ExtractGeneratedCodecIdentity(first, "DeterministicPayload") ==
+ ExtractGeneratedCodecIdentity(second, "DeterministicPayload"),
+ "method-only semantics must not perturb payload CodecHash");
+ Ensure(
+ ExtractGeneratedMethodId(first, "Echo") == ExtractGeneratedMethodId(second, "Echo"),
+ "a method semantic flag must not be encoded by changing the dispatch MethodId");
+ Ensure(
+ ExtractGeneratedRpcAssemblyHash(first) != ExtractGeneratedRpcAssemblyHash(second),
+ "method semantic changes must flow through MethodHash/ContractHash into RpcAssemblyHash");
+ return Task.CompletedTask;
+ }
+
[Test]
public Task OpaqueSemanticIdentityShouldIgnoreUnrelatedImplementationChanges()
{
@@ -19,7 +70,8 @@ public Task OpaqueSemanticIdentityShouldIgnoreUnrelatedImplementationChanges()
semanticLow: 0x1112131415161718UL);
Ensure(
- ExtractGeneratedCodecIdentity(first) == ExtractGeneratedCodecIdentity(second),
+ ExtractGeneratedCodecIdentity(first, "OpaquePayload") ==
+ ExtractGeneratedCodecIdentity(second, "OpaquePayload"),
"opaque CodecHash must be controlled by its fixed semantic identity rather than unrelated implementation details");
Ensure(
ExtractGeneratedRpcAssemblyHash(first) == ExtractGeneratedRpcAssemblyHash(second),
@@ -40,7 +92,8 @@ public Task OpaqueSemanticIdentityChangeShouldChangeFinalRpcIdentity()
semanticLow: 0x2112131415161718UL);
Ensure(
- ExtractGeneratedCodecIdentity(first) != ExtractGeneratedCodecIdentity(second),
+ ExtractGeneratedCodecIdentity(first, "OpaquePayload") !=
+ ExtractGeneratedCodecIdentity(second, "OpaquePayload"),
"changing opaque serializer semantics must change CodecHash");
Ensure(
ExtractGeneratedRpcAssemblyHash(first) != ExtractGeneratedRpcAssemblyHash(second),
@@ -48,6 +101,37 @@ public Task OpaqueSemanticIdentityChangeShouldChangeFinalRpcIdentity()
return Task.CompletedTask;
}
+ private static string GenerateDtoIdentityManifest(bool includeExtraMember, bool idempotent)
+ {
+ var extraMember = includeExtraMember
+ ? "public long Extra { get; set; }"
+ : string.Empty;
+ var methodAttribute = idempotent ? "[Idempotent]" : string.Empty;
+ var source = $$"""
+using System.Threading;
+using System.Threading.Tasks;
+using SharpLink.Sdk;
+
+[RpcSerializable]
+public sealed class DeterministicPayload
+{
+ public int Value { get; set; }
+ {{extraMember}}
+}
+
+[RpcContract]
+public interface IDeterministicIdentityContract : IService
+{
+ {{methodAttribute}}
+ ValueTask Echo(DeterministicPayload value, CancellationToken cancellationToken);
+}
+""";
+
+ return RunGeneratorAndGetSources(source)
+ .Single(static generated =>
+ generated.Contains("ISharpLinkGeneratedAssemblyManifest", StringComparison.Ordinal));
+ }
+
private static string GenerateOpaqueIdentityManifest(
string implementationMarker,
ulong semanticHigh,
@@ -89,14 +173,29 @@ public interface IOpaqueIdentityContract : IService
generated.Contains("ISharpLinkGeneratedAssemblyManifest", StringComparison.Ordinal));
}
- private static string ExtractGeneratedCodecIdentity(string manifest)
+ private static string ExtractGeneratedCodecIdentity(string manifest, string typeName)
=> manifest.Split('\n')
- .Single(static line =>
+ .Single(line =>
line.Contains(
- "SharpLinkGeneratedCodecIdentityAttribute(typeof(global::OpaquePayload)",
+ $"SharpLinkGeneratedCodecIdentityAttribute(typeof(global::{typeName})",
StringComparison.Ordinal))
.Trim();
+ private static string ExtractGeneratedMethodId(string manifest, string methodName)
+ {
+ var lines = manifest.Split('\n');
+ for (var index = 0; index + 2 < lines.Length; index++)
+ {
+ if (lines[index].Contains("new SharpLinkGeneratedMethodDescriptor(", StringComparison.Ordinal) &&
+ lines[index + 1].Contains($"\"{methodName}\"", StringComparison.Ordinal))
+ {
+ return lines[index + 2].Trim();
+ }
+ }
+
+ throw new InvalidOperationException($"Generated method descriptor '{methodName}' was not found.");
+ }
+
private static string ExtractGeneratedRpcAssemblyHash(string manifest)
=> manifest.Split('\n')
.Single(static line => line.Contains("public RpcHash128 RpcAssemblyHash =>", StringComparison.Ordinal))
From eed9e830647b366d0ca803a96ce3c5e8b006965a Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:19:05 +0800
Subject: [PATCH 041/399] refactor: compare client dynamic codecs by CodecHash
---
.../SharpLinkClient.AssemblyRegistration.cs | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/src/SharpLink.Client/SharpLinkClient.AssemblyRegistration.cs b/src/SharpLink.Client/SharpLinkClient.AssemblyRegistration.cs
index dce5fb7fe..b5163b1ab 100644
--- a/src/SharpLink.Client/SharpLinkClient.AssemblyRegistration.cs
+++ b/src/SharpLink.Client/SharpLinkClient.AssemblyRegistration.cs
@@ -399,12 +399,15 @@ private RegistrationCandidate BuildRegistrationCandidate(
var codec = pair.Value;
if (nextFactories.TryGetValue(pair.Key, out var existingCodec))
{
- if (!string.Equals(existingCodec.Factory.SchemaId, codec.Factory.SchemaId, StringComparison.Ordinal) ||
- !string.Equals(existingCodec.Factory.WireFormatId, codec.Factory.WireFormatId, StringComparison.Ordinal))
+ if (existingCodec.Factory.CodecHash != codec.Factory.CodecHash)
{
- error = CreateError(SharpLinkAssemblyRegistrationErrorCode.CodecConflict,
- $"Codec conflict for '{pair.Key.FullName}': existing schema/wire '{existingCodec.Factory.SchemaId}'/'{existingCodec.Factory.WireFormatId}', incoming schema/wire '{codec.Factory.SchemaId}'/'{codec.Factory.WireFormatId}'.",
- incoming.OwnerAssembly, "Codec", existingCodec.Factory.SchemaId, codec.Factory.SchemaId);
+ error = CreateError(
+ SharpLinkAssemblyRegistrationErrorCode.CodecConflict,
+ $"Codec conflict for '{pair.Key.FullName}': existing CodecHash '{existingCodec.Factory.CodecHash}', incoming CodecHash '{codec.Factory.CodecHash}'.",
+ incoming.OwnerAssembly,
+ "Codec",
+ existingCodec.Factory.CodecHash.ToString(),
+ codec.Factory.CodecHash.ToString());
return default;
}
continue;
From 87e66f0831983ebb8f079af1abbd449bdfdac880 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:20:06 +0800
Subject: [PATCH 042/399] refactor: compare server dynamic codecs by CodecHash
---
.../SharpLinkServer.AssemblyRegistration.cs | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/src/SharpLink.Server/SharpLinkServer.AssemblyRegistration.cs b/src/SharpLink.Server/SharpLinkServer.AssemblyRegistration.cs
index ebc8ffa0b..10e78099a 100644
--- a/src/SharpLink.Server/SharpLinkServer.AssemblyRegistration.cs
+++ b/src/SharpLink.Server/SharpLinkServer.AssemblyRegistration.cs
@@ -385,16 +385,15 @@ private RegistrationCandidate BuildRegistrationCandidate(
var codec = pair.Value;
if (nextFactories.TryGetValue(pair.Key, out var existingCodec))
{
- if (!string.Equals(existingCodec.Factory.SchemaId, codec.Factory.SchemaId, StringComparison.Ordinal) ||
- !string.Equals(existingCodec.Factory.WireFormatId, codec.Factory.WireFormatId, StringComparison.Ordinal))
+ if (existingCodec.Factory.CodecHash != codec.Factory.CodecHash)
{
error = CreateError(
SharpLinkAssemblyRegistrationErrorCode.CodecConflict,
- $"Codec conflict for '{pair.Key.FullName}': existing schema/wire '{existingCodec.Factory.SchemaId}'/'{existingCodec.Factory.WireFormatId}', incoming schema/wire '{codec.Factory.SchemaId}'/'{codec.Factory.WireFormatId}'.",
+ $"Codec conflict for '{pair.Key.FullName}': existing CodecHash '{existingCodec.Factory.CodecHash}', incoming CodecHash '{codec.Factory.CodecHash}'.",
incoming.OwnerAssembly,
artifact: "Codec",
- existingFingerprint: existingCodec.Factory.SchemaId,
- incomingFingerprint: codec.Factory.SchemaId);
+ existingFingerprint: existingCodec.Factory.CodecHash.ToString(),
+ incomingFingerprint: codec.Factory.CodecHash.ToString());
return default;
}
continue;
From d0b8e8f9600a5cab41306aee33e3a5c8ed96e457 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:22:59 +0800
Subject: [PATCH 043/399] style: restore final newline in SharpPack codec
---
src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs b/src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs
index 099fafdcd..b8d12dae7 100644
--- a/src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs
+++ b/src/SharpLink.Serializer.SharpPack/SharpPackRpcCodec.cs
@@ -170,4 +170,4 @@ internal readonly struct SharpPackBufferWriter(IBufferWriter writer) : IBu
public void Advance(int count) => writer.Advance(count);
public Memory GetMemory(int sizeHint = 0) => writer.GetMemory(sizeHint);
public Span GetSpan(int sizeHint = 0) => writer.GetSpan(sizeHint);
-}
\ No newline at end of file
+}
From 072596e2df93256c5de36d4a54a1837a8bb8bab9 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:35:06 +0800
Subject: [PATCH 044/399] test: migrate malformed codec semantic identity
---
test/SharpLink.IntegrationTests/AssemblyCodecBindings.cs | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/test/SharpLink.IntegrationTests/AssemblyCodecBindings.cs b/test/SharpLink.IntegrationTests/AssemblyCodecBindings.cs
index 1dec4f19a..8e1e7b28f 100644
--- a/test/SharpLink.IntegrationTests/AssemblyCodecBindings.cs
+++ b/test/SharpLink.IntegrationTests/AssemblyCodecBindings.cs
@@ -8,9 +8,7 @@ namespace SharpLink.IntegrationTests;
public readonly record struct MalformedHeader(int Value);
-[RpcCodecImplementation(
- "sharplink-integration-malformed-header/v1",
- "sharplink-integration-malformed-header-schema/v1")]
+[RpcCodecSemanticIdentity(0x4b166fb4cfa21e94UL, 0x915bac210ac312dbUL)]
public sealed class MalformedHeaderCodec : IRpcCodec
{
public void Serialize(in MalformedHeader value, IBufferWriter buffer)
From a3eb41eaa7f3d14122dfc22684c1b8a448c0f72e Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:35:29 +0800
Subject: [PATCH 045/399] test: migrate pre-credit adapter identity
---
test/SharpLink.PreCreditAotSmoke/Program.cs | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/test/SharpLink.PreCreditAotSmoke/Program.cs b/test/SharpLink.PreCreditAotSmoke/Program.cs
index 682f7fb49..92e6932ad 100644
--- a/test/SharpLink.PreCreditAotSmoke/Program.cs
+++ b/test/SharpLink.PreCreditAotSmoke/Program.cs
@@ -14,8 +14,7 @@
[assembly: RpcCodecAdapterRegistration(
typeof(SharpLink.PreCreditAotSmoke.PreCreditPayloadCodecAdapter),
- "sharplink.precredit-aot.unsized",
- "sharplink.precredit-aot.unsized.v1")]
+ "sharplink.precredit-aot.unsized")]
namespace SharpLink.PreCreditAotSmoke;
@@ -191,12 +190,11 @@ public async IAsyncEnumerable StreamAsync(int count)
[RpcCodecAdapter(typeof(PreCreditPayloadCodecAdapter))]
public readonly record struct PreCreditPayload(int Sequence);
+[RpcCodecSemanticIdentity(0x5937fbbdf810875fUL, 0xea08c7aeef8cbe0fUL)]
public sealed class PreCreditPayloadCodecAdapter : IRpcCodecAdapter
{
public string AdapterId => "sharplink.precredit-aot.unsized";
- public string WireFormatId => "sharplink.precredit-aot.unsized.v1";
-
public IRpcCodecAdapterScope CreateScope() => new Scope();
private sealed class Scope : IRpcCodecAdapterScope
From 67265799bb085d58a38fa4b33a2aec5f37610d08 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:37:29 +0800
Subject: [PATCH 046/399] fix: skip invalid codec bindings during hash
finalization
---
src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs b/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
index 7b9cecd91..a174d467c 100644
--- a/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
+++ b/src/SharpLink.Generator/RpcGenerator.CodecIdentity.cs
@@ -25,6 +25,7 @@ internal ImmutableArray BuildFinalCodecHashes(
var cache = new Dictionary(StringComparer.Ordinal);
return reachable
+ .Where(pair => !_failed.Contains(pair.Key))
.OrderBy(static pair => pair.Key, StringComparer.Ordinal)
.Select(pair =>
{
From 4479b32b6f510371f0876a75302198576bf5d466 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:47:39 +0800
Subject: [PATCH 047/399] test: add deterministic synthetic manifest identities
---
.../TestGeneratedIdentity.cs | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
create mode 100644 test/SharpLink.UnitTests/TestGeneratedIdentity.cs
diff --git a/test/SharpLink.UnitTests/TestGeneratedIdentity.cs b/test/SharpLink.UnitTests/TestGeneratedIdentity.cs
new file mode 100644
index 000000000..b3d0f6f1a
--- /dev/null
+++ b/test/SharpLink.UnitTests/TestGeneratedIdentity.cs
@@ -0,0 +1,25 @@
+namespace SharpLink.UnitTests;
+
+internal static class TestGeneratedIdentity
+{
+ internal static readonly RpcHash128 ManifestHash =
+ new(0x746573742d6d616eUL, 0x69666573742d7631UL);
+
+ internal static readonly RpcHash128 CodecHash =
+ new(0x746573742d636f64UL, 0x65632d6861736831UL);
+
+ internal static readonly RpcHash128 AlternateCodecHash =
+ new(0x746573742d636f64UL, 0x65632d6861736832UL);
+}
+
+internal interface ITestGeneratedManifest : ISharpLinkGeneratedAssemblyManifest
+{
+ RpcHash128 ISharpLinkGeneratedAssemblyManifest.RpcAssemblyHash
+ => TestGeneratedIdentity.ManifestHash;
+}
+
+internal interface ITestGeneratedCodecFactory : IRpcGeneratedCodecFactory
+{
+ RpcHash128 IRpcGeneratedCodecFactory.CodecHash
+ => TestGeneratedIdentity.CodecHash;
+}
From 71012918645d63121b9f9657005a5b999b281887 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:48:05 +0800
Subject: [PATCH 048/399] test: give dynamic module manifests semantic identity
---
test/SharpLink.UnitTests/Runtime/DynamicModuleTests.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/SharpLink.UnitTests/Runtime/DynamicModuleTests.cs b/test/SharpLink.UnitTests/Runtime/DynamicModuleTests.cs
index c37ecbb04..ebac839a6 100644
--- a/test/SharpLink.UnitTests/Runtime/DynamicModuleTests.cs
+++ b/test/SharpLink.UnitTests/Runtime/DynamicModuleTests.cs
@@ -162,7 +162,7 @@ private static void Ensure(bool condition, string message)
throw new Exception(message);
}
- private sealed class EmptyManifest : ISharpLinkGeneratedAssemblyManifest
+ private sealed class EmptyManifest : ITestGeneratedManifest
{
public int ApiVersion => SharpLinkGeneratedManifestVersions.Api;
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
From 6048c7aa3bda1f285bbcb5aac61049b0ecaa0b11 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:48:34 +0800
Subject: [PATCH 049/399] test: migrate routed codec identity fixtures
---
.../Runtime/RpcCodecRouteRuntimeTests.cs | 19 +++++++------------
1 file changed, 7 insertions(+), 12 deletions(-)
diff --git a/test/SharpLink.UnitTests/Runtime/RpcCodecRouteRuntimeTests.cs b/test/SharpLink.UnitTests/Runtime/RpcCodecRouteRuntimeTests.cs
index 1a8daaf4c..d1819a42d 100644
--- a/test/SharpLink.UnitTests/Runtime/RpcCodecRouteRuntimeTests.cs
+++ b/test/SharpLink.UnitTests/Runtime/RpcCodecRouteRuntimeTests.cs
@@ -20,7 +20,7 @@ public void ContractRoutesShouldCoexistWithoutChangingGlobalBuiltin()
using var context = new SharpLinkRuntimeContextBuilder()
.Build(includeGeneratedAssemblyCatalog: false);
var registrationA = context.PrepareGeneratedManifest(
- new RoutedManifest(ownerA, routeA, "route-a/v1", "wire-a/v1"));
+ new RoutedManifest(ownerA, routeA, "route-a/v1"));
var registrationB = context.PrepareGeneratedManifest(new DefaultManifest(ownerB));
Ensure(registrationA.Codecs.Count == 0,
@@ -62,11 +62,9 @@ public void Serialize(in int value, IBufferWriter buffer)
private sealed class RouteAdapter(
RoutedInt32Codec codec,
- string adapterId,
- string wireFormatId) : IRpcCodecAdapter
+ string adapterId) : IRpcCodecAdapter
{
public string AdapterId { get; } = adapterId;
- public string WireFormatId { get; } = wireFormatId;
public IRpcCodecAdapterScope CreateScope() => new RouteScope(codec);
}
@@ -82,11 +80,9 @@ public void Dispose()
}
}
- private sealed class RoutedInt32Factory(RouteAdapter adapter) : IRpcGeneratedCodecFactory
+ private sealed class RoutedInt32Factory(RouteAdapter adapter) : ITestGeneratedCodecFactory
{
public Type TargetType => typeof(int);
- public string SchemaId => $"route-native-int32-{adapter.AdapterId}";
- public string WireFormatId => adapter.WireFormatId;
public string? AdapterId => adapter.AdapterId;
public IRpcCodecAdapter? Adapter => adapter;
@@ -97,16 +93,15 @@ public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapt
public bool IsCompatibleCodec(IRpcCodec candidate) => candidate is IRpcCodec;
}
- private sealed class RoutedManifest : ISharpLinkGeneratedAssemblyManifest
+ private sealed class RoutedManifest : ITestGeneratedManifest
{
public RoutedManifest(
Assembly ownerAssembly,
RoutedInt32Codec codec,
- string adapterId,
- string wireFormatId)
+ string adapterId)
{
OwnerAssembly = ownerAssembly;
- ContractCodecs = [new RoutedInt32Factory(new RouteAdapter(codec, adapterId, wireFormatId))];
+ ContractCodecs = [new RoutedInt32Factory(new RouteAdapter(codec, adapterId))];
}
public int ApiVersion => SharpLinkGeneratedManifestVersions.Api;
@@ -121,7 +116,7 @@ public RoutedManifest(
public IReadOnlyList Dependencies => [];
}
- private sealed class DefaultManifest : ISharpLinkGeneratedAssemblyManifest
+ private sealed class DefaultManifest : ITestGeneratedManifest
{
public DefaultManifest(Assembly ownerAssembly)
{
From 6eb59f9b0cd7e158d048cb8760439cb29ec731ce Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:48:51 +0800
Subject: [PATCH 050/399] test: migrate custom codec policy identity fixture
---
.../Runtime/RpcCodecPolicyRegressionTests.cs | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/test/SharpLink.UnitTests/Runtime/RpcCodecPolicyRegressionTests.cs b/test/SharpLink.UnitTests/Runtime/RpcCodecPolicyRegressionTests.cs
index a572feeb9..3c78f5b5d 100644
--- a/test/SharpLink.UnitTests/Runtime/RpcCodecPolicyRegressionTests.cs
+++ b/test/SharpLink.UnitTests/Runtime/RpcCodecPolicyRegressionTests.cs
@@ -11,7 +11,7 @@ namespace SharpLink.UnitTests.Runtime;
public sealed class RpcCodecPolicyRegressionTests
{
[Test]
- public void CustomFactoryWithCustomWireFormatShouldPrepareAndResolve()
+ public void CustomFactoryWithSemanticIdentityShouldPrepareAndResolve()
{
var manifest = new TestManifest(
typeof(IContractA).Assembly,
@@ -69,11 +69,9 @@ public void Serialize(in CustomPayload value, IBufferWriter buffer) { }
public CustomPayload Deserialize(in ReadOnlySequence buffer) => new();
}
- private sealed class CustomPayloadFactory : IRpcGeneratedCodecFactory
+ private sealed class CustomPayloadFactory : ITestGeneratedCodecFactory
{
public Type TargetType => typeof(CustomPayload);
- public string SchemaId => "custom-payload-schema/v1";
- public string WireFormatId => "custom-payload-wire/v1";
public string? AdapterId => null;
public IRpcCodecAdapter? Adapter => null;
public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapterScope)
@@ -85,7 +83,7 @@ public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapt
private sealed class TestManifest(
Assembly ownerAssembly,
- IReadOnlyList contractCodecs) : ISharpLinkGeneratedAssemblyManifest
+ IReadOnlyList contractCodecs) : ITestGeneratedManifest
{
public int ApiVersion => SharpLinkGeneratedManifestVersions.Api;
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
From 815190e44f04b07328c5163a978894a6b040f0f5 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:49:08 +0800
Subject: [PATCH 051/399] test: give enum override manifest semantic identity
---
.../Runtime/RpcEnumCodecOverrideRegressionTests.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/SharpLink.UnitTests/Runtime/RpcEnumCodecOverrideRegressionTests.cs b/test/SharpLink.UnitTests/Runtime/RpcEnumCodecOverrideRegressionTests.cs
index ed0059d53..3ed222c4d 100644
--- a/test/SharpLink.UnitTests/Runtime/RpcEnumCodecOverrideRegressionTests.cs
+++ b/test/SharpLink.UnitTests/Runtime/RpcEnumCodecOverrideRegressionTests.cs
@@ -53,7 +53,7 @@ public void Serialize(in TestMode value, IBufferWriter writer)
public TestMode Deserialize(in ReadOnlySequence buffer) => TestMode.Active;
}
- private sealed class AssemblyEnumManifest : ISharpLinkGeneratedAssemblyManifest
+ private sealed class AssemblyEnumManifest : ITestGeneratedManifest
{
public int ApiVersion => SharpLinkGeneratedManifestVersions.Api;
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
From d11924b1794bc71195e042bb8575c94df2f40c1d Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:50:28 +0800
Subject: [PATCH 052/399] test: migrate manifest codec provider identity
fixtures
---
.../Runtime/RpcManifestCodecProviderTests.cs | 31 ++++++++-----------
1 file changed, 13 insertions(+), 18 deletions(-)
diff --git a/test/SharpLink.UnitTests/Runtime/RpcManifestCodecProviderTests.cs b/test/SharpLink.UnitTests/Runtime/RpcManifestCodecProviderTests.cs
index ee80b82d5..71f4c6abb 100644
--- a/test/SharpLink.UnitTests/Runtime/RpcManifestCodecProviderTests.cs
+++ b/test/SharpLink.UnitTests/Runtime/RpcManifestCodecProviderTests.cs
@@ -175,12 +175,10 @@ public void Serialize(in List value, IBufferWriter buffer)
public List? Deserialize(in ReadOnlySequence buffer) => [];
}
- private sealed class NativeFactory(Func> create, string schemaId)
- : IRpcGeneratedCodecFactory
+ private sealed class NativeFactory(Func> create)
+ : ITestGeneratedCodecFactory
{
public Type TargetType => typeof(T);
- public string SchemaId { get; } = schemaId;
- public string WireFormatId => "sharplink-native/v1";
public string? AdapterId => null;
public IRpcCodecAdapter? Adapter => null;
@@ -197,7 +195,6 @@ public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapt
private sealed class PointAdapter(RoutedPointCodec codec) : IRpcCodecAdapter
{
public string AdapterId => "nested-point-route/v1";
- public string WireFormatId => "nested-point-wire/v1";
public IRpcCodecAdapterScope CreateScope() => new PointScope(codec);
}
@@ -213,11 +210,9 @@ public void Dispose()
}
}
- private sealed class RoutedPointFactory(PointAdapter adapter) : IRpcGeneratedCodecFactory
+ private sealed class RoutedPointFactory(PointAdapter adapter) : ITestGeneratedCodecFactory
{
public Type TargetType => typeof(Point);
- public string SchemaId => "nested-point-route";
- public string WireFormatId => adapter.WireFormatId;
public string? AdapterId => adapter.AdapterId;
public IRpcCodecAdapter? Adapter => adapter;
@@ -240,14 +235,14 @@ public void Serialize(in NoRouteValue value, IBufferWriter buffer) { }
public NoRouteValue Deserialize(in ReadOnlySequence buffer) => default;
}
- private sealed class NoRouteManifest : ISharpLinkGeneratedAssemblyManifest
+ private sealed class NoRouteManifest : ITestGeneratedManifest
{
internal NoRouteManifest(Assembly ownerAssembly)
{
OwnerAssembly = ownerAssembly;
Codecs =
[
- new NativeFactory(static _ => new GeneratedNoRouteCodec(), "no-route-generated")
+ new NativeFactory(static _ => new GeneratedNoRouteCodec())
];
}
@@ -262,7 +257,7 @@ internal NoRouteManifest(Assembly ownerAssembly)
public IReadOnlyList Dependencies => [];
}
- private sealed class PreviousApiManifest(Assembly ownerAssembly) : ISharpLinkGeneratedAssemblyManifest
+ private sealed class PreviousApiManifest(Assembly ownerAssembly) : ITestGeneratedManifest
{
public int ApiVersion => SharpLinkGeneratedManifestVersions.Api - 1;
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
@@ -282,19 +277,19 @@ public void Serialize(in ContractValue value, IBufferWriter buffer) { }
public ContractValue Deserialize(in ReadOnlySequence buffer) => default;
}
- private sealed class ContractCodecManifest(Assembly ownerAssembly, NamedContractCodec codec, string schemaId)
- : ISharpLinkGeneratedAssemblyManifest
+ private sealed class ContractCodecManifest(Assembly ownerAssembly, NamedContractCodec codec, string descriptor)
+ : ITestGeneratedManifest
{
public int ApiVersion => SharpLinkGeneratedManifestVersions.Api;
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "contract-codec-test";
public Assembly OwnerAssembly { get; } = ownerAssembly;
- public string CompileTimeDescriptor => schemaId;
+ public string CompileTimeDescriptor => descriptor;
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
public IReadOnlyList Codecs => [];
public IReadOnlyList ContractCodecs { get; } =
- [new NativeFactory(_ => codec, schemaId)];
+ [new NativeFactory(_ => codec)];
public IReadOnlyList Dependencies => [];
}
@@ -316,15 +311,15 @@ private sealed class ThrowingBufferPool : IRpcBufferWriterPool
public void Return(IRpcByteBufferWriter writer) { }
}
- private sealed class NestedRouteManifest : ISharpLinkGeneratedAssemblyManifest
+ private sealed class NestedRouteManifest : ITestGeneratedManifest
{
internal NestedRouteManifest(Assembly ownerAssembly, RoutedPointCodec routedPoint)
{
OwnerAssembly = ownerAssembly;
ContractCodecs =
[
- new NativeFactory(static provider => new EnvelopeCodec(provider), "nested-envelope-native"),
- new NativeFactory>(static provider => new PointListCodec(provider), "nested-list-native"),
+ new NativeFactory(static provider => new EnvelopeCodec(provider)),
+ new NativeFactory>(static provider => new PointListCodec(provider)),
new RoutedPointFactory(new PointAdapter(routedPoint))
];
}
From 477e46457bc4fbcf58495d54674f886dd6afda44 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:51:41 +0800
Subject: [PATCH 053/399] test: migrate rollback plugin to codec hash identity
---
.../RollbackManifest.cs | 35 ++++++++++++++-----
1 file changed, 27 insertions(+), 8 deletions(-)
diff --git a/test/SharpLink.RollbackPlugin/RollbackManifest.cs b/test/SharpLink.RollbackPlugin/RollbackManifest.cs
index 0fdf3b797..a8571e161 100644
--- a/test/SharpLink.RollbackPlugin/RollbackManifest.cs
+++ b/test/SharpLink.RollbackPlugin/RollbackManifest.cs
@@ -29,6 +29,16 @@ public sealed class RollbackManifest : ISharpLinkGeneratedAssemblyManifest
{
public RollbackManifest()
{
+ var identity = Environment.GetEnvironmentVariable("SHARPLINK_ROLLBACK_CODEC_IDENTITY") ?? "default";
+ var codecHash = ComputeIdentityHash(identity);
+ RpcAssemblyHash = new RpcHash128(0x726f6c6c6261636bUL, codecHash.Low);
+ Codecs = string.Equals(
+ Environment.GetEnvironmentVariable("SHARPLINK_ROLLBACK_DISABLE_CODEC"),
+ "1",
+ StringComparison.Ordinal)
+ ? []
+ : [new RollbackCodecFactory(codecHash)];
+
var started = RollbackState.ManifestConstructionStarted;
if (started is null)
return;
@@ -41,22 +51,32 @@ public RollbackManifest()
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "rollback-test";
public Assembly OwnerAssembly => typeof(RollbackManifest).Assembly;
+ public RpcHash128 RpcAssemblyHash { get; }
public string CompileTimeDescriptor => "rollback-test";
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
- public IReadOnlyList Codecs { get; } =
- string.Equals(Environment.GetEnvironmentVariable("SHARPLINK_ROLLBACK_DISABLE_CODEC"), "1", StringComparison.Ordinal)
- ? []
- : [new RollbackCodecFactory(Environment.GetEnvironmentVariable("SHARPLINK_ROLLBACK_SCHEMA") ?? "default")];
+ public IReadOnlyList Codecs { get; }
public IReadOnlyList ContractCodecs => [];
public IReadOnlyList Dependencies => [];
+
+ private static RpcHash128 ComputeIdentityHash(string value)
+ {
+ const ulong offset = 14695981039346656037UL;
+ const ulong prime = 1099511628211UL;
+ var low = offset;
+ foreach (var character in value)
+ {
+ low ^= character;
+ low *= prime;
+ }
+ return new RpcHash128(0x726f6c6c6261636bUL, low == 0 ? 1UL : low);
+ }
}
-internal sealed class RollbackCodecFactory(string schemaId) : IRpcGeneratedCodecFactory
+internal sealed class RollbackCodecFactory(RpcHash128 codecHash) : IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(string);
- public string SchemaId { get; } = schemaId;
- public string WireFormatId => "rollback-wire/v1";
+ public RpcHash128 CodecHash { get; } = codecHash;
public string AdapterId => "rollback-adapter/v1";
public IRpcCodecAdapter Adapter { get; } = new RollbackAdapter();
@@ -69,7 +89,6 @@ public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapt
internal sealed class RollbackAdapter : IRpcCodecAdapter
{
public string AdapterId => "rollback-adapter/v1";
- public string WireFormatId => "rollback-wire/v1";
public IRpcCodecAdapterScope CreateScope() => new RollbackScope();
}
From 88c43186d0bb69784ed79d5595f9841c7970c083 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:52:17 +0800
Subject: [PATCH 054/399] test: preserve rollback identity injection
compatibility
---
test/SharpLink.RollbackPlugin/RollbackManifest.cs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/test/SharpLink.RollbackPlugin/RollbackManifest.cs b/test/SharpLink.RollbackPlugin/RollbackManifest.cs
index a8571e161..f5ba4d8ad 100644
--- a/test/SharpLink.RollbackPlugin/RollbackManifest.cs
+++ b/test/SharpLink.RollbackPlugin/RollbackManifest.cs
@@ -29,7 +29,9 @@ public sealed class RollbackManifest : ISharpLinkGeneratedAssemblyManifest
{
public RollbackManifest()
{
- var identity = Environment.GetEnvironmentVariable("SHARPLINK_ROLLBACK_CODEC_IDENTITY") ?? "default";
+ var identity = Environment.GetEnvironmentVariable("SHARPLINK_ROLLBACK_CODEC_IDENTITY") ??
+ Environment.GetEnvironmentVariable("SHARPLINK_ROLLBACK_SCHEMA") ??
+ "default";
var codecHash = ComputeIdentityHash(identity);
RpcAssemblyHash = new RpcHash128(0x726f6c6c6261636bUL, codecHash.Low);
Codecs = string.Equals(
From 461f3fb74450dfc00e6c88331783dc34dce19265 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 14:52:51 +0800
Subject: [PATCH 055/399] test: migrate codec ownership identity fixtures
---
...pcManifestCodecOwnershipRegressionTests.cs | 21 +++++++------------
1 file changed, 8 insertions(+), 13 deletions(-)
diff --git a/test/SharpLink.UnitTests/Runtime/RpcManifestCodecOwnershipRegressionTests.cs b/test/SharpLink.UnitTests/Runtime/RpcManifestCodecOwnershipRegressionTests.cs
index 5771c6768..be7078005 100644
--- a/test/SharpLink.UnitTests/Runtime/RpcManifestCodecOwnershipRegressionTests.cs
+++ b/test/SharpLink.UnitTests/Runtime/RpcManifestCodecOwnershipRegressionTests.cs
@@ -149,7 +149,6 @@ public SharedValue Deserialize(in ReadOnlySequence buffer)
private sealed class SharedAdapter(SharedScopeState state) : IRpcCodecAdapter
{
public string AdapterId => "shared-owner-lifetime/v1";
- public string WireFormatId => "shared-owner-wire/v1";
public IRpcCodecAdapterScope CreateScope() => new SharedAdapterScope(state);
}
@@ -163,12 +162,10 @@ public IRpcCodec CreateCodec()
public void Dispose() => state.Disposed = true;
}
- private sealed class NativeFactory(Func> create, string schemaId)
- : IRpcGeneratedCodecFactory
+ private sealed class NativeFactory(Func> create)
+ : ITestGeneratedCodecFactory
{
public Type TargetType => typeof(T);
- public string SchemaId { get; } = schemaId;
- public string WireFormatId => "sharplink-native/v1";
public string? AdapterId => null;
public IRpcCodecAdapter? Adapter => null;
@@ -182,11 +179,9 @@ public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapt
public bool IsCompatibleCodec(IRpcCodec codec) => codec is IRpcCodec;
}
- private sealed class SharedAdapterFactory(SharedAdapter adapter) : IRpcGeneratedCodecFactory
+ private sealed class SharedAdapterFactory(SharedAdapter adapter) : ITestGeneratedCodecFactory
{
public Type TargetType => typeof(SharedValue);
- public string SchemaId => "shared-owner-schema/v1";
- public string WireFormatId => adapter.WireFormatId;
public string? AdapterId => adapter.AdapterId;
public IRpcCodecAdapter? Adapter => adapter;
public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapterScope)
@@ -195,7 +190,7 @@ public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapt
}
private sealed class PolicyManifest(Assembly ownerAssembly, PolicyPointCodec codec)
- : ISharpLinkGeneratedAssemblyManifest
+ : ITestGeneratedManifest
{
public int ApiVersion => SharpLinkGeneratedManifestVersions.Api;
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
@@ -206,12 +201,12 @@ private sealed class PolicyManifest(Assembly ownerAssembly, PolicyPointCodec cod
public IReadOnlyList Services => [];
public IReadOnlyList Codecs => [];
public IReadOnlyList ContractCodecs { get; } =
- [new NativeFactory(_ => codec, "policy-point/v1")];
+ [new NativeFactory(_ => codec)];
public IReadOnlyList Dependencies => [];
}
private sealed class SharedAdapterManifest(Assembly ownerAssembly, SharedAdapter adapter)
- : ISharpLinkGeneratedAssemblyManifest
+ : ITestGeneratedManifest
{
public int ApiVersion => SharpLinkGeneratedManifestVersions.Api;
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
@@ -226,7 +221,7 @@ private sealed class SharedAdapterManifest(Assembly ownerAssembly, SharedAdapter
}
private sealed class IncomingGeneratedManifest(Assembly ownerAssembly)
- : ISharpLinkGeneratedAssemblyManifest
+ : ITestGeneratedManifest
{
public int ApiVersion => SharpLinkGeneratedManifestVersions.Api;
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
@@ -236,7 +231,7 @@ private sealed class IncomingGeneratedManifest(Assembly ownerAssembly)
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
public IReadOnlyList Codecs { get; } =
- [new NativeFactory(static _ => new IncomingValueCodec(), "incoming-generated/v1")];
+ [new NativeFactory(static _ => new IncomingValueCodec())];
public IReadOnlyList ContractCodecs => [];
public IReadOnlyList Dependencies => [];
}
From 99b977384c4a540d1d0676f03b581577edc76fdc Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 15:02:23 +0800
Subject: [PATCH 056/399] test: default synthetic generated identities
---
.../TestGeneratedIdentityDefaults.cs | 13 +++++++++++++
1 file changed, 13 insertions(+)
create mode 100644 test/SharpLink.UnitTests/TestGeneratedIdentityDefaults.cs
diff --git a/test/SharpLink.UnitTests/TestGeneratedIdentityDefaults.cs b/test/SharpLink.UnitTests/TestGeneratedIdentityDefaults.cs
new file mode 100644
index 000000000..bc7f7fd56
--- /dev/null
+++ b/test/SharpLink.UnitTests/TestGeneratedIdentityDefaults.cs
@@ -0,0 +1,13 @@
+namespace SharpLink.UnitTests;
+
+internal interface ISharpLinkGeneratedAssemblyManifest : SharpLink.Abstractions.ISharpLinkGeneratedAssemblyManifest
+{
+ RpcHash128 SharpLink.Abstractions.ISharpLinkGeneratedAssemblyManifest.RpcAssemblyHash
+ => new(0x746573742d6d616eUL, 0x69666573742d7631UL);
+}
+
+internal interface IRpcGeneratedCodecFactory : SharpLink.Abstractions.IRpcGeneratedCodecFactory
+{
+ RpcHash128 SharpLink.Abstractions.IRpcGeneratedCodecFactory.CodecHash
+ => new(0x746573742d636f64UL, 0x65632d6861736831UL);
+}
From b5728dbb7c33492bc93561102840aaf241867cb7 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 15:04:29 +0800
Subject: [PATCH 057/399] revert: avoid shadowing generated interfaces
---
.../TestGeneratedIdentityDefaults.cs | 13 -------------
1 file changed, 13 deletions(-)
delete mode 100644 test/SharpLink.UnitTests/TestGeneratedIdentityDefaults.cs
diff --git a/test/SharpLink.UnitTests/TestGeneratedIdentityDefaults.cs b/test/SharpLink.UnitTests/TestGeneratedIdentityDefaults.cs
deleted file mode 100644
index bc7f7fd56..000000000
--- a/test/SharpLink.UnitTests/TestGeneratedIdentityDefaults.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace SharpLink.UnitTests;
-
-internal interface ISharpLinkGeneratedAssemblyManifest : SharpLink.Abstractions.ISharpLinkGeneratedAssemblyManifest
-{
- RpcHash128 SharpLink.Abstractions.ISharpLinkGeneratedAssemblyManifest.RpcAssemblyHash
- => new(0x746573742d6d616eUL, 0x69666573742d7631UL);
-}
-
-internal interface IRpcGeneratedCodecFactory : SharpLink.Abstractions.IRpcGeneratedCodecFactory
-{
- RpcHash128 SharpLink.Abstractions.IRpcGeneratedCodecFactory.CodecHash
- => new(0x746573742d636f64UL, 0x65632d6861736831UL);
-}
From 9b5d37415abd42e2ea298e516319a1cd6546bace Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 15:06:41 +0800
Subject: [PATCH 058/399] test: migrate dependency manifest identity
---
.../Client/RpcCodecRouteMultiClusterTests.cs | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/test/SharpLink.UnitTests/Client/RpcCodecRouteMultiClusterTests.cs b/test/SharpLink.UnitTests/Client/RpcCodecRouteMultiClusterTests.cs
index 06e3fbc3a..75491cb9d 100644
--- a/test/SharpLink.UnitTests/Client/RpcCodecRouteMultiClusterTests.cs
+++ b/test/SharpLink.UnitTests/Client/RpcCodecRouteMultiClusterTests.cs
@@ -20,6 +20,8 @@ public Task DependencyManifestViewShouldHideContractPolicy()
.Single();
var view = (ISharpLinkGeneratedAssemblyManifest)constructor.Invoke([source]);
+ Ensure(view.RpcAssemblyHash == source.RpcAssemblyHash,
+ "the dependency view must preserve the source assembly semantic identity");
Ensure(view.Codecs.Count == 0,
"the dependency view must not republish a Contract-owned Codec globally");
Ensure(view.ContractCodecs.Count == 0,
@@ -35,6 +37,7 @@ private sealed class RoutedDependencyManifest : ISharpLinkGeneratedAssemblyManif
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "test";
public Assembly OwnerAssembly => typeof(IOrdersContract).Assembly;
+ public RpcHash128 RpcAssemblyHash => new(0x72706f757465642dUL, 0x646570656e64656eUL);
public string CompileTimeDescriptor => "dependency-view-test";
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
@@ -46,8 +49,7 @@ private sealed class RoutedDependencyManifest : ISharpLinkGeneratedAssemblyManif
private sealed class ScopedFactory : IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(ScopedPayload);
- public string SchemaId => "scoped-dependency/v1";
- public string WireFormatId => HiddenPolicyAdapter.Instance.WireFormatId;
+ public RpcHash128 CodecHash => new(0x73636f7065642d64UL, 0x6570656e64656e63UL);
public string? AdapterId => HiddenPolicyAdapter.Instance.AdapterId;
public IRpcCodecAdapter? Adapter => HiddenPolicyAdapter.Instance;
public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapterScope)
@@ -59,7 +61,6 @@ private sealed class HiddenPolicyAdapter : IRpcCodecAdapter
{
internal static readonly HiddenPolicyAdapter Instance = new();
public string AdapterId => "hidden-dependency-policy/v1";
- public string WireFormatId => "hidden-dependency-wire/v1";
public IRpcCodecAdapterScope CreateScope()
=> throw new InvalidOperationException("hidden Contract policy adapter scope must not be created by a dependency view");
}
@@ -71,4 +72,4 @@ private static void Ensure(bool condition, string message)
if (!condition)
throw new InvalidOperationException(message);
}
-}
+}
\ No newline at end of file
From 5518b4b6a7ebb6cb66203ae054e473a7b1f84655 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 15:07:35 +0800
Subject: [PATCH 059/399] fix: preserve dependency manifest identity
---
src/SharpLink.Client/SharpLinkMultiClusterClientBuilder.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/SharpLink.Client/SharpLinkMultiClusterClientBuilder.cs b/src/SharpLink.Client/SharpLinkMultiClusterClientBuilder.cs
index 6a37be229..54a03ff9a 100644
--- a/src/SharpLink.Client/SharpLinkMultiClusterClientBuilder.cs
+++ b/src/SharpLink.Client/SharpLinkMultiClusterClientBuilder.cs
@@ -251,7 +251,7 @@ internal static SharpLinkPreparedCluster PrepareRuntimeCluster(
}
if (!assemblyOwners.TryAdd(route.ContractAssembly, cluster))
continue;
- AddManifestClosure(contractManifest, cluster, manifestsByCluster, manifestsByAssembly, includeContractPolicyDependencies: true);
+ AddManifestClosure(contractManifest, route.Cluster, manifestsByCluster, manifestsByAssembly, includeContractPolicyDependencies: true);
}
if (manifestsByCluster[cluster].Values.All(static manifest => manifest.Contracts.Count == 0) &&
@@ -565,6 +565,7 @@ private sealed class DependencyManifestView(ISharpLinkGeneratedAssemblyManifest
public int ProtocolVersion => source.ProtocolVersion;
public string GeneratorVersion => source.GeneratorVersion;
public Assembly OwnerAssembly => source.OwnerAssembly;
+ public RpcHash128 RpcAssemblyHash => source.RpcAssemblyHash;
public string CompileTimeDescriptor => source.CompileTimeDescriptor;
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
From 732b4ce13d49ddaca5c97f7b80eac700ae47e2e2 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 15:09:16 +0800
Subject: [PATCH 060/399] test: migrate build plan generated identities
---
.../Builder/BuildPlanBuilderTests.cs | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/test/SharpLink.UnitTests/Builder/BuildPlanBuilderTests.cs b/test/SharpLink.UnitTests/Builder/BuildPlanBuilderTests.cs
index 0751d9971..864850637 100644
--- a/test/SharpLink.UnitTests/Builder/BuildPlanBuilderTests.cs
+++ b/test/SharpLink.UnitTests/Builder/BuildPlanBuilderTests.cs
@@ -13,6 +13,8 @@ namespace SharpLink.UnitTests.Builder;
public sealed class BuildPlanBuilderTests
{
private const string ConsumedBuilderMessage = "This SharpLink builder has already been consumed.";
+ private static RpcHash128 SyntheticManifestHash => new(0x6275696c642d706cUL, 0x616e2d6d616e6966UL);
+ private static RpcHash128 SyntheticCodecHash => new(0x6275696c642d706cUL, 0x616e2d636f646563UL);
[Test]
public async Task CrossTopologyConfigurationShouldFailAtTheSecondCall()
@@ -781,6 +783,7 @@ private sealed class EmptyManifest : ISharpLinkGeneratedAssemblyManifest
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "phase11-test";
public Assembly OwnerAssembly => typeof(BuildPlanBuilderTests).Assembly;
+ public RpcHash128 RpcAssemblyHash => SyntheticManifestHash;
public string CompileTimeDescriptor => "phase11-empty";
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
@@ -794,6 +797,7 @@ private sealed class IncompatibleManifest : ISharpLinkGeneratedAssemblyManifest
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "phase11-test";
public Assembly OwnerAssembly => typeof(BuildPlanBuilderTests).Assembly;
+ public RpcHash128 RpcAssemblyHash => SyntheticManifestHash;
public string CompileTimeDescriptor => "phase11-incompatible";
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
@@ -807,6 +811,7 @@ private sealed class MalformedApi4Manifest : ISharpLinkGeneratedAssemblyManifest
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "phase11-test";
public Assembly OwnerAssembly => typeof(BuildPlanBuilderTests).Assembly;
+ public RpcHash128 RpcAssemblyHash => SyntheticManifestHash;
public string CompileTimeDescriptor => "phase11-malformed";
public IReadOnlyList Contracts => null!;
public IReadOnlyList Services => [];
@@ -820,6 +825,7 @@ private sealed class ForeignContractOwnershipManifest : ISharpLinkGeneratedAssem
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "phase11-test";
public Assembly OwnerAssembly => typeof(BuildPlanBuilderTests).Assembly;
+ public RpcHash128 RpcAssemblyHash => SyntheticManifestHash;
public string CompileTimeDescriptor => "phase11-foreign-contract";
public IReadOnlyList Contracts { get; } =
[
@@ -843,6 +849,7 @@ private sealed class DeferredAdapterManifest(DeferredAdapterCodecFactory factory
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "phase11-test";
public Assembly OwnerAssembly => typeof(BuildPlanBuilderTests).Assembly;
+ public RpcHash128 RpcAssemblyHash => SyntheticManifestHash;
public string CompileTimeDescriptor => "phase11-deferred-adapter";
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
@@ -856,8 +863,7 @@ private sealed class DeferredAdapterCodecFactory(DeferredAdapter adapter) : IRpc
internal int CodecCreateCount => Volatile.Read(ref _codecCreateCount);
public Type TargetType => typeof(DeferredCodecValue);
- public string SchemaId => "phase11-deferred-adapter/v1";
- public string WireFormatId => "phase11-deferred-wire/v1";
+ public RpcHash128 CodecHash => SyntheticCodecHash;
public string? AdapterId => "phase11-deferred-adapter/v1";
public IRpcCodecAdapter Adapter { get; } = adapter;
@@ -876,7 +882,6 @@ private sealed class DeferredAdapter : IRpcCodecAdapter
internal int ScopeCreateCount => Volatile.Read(ref _scopeCreateCount);
public string AdapterId => "phase11-deferred-adapter/v1";
- public string WireFormatId => "phase11-deferred-wire/v1";
public IRpcCodecAdapterScope CreateScope()
{
From ecc6c1640f6e0cd9e607888771904075bcd2a340 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 15:10:43 +0800
Subject: [PATCH 061/399] test: migrate builder rollback identities
---
.../Builder/BuilderOwnershipRollbackTests.cs | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/test/SharpLink.UnitTests/Builder/BuilderOwnershipRollbackTests.cs b/test/SharpLink.UnitTests/Builder/BuilderOwnershipRollbackTests.cs
index 9d3a628dc..1c287bbe0 100644
--- a/test/SharpLink.UnitTests/Builder/BuilderOwnershipRollbackTests.cs
+++ b/test/SharpLink.UnitTests/Builder/BuilderOwnershipRollbackTests.cs
@@ -13,6 +13,9 @@ namespace SharpLink.UnitTests.Builder;
public class BuilderOwnershipRollbackTests
{
+ private static RpcHash128 SyntheticManifestHash => new(0x6275696c6465722dUL, 0x726f6c6c6261636bUL);
+ private static RpcHash128 SyntheticCodecHash => new(0x6275696c6465722dUL, 0x636f6465632d7631UL);
+
[Test]
public void ClientProfileFailureShouldDisposeTransportAndPreserveBothFailures()
{
@@ -622,6 +625,7 @@ private sealed class ThrowingRuntimeContextManifest : ISharpLinkGeneratedAssembl
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "test";
public Assembly OwnerAssembly => typeof(ThrowingRuntimeContextManifest).Assembly;
+ public RpcHash128 RpcAssemblyHash => SyntheticManifestHash;
public string CompileTimeDescriptor => "builder-runtime-context-throw";
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
@@ -632,8 +636,7 @@ private sealed class ThrowingRuntimeContextManifest : ISharpLinkGeneratedAssembl
private sealed class ThrowingRuntimeContextCodecFactory : IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(CodecValue);
- public string SchemaId => "builder-runtime-context-throw/v1";
- public string WireFormatId => "builder-runtime-context-wire/v1";
+ public RpcHash128 CodecHash => SyntheticCodecHash;
public string? AdapterId => "builder-runtime-context-adapter/v1";
public IRpcCodecAdapter Adapter { get; } = new ThrowingRuntimeContextAdapter();
@@ -646,7 +649,6 @@ public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapt
private sealed class ThrowingRuntimeContextAdapter : IRpcCodecAdapter
{
public string AdapterId => "builder-runtime-context-adapter/v1";
- public string WireFormatId => "builder-runtime-context-wire/v1";
public IRpcCodecAdapterScope CreateScope()
=> throw new InvalidOperationException("controlled Runtime Context construction failure");
@@ -686,6 +688,7 @@ private sealed class RegistrationRollbackManifest : ISharpLinkGeneratedAssemblyM
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "test";
public Assembly OwnerAssembly => typeof(RegistrationRollbackManifest).Assembly;
+ public RpcHash128 RpcAssemblyHash => SyntheticManifestHash;
public string CompileTimeDescriptor => "builder-registration-rollback";
public IReadOnlyList Contracts { get; } =
[
From cc71a64be5c4c4f733589ebef82d76996fb385e8 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 15:11:26 +0800
Subject: [PATCH 062/399] test: migrate serializer generated identities
---
test/SharpLink.UnitTests/Builder/SerializerBuilderTests.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/test/SharpLink.UnitTests/Builder/SerializerBuilderTests.cs b/test/SharpLink.UnitTests/Builder/SerializerBuilderTests.cs
index dbec24f09..f044bf038 100644
--- a/test/SharpLink.UnitTests/Builder/SerializerBuilderTests.cs
+++ b/test/SharpLink.UnitTests/Builder/SerializerBuilderTests.cs
@@ -157,8 +157,7 @@ public void Serialize(in Payload value, IBufferWriter buffer)
private sealed class TaggedCodecFactory : IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(Payload);
- public string SchemaId => "generated-test-v1";
- public string WireFormatId => "sharplink-native/v1";
+ public RpcHash128 CodecHash => new(0x73657269616c697aUL, 0x65722d636f646563UL);
public string? AdapterId => null;
public IRpcCodecAdapter? Adapter => null;
public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapterScope)
@@ -174,6 +173,7 @@ private sealed class TaggedManifest : ISharpLinkGeneratedAssemblyManifest
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "test";
public Assembly OwnerAssembly => typeof(TaggedManifest).Assembly;
+ public RpcHash128 RpcAssemblyHash => new(0x73657269616c697aUL, 0x65722d6d616e6966UL);
public string CompileTimeDescriptor => "tagged-test";
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
From 7da67e061c16452be622c41d6e5657f84abed7be Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 15:12:46 +0800
Subject: [PATCH 063/399] test: migrate service registration manifest identity
---
test/SharpLink.UnitTests/Server/ServiceRegistrationTests.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/test/SharpLink.UnitTests/Server/ServiceRegistrationTests.cs b/test/SharpLink.UnitTests/Server/ServiceRegistrationTests.cs
index f6792f8e3..77dc294a8 100644
--- a/test/SharpLink.UnitTests/Server/ServiceRegistrationTests.cs
+++ b/test/SharpLink.UnitTests/Server/ServiceRegistrationTests.cs
@@ -448,6 +448,7 @@ private sealed class EmptyManifest(Assembly ownerAssembly) : ISharpLinkGenerated
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "test";
public Assembly OwnerAssembly { get; } = ownerAssembly;
+ public RpcHash128 RpcAssemblyHash => new(0x736572766963652dUL, 0x656d7074792d7631UL);
public string CompileTimeDescriptor => "test";
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
@@ -461,6 +462,7 @@ private sealed class StaticCleanupManifest : ISharpLinkGeneratedAssemblyManifest
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "test";
public Assembly OwnerAssembly => typeof(StaticCleanupManifest).Assembly;
+ public RpcHash128 RpcAssemblyHash => new(0x736572766963652dUL, 0x636c65616e75702dUL);
public string CompileTimeDescriptor => "service-cleanup";
public IReadOnlyList Contracts { get; } =
[
From 0f931e69b1f2d70191ba998eabe3b7c8b9bf82f5 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 15:14:55 +0800
Subject: [PATCH 064/399] test: migrate manifest source identities
---
.../Runtime/ManifestSourceIsolationTests.cs | 29 ++++++++++---------
1 file changed, 16 insertions(+), 13 deletions(-)
diff --git a/test/SharpLink.UnitTests/Runtime/ManifestSourceIsolationTests.cs b/test/SharpLink.UnitTests/Runtime/ManifestSourceIsolationTests.cs
index bc9af354c..9c8f2d8b7 100644
--- a/test/SharpLink.UnitTests/Runtime/ManifestSourceIsolationTests.cs
+++ b/test/SharpLink.UnitTests/Runtime/ManifestSourceIsolationTests.cs
@@ -14,6 +14,10 @@ namespace SharpLink.UnitTests.Runtime;
public sealed class ManifestSourceIsolationTests
{
+ private static RpcHash128 SyntheticManifestHash => new(0x6d616e6966657374UL, 0x2d736f757263652dUL);
+ private static RpcHash128 NativeCodecHash => new(0x6d616e6966657374UL, 0x2d6e61746976652dUL);
+ private static RpcHash128 ScopedCodecHash => new(0x6d616e6966657374UL, 0x2d73636f7065642dUL);
+
[Test]
public void RuntimeCompileShouldCaptureItsSourceExactlyOnceAndFreezeTheReturnedList()
{
@@ -464,17 +468,18 @@ private CodecManifest(string descriptor, IRpcGeneratedCodecFactory factory)
}
internal static CodecManifest For(string descriptor)
- => new(descriptor, new TestCodecFactory(descriptor));
+ => new(descriptor, new TestCodecFactory());
internal static CodecManifest ForDisposableScope(
string descriptor,
DisposableScopeCounters counters)
- => new(descriptor, new DisposableScopeCodecFactory(descriptor, counters));
+ => new(descriptor, new DisposableScopeCodecFactory(counters));
public int ApiVersion => SharpLinkGeneratedManifestVersions.Api;
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "phase15-test";
public Assembly OwnerAssembly => typeof(ManifestSourceIsolationTests).Assembly;
+ public RpcHash128 RpcAssemblyHash => SyntheticManifestHash;
public string CompileTimeDescriptor { get; }
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
@@ -504,6 +509,7 @@ internal static ContractManifest For(
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "phase15-test";
public Assembly OwnerAssembly => typeof(ManifestSourceIsolationTests).Assembly;
+ public RpcHash128 RpcAssemblyHash => SyntheticManifestHash;
public string CompileTimeDescriptor => "phase15-contract";
public IReadOnlyList Contracts { get; }
public IReadOnlyList Services => [];
@@ -534,7 +540,7 @@ internal static CompositeManifest ForClient(
(channel, _) => proxyFactory(channel),
static _ => new TestStub(8_301)),
service: null,
- new TestCodecFactory($"client-composite:{typeof(TCodec).FullName}"));
+ new TestCodecFactory());
internal static CompositeManifest ForServer(
Type contractType,
@@ -560,7 +566,7 @@ internal static CompositeManifest ForServer(
return new CompositeManifest(
contract,
service,
- new TestCodecFactory($"server-composite:{typeof(TCodec).FullName}"));
+ new TestCodecFactory());
}
private static SharpLinkGeneratedContractDescriptor CreateContract(
@@ -581,6 +587,7 @@ private static SharpLinkGeneratedContractDescriptor CreateContract(
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "phase15-test";
public Assembly OwnerAssembly => typeof(ManifestSourceIsolationTests).Assembly;
+ public RpcHash128 RpcAssemblyHash => SyntheticManifestHash;
public string CompileTimeDescriptor => "phase15-composite";
public IReadOnlyList Contracts { get; }
public IReadOnlyList Services { get; }
@@ -594,6 +601,7 @@ private sealed class IncompatibleCatalogPoisonManifest : ISharpLinkGeneratedAsse
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "phase15-global-poison";
public Assembly OwnerAssembly => typeof(ManifestSourceIsolationTests).Assembly;
+ public RpcHash128 RpcAssemblyHash => SyntheticManifestHash;
public string CompileTimeDescriptor => throw new InvalidOperationException("poison shape read");
public IReadOnlyList Contracts =>
throw new InvalidOperationException("poison shape read");
@@ -604,11 +612,10 @@ private sealed class IncompatibleCatalogPoisonManifest : ISharpLinkGeneratedAsse
public IReadOnlyList Dependencies => throw new InvalidOperationException("poison shape read");
}
- private sealed class TestCodecFactory(string schemaId) : IRpcGeneratedCodecFactory
+ private sealed class TestCodecFactory : IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(T);
- public string SchemaId { get; } = schemaId;
- public string WireFormatId => "sharplink-native/v1";
+ public RpcHash128 CodecHash => NativeCodecHash;
public string? AdapterId => null;
public IRpcCodecAdapter? Adapter => null;
public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapterScope)
@@ -627,13 +634,10 @@ public void Serialize(in T value, IBufferWriter buffer)
public T? Deserialize(in ReadOnlySequence buffer) => default;
}
- private sealed class DisposableScopeCodecFactory(
- string schemaId,
- DisposableScopeCounters counters) : IRpcGeneratedCodecFactory
+ private sealed class DisposableScopeCodecFactory(DisposableScopeCounters counters) : IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(T);
- public string SchemaId { get; } = schemaId;
- public string WireFormatId => "phase15-disposable/v1";
+ public RpcHash128 CodecHash => ScopedCodecHash;
public string AdapterId => "phase15.disposable-scope/v1";
public IRpcCodecAdapter Adapter { get; } = new DisposableScopeAdapter(counters);
public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapterScope)
@@ -644,7 +648,6 @@ public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapt
private sealed class DisposableScopeAdapter(DisposableScopeCounters counters) : IRpcCodecAdapter
{
public string AdapterId => "phase15.disposable-scope/v1";
- public string WireFormatId => "phase15-disposable/v1";
public IRpcCodecAdapterScope CreateScope()
{
Interlocked.Increment(ref counters.ScopeCreateCount);
From 948ead30588aa294a8e0f5c98c4f1b5a16a8df6c Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 15:19:33 +0800
Subject: [PATCH 065/399] style: add final newline
---
.../Client/RpcCodecRouteMultiClusterTests.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/SharpLink.UnitTests/Client/RpcCodecRouteMultiClusterTests.cs b/test/SharpLink.UnitTests/Client/RpcCodecRouteMultiClusterTests.cs
index 75491cb9d..287199119 100644
--- a/test/SharpLink.UnitTests/Client/RpcCodecRouteMultiClusterTests.cs
+++ b/test/SharpLink.UnitTests/Client/RpcCodecRouteMultiClusterTests.cs
@@ -72,4 +72,4 @@ private static void Ensure(bool condition, string message)
if (!condition)
throw new InvalidOperationException(message);
}
-}
\ No newline at end of file
+}
From 8ecdaa2e89981dc361683b73fb53631299cdea45 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 15:27:28 +0800
Subject: [PATCH 066/399] test: migrate multi-cluster generated identities
---
.../Client/SharpLinkMultiClusterClientTests.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/test/SharpLink.UnitTests/Client/SharpLinkMultiClusterClientTests.cs b/test/SharpLink.UnitTests/Client/SharpLinkMultiClusterClientTests.cs
index 9daae1286..7a146ea70 100644
--- a/test/SharpLink.UnitTests/Client/SharpLinkMultiClusterClientTests.cs
+++ b/test/SharpLink.UnitTests/Client/SharpLinkMultiClusterClientTests.cs
@@ -1678,6 +1678,7 @@ private sealed class Manifest : ISharpLinkGeneratedAssemblyManifest
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "test";
public Assembly OwnerAssembly => TestManifestAssembly;
+ public RpcHash128 RpcAssemblyHash => new(0x6d756c7469636c75UL, 0x737465722d763031UL);
public string CompileTimeDescriptor => "multi-cluster-test";
public IReadOnlyList Contracts { get; } =
[
@@ -1709,11 +1710,10 @@ private sealed class RouteManifest : ISharpLinkGeneratedClusterRouteManifest
];
}
- private sealed class TestCodecFactory(string schemaId) : IRpcGeneratedCodecFactory
+ private sealed class TestCodecFactory(string _) : IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(T);
- public string SchemaId { get; } = schemaId;
- public string WireFormatId => "sharplink-native/v1";
+ public RpcHash128 CodecHash => new(0x6d756c7469636c75UL, 0x737465722d636f64UL);
public string? AdapterId => null;
public IRpcCodecAdapter? Adapter => null;
public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapterScope)
From 9d7df4e3b3a44453e3d4cbb1d29df0b7c3ef0fc8 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 15:56:13 +0800
Subject: [PATCH 067/399] Temporarily suppress unread test fixture parameter
warning
---
test/SharpLink.UnitTests/Client/.editorconfig | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 test/SharpLink.UnitTests/Client/.editorconfig
diff --git a/test/SharpLink.UnitTests/Client/.editorconfig b/test/SharpLink.UnitTests/Client/.editorconfig
new file mode 100644
index 000000000..8cba9048f
--- /dev/null
+++ b/test/SharpLink.UnitTests/Client/.editorconfig
@@ -0,0 +1,2 @@
+[SharpLinkMultiClusterClientTests.cs]
+dotnet_diagnostic.CS9113.severity = none
From 7baac44c6318a47e1a282e9b8b0e679a48e02e41 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 15:59:02 +0800
Subject: [PATCH 068/399] Migrate static contract codec provider fixture
identity
---
.../Server/StaticContractCodecProviderRegressionTests.cs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/test/SharpLink.UnitTests/Server/StaticContractCodecProviderRegressionTests.cs b/test/SharpLink.UnitTests/Server/StaticContractCodecProviderRegressionTests.cs
index bf11c08d8..c9642a511 100644
--- a/test/SharpLink.UnitTests/Server/StaticContractCodecProviderRegressionTests.cs
+++ b/test/SharpLink.UnitTests/Server/StaticContractCodecProviderRegressionTests.cs
@@ -107,13 +107,14 @@ internal TwoContractManifest()
[],
static _ => new ContractBService())
];
- _contractCodecs = [new CustomFactory(SharedCodec, "test/assembly-shared")];
+ _contractCodecs = [new CustomFactory(SharedCodec)];
}
public int ApiVersion => SharpLinkGeneratedManifestVersions.Api;
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "test";
public Assembly OwnerAssembly => typeof(IContractA).Assembly;
+ public RpcHash128 RpcAssemblyHash => new(0x7374617469632d63UL, 0x6f6465632d6f776eUL);
public string CompileTimeDescriptor => "test";
public IReadOnlyList Contracts => _contracts;
public IReadOnlyList Services => _services;
@@ -125,12 +126,11 @@ internal TwoContractManifest()
internal IRpcCodec? CapturedA { get; private set; }
internal IRpcCodec? CapturedB { get; private set; }
- private sealed class CustomFactory(IRpcCodec codec, string schemaId)
+ private sealed class CustomFactory(IRpcCodec codec)
: IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(SharedPayload);
- public string SchemaId => schemaId;
- public string WireFormatId => "test/shared-payload/v1";
+ public RpcHash128 CodecHash => new(0x617373656d626c79UL, 0x2d7368617265642dUL);
public string? AdapterId => null;
public IRpcCodecAdapter? Adapter => null;
From 11a230662bd59a649fbc4518194b341bb90d7b60 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 16:01:13 +0800
Subject: [PATCH 069/399] Add identity to time budget test manifest
---
.../SharpLink.UnitTests/Client/SharpLinkClientTimeBudgetTests.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/test/SharpLink.UnitTests/Client/SharpLinkClientTimeBudgetTests.cs b/test/SharpLink.UnitTests/Client/SharpLinkClientTimeBudgetTests.cs
index 9f48032ef..2d665920b 100644
--- a/test/SharpLink.UnitTests/Client/SharpLinkClientTimeBudgetTests.cs
+++ b/test/SharpLink.UnitTests/Client/SharpLinkClientTimeBudgetTests.cs
@@ -369,6 +369,7 @@ private sealed class EmptyManifest : ISharpLinkGeneratedAssemblyManifest
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "test";
public Assembly OwnerAssembly => typeof(SharpLinkClientTimeBudgetTests).Assembly;
+ public RpcHash128 RpcAssemblyHash => new(0x74696d652d627564UL, 0x6765742d74657374UL);
public string CompileTimeDescriptor => "test";
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
From 9960c74b9fce65d6245369ae4656a5e9f5655d97 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 16:03:26 +0800
Subject: [PATCH 070/399] Migrate static endpoint rollback fixture identity
---
.../SharpLink.UnitTests/Client/StaticEndpointBuilderTests.cs | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/test/SharpLink.UnitTests/Client/StaticEndpointBuilderTests.cs b/test/SharpLink.UnitTests/Client/StaticEndpointBuilderTests.cs
index 815e09e84..c5c544b3f 100644
--- a/test/SharpLink.UnitTests/Client/StaticEndpointBuilderTests.cs
+++ b/test/SharpLink.UnitTests/Client/StaticEndpointBuilderTests.cs
@@ -623,6 +623,7 @@ private sealed class ThrowingScopeManifest : ISharpLinkGeneratedAssemblyManifest
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "test";
public Assembly OwnerAssembly => typeof(ThrowingScopeManifest).Assembly;
+ public RpcHash128 RpcAssemblyHash => new(0x6275696c6465722dUL, 0x726f6c6c6261636bUL);
public string CompileTimeDescriptor => "client-build-rollback";
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
@@ -633,8 +634,7 @@ private sealed class ThrowingScopeManifest : ISharpLinkGeneratedAssemblyManifest
private sealed class ThrowingScopeCodecFactory : IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(BuilderValue);
- public string SchemaId => "builder-value/v1";
- public string WireFormatId => "builder-wire/v1";
+ public RpcHash128 CodecHash => new(0x6275696c6465722dUL, 0x636f6465632d7631UL);
public string AdapterId => "builder-adapter/v1";
public IRpcCodecAdapter Adapter { get; } = new ThrowingScopeAdapter();
public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapterScope)
@@ -645,7 +645,6 @@ public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapt
private sealed class ThrowingScopeAdapter : IRpcCodecAdapter
{
public string AdapterId => "builder-adapter/v1";
- public string WireFormatId => "builder-wire/v1";
public IRpcCodecAdapterScope CreateScope() => new ThrowingScope();
}
From 0fc2a75b197a70875242ff63158c717006a0ac69 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 16:06:15 +0800
Subject: [PATCH 071/399] Migrate runtime context fixtures to deterministic
hashes
---
.../Runtime/SharpLinkRuntimeContextTests.cs | 42 +++++++++++--------
1 file changed, 25 insertions(+), 17 deletions(-)
diff --git a/test/SharpLink.UnitTests/Runtime/SharpLinkRuntimeContextTests.cs b/test/SharpLink.UnitTests/Runtime/SharpLinkRuntimeContextTests.cs
index 7bf4a3aed..8cc12db58 100644
--- a/test/SharpLink.UnitTests/Runtime/SharpLinkRuntimeContextTests.cs
+++ b/test/SharpLink.UnitTests/Runtime/SharpLinkRuntimeContextTests.cs
@@ -9,6 +9,8 @@ namespace SharpLink.UnitTests.Runtime;
public class SharpLinkRuntimeContextTests
{
private static readonly TimeSpan RaceCoordinationTimeout = TimeSpan.FromSeconds(10);
+ private static readonly RpcHash128 TestAssemblyHash =
+ new(0x72756e74696d652dUL, 0x746573742d763031UL);
[Test]
// This is the intentional default-global adapter test; the other RuntimeContext tests use fixed sources.
@@ -551,7 +553,7 @@ public void AdapterIdentityMismatchShouldRejectAndDisposePreparedScopes()
}
catch (InvalidOperationException exception)
{
- Ensure(exception.Message.Contains("runtime identity", StringComparison.Ordinal),
+ Ensure(exception.Message.Contains("lifecycle identity", StringComparison.Ordinal),
"identity mismatch is reported before publication");
}
@@ -590,8 +592,8 @@ public void EveryFactoryAdapterInstanceShouldMatchGeneratedIdentity()
}
catch (InvalidOperationException exception)
{
- Ensure(exception.Message.Contains("runtime identity", StringComparison.Ordinal),
- "a later same-type Adapter instance cannot bypass runtime identity validation");
+ Ensure(exception.Message.Contains("lifecycle identity", StringComparison.Ordinal),
+ "a later same-type Adapter instance cannot bypass generated AdapterId validation");
}
Ensure(preparedCounters.ScopeCreateCount == 1, "the first valid Adapter Scope was prepared");
@@ -660,14 +662,16 @@ public void ConflictingManifestCodecsShouldRollbackBothAdapterScopes()
new AlternateCountingAdapter(secondCounters),
AlternateCountingAdapter.Id,
AlternateCountingAdapter.Wire,
- schemaId: "incompatible-schema"))
+ codecHash: new RpcHash128(
+ 0x636f6e666c696374UL,
+ 0x2d636f6465632d32UL)))
]);
throw new Exception("expected generated Codec conflict");
}
catch (InvalidOperationException exception)
{
- Ensure(exception.Message.Contains("schema/wire", StringComparison.Ordinal),
- "same-target schema/wire conflict is rejected");
+ Ensure(exception.Message.Contains("Generated Codec conflict", StringComparison.Ordinal),
+ "same-target CodecHash conflict is rejected");
}
Ensure(firstCounters.ScopeDisposeCount == 1, "first Manifest Scope is rolled back");
@@ -1073,7 +1077,7 @@ public void AdapterFreeCustomWireCodecShouldBeAccepted()
context.PublishGeneratedCodecs(registration.Codecs);
Ensure(context.Codecs.GetCodec() is TaggedThirdAdapterValueCodec { Tag: 7 },
- "an adapter-free Codec with a custom wire-format identity must resolve through the generated registration");
+ "an adapter-free Codec with a custom deterministic identity must resolve through the generated registration");
}
private sealed class TaggedValue;
@@ -1115,8 +1119,7 @@ public void Serialize(in CatalogValue value, IBufferWriter buffer)
private sealed class CatalogCodecFactory : IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(CatalogValue);
- public string SchemaId => "catalog-test-v1";
- public string WireFormatId => "sharplink-native/v1";
+ public RpcHash128 CodecHash => new(0x636174616c6f672dUL, 0x636f6465632d7631UL);
public string? AdapterId => null;
public IRpcCodecAdapter? Adapter => null;
public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapterScope)
@@ -1132,6 +1135,7 @@ private sealed class CatalogManifest : ISharpLinkGeneratedAssemblyManifest
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "test";
public Assembly OwnerAssembly => typeof(CatalogManifest).Assembly;
+ public RpcHash128 RpcAssemblyHash => TestAssemblyHash;
public string CompileTimeDescriptor => "catalog-test";
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
@@ -1362,8 +1366,7 @@ public void Serialize(in ThirdAdapterValue value, IBufferWriter buffer)
private sealed class FixedNativeFactory(IRpcCodec codec) : IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(T);
- public string SchemaId => $"native:{typeof(T).FullName}";
- public string WireFormatId => "sharplink-native/v1";
+ public RpcHash128 CodecHash => new(0x66697865642d6e61UL, 0x746976652d763031UL);
public string? AdapterId => null;
public IRpcCodecAdapter? Adapter => null;
public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapterScope)
@@ -1376,7 +1379,7 @@ public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapt
private sealed class CustomWireFactory(IRpcCodec codec, string wireFormatId) : IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(T);
- public string SchemaId => $"custom:{typeof(T).FullName}";
+ public RpcHash128 CodecHash => new(0x637573746f6d2d63UL, 0x6f6465632d763031UL);
public string WireFormatId => wireFormatId;
public string? AdapterId => null;
public IRpcCodecAdapter? Adapter => null;
@@ -1393,8 +1396,7 @@ private sealed class BlockingNativeFactory(
TaskCompletionSource release) : IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(T);
- public string SchemaId => $"blocking-native:{typeof(T).FullName}";
- public string WireFormatId => "sharplink-native/v1";
+ public RpcHash128 CodecHash => new(0x626c6f636b696e67UL, 0x2d636f6465632d31UL);
public string? AdapterId => null;
public IRpcCodecAdapter? Adapter => null;
@@ -1413,8 +1415,7 @@ public IRpcCodec Create(IRpcCodecProvider provider, IRpcCodecAdapterScope? adapt
private sealed class AdapterFactory(AdapterCounters counters) : IRpcGeneratedCodecFactory
{
public Type TargetType => typeof(T);
- public string SchemaId => $"adapter:{typeof(T).FullName}";
- public string WireFormatId => "test-wire/v1";
+ public RpcHash128 CodecHash => new(0x616461707465722dUL, 0x636f6465632d7631UL);
public string? AdapterId => "test.adapter/v1";
public IRpcCodecAdapter Adapter { get; } = new CountingAdapter(counters);
@@ -1432,16 +1433,21 @@ internal ConfigurableAdapterFactory(
string adapterId,
string wireFormatId,
IRpcCodec? codec = null,
- string? schemaId = null)
+ string? schemaId = null,
+ RpcHash128 codecHash = default)
{
Adapter = adapter;
AdapterId = adapterId;
WireFormatId = wireFormatId;
SchemaId = schemaId ?? $"adapter:{typeof(T).FullName}";
+ CodecHash = codecHash.IsEmpty
+ ? new RpcHash128(0x636f6e6669672d61UL, 0x6461707465722d31UL)
+ : codecHash;
_codec = codec;
}
public Type TargetType => typeof(T);
+ public RpcHash128 CodecHash { get; }
public string SchemaId { get; }
public string WireFormatId { get; }
public string AdapterId { get; }
@@ -1459,6 +1465,7 @@ private sealed class AdapterManifest(AdapterCounters counters, bool includeSecon
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "test";
public Assembly OwnerAssembly => typeof(AdapterManifest).Assembly;
+ public RpcHash128 RpcAssemblyHash => TestAssemblyHash;
public string CompileTimeDescriptor => "adapter-test";
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
@@ -1475,6 +1482,7 @@ private sealed class TestManifest(string descriptor, params IRpcGeneratedCodecFa
public int ProtocolVersion => SharpLinkGeneratedManifestVersions.Protocol;
public string GeneratorVersion => "test";
public Assembly OwnerAssembly => typeof(TestManifest).Assembly;
+ public RpcHash128 RpcAssemblyHash => TestAssemblyHash;
public string CompileTimeDescriptor => descriptor;
public IReadOnlyList Contracts => [];
public IReadOnlyList Services => [];
From e1c51a14d5c386611dc13b150d6297e7c3401b89 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 16:10:25 +0800
Subject: [PATCH 072/399] Trim legacy runtime fixture identity plumbing
---
.../Runtime/SharpLinkRuntimeContextTests.cs | 12 +-----------
1 file changed, 1 insertion(+), 11 deletions(-)
diff --git a/test/SharpLink.UnitTests/Runtime/SharpLinkRuntimeContextTests.cs b/test/SharpLink.UnitTests/Runtime/SharpLinkRuntimeContextTests.cs
index 8cc12db58..522fd0af1 100644
--- a/test/SharpLink.UnitTests/Runtime/SharpLinkRuntimeContextTests.cs
+++ b/test/SharpLink.UnitTests/Runtime/SharpLinkRuntimeContextTests.cs
@@ -9,8 +9,7 @@ namespace SharpLink.UnitTests.Runtime;
public class SharpLinkRuntimeContextTests
{
private static readonly TimeSpan RaceCoordinationTimeout = TimeSpan.FromSeconds(10);
- private static readonly RpcHash128 TestAssemblyHash =
- new(0x72756e74696d652dUL, 0x746573742d763031UL);
+ private static readonly RpcHash128 TestAssemblyHash = new(0x72756e74696d652dUL, 0x746573742d763031UL);
[Test]
// This is the intentional default-global adapter test; the other RuntimeContext tests use fixed sources.
@@ -1157,7 +1156,6 @@ public CountingAdapter()
internal CountingAdapter(AdapterCounters counters) => _counters = counters;
public string AdapterId => Id;
- public string WireFormatId => Wire;
public IRpcCodecAdapterScope CreateScope()
{
@@ -1180,7 +1178,6 @@ public AlternateCountingAdapter()
internal AlternateCountingAdapter(AdapterCounters counters) => _counters = counters;
public string AdapterId => Id;
- public string WireFormatId => Wire;
public IRpcCodecAdapterScope CreateScope()
{
@@ -1236,7 +1233,6 @@ public ThrowingDisposeAdapter()
internal ThrowingDisposeAdapter(AdapterCounters counters) => _counters = counters;
public string AdapterId => Id;
- public string WireFormatId => Wire;
public IRpcCodecAdapterScope CreateScope()
{
@@ -1264,7 +1260,6 @@ internal FailingScopeAdapter(AdapterCounters counters, bool returnNull)
}
public string AdapterId => Id;
- public string WireFormatId => Wire;
public IRpcCodecAdapterScope CreateScope()
{
@@ -1433,13 +1428,10 @@ internal ConfigurableAdapterFactory(
string adapterId,
string wireFormatId,
IRpcCodec? codec = null,
- string? schemaId = null,
RpcHash128 codecHash = default)
{
Adapter = adapter;
AdapterId = adapterId;
- WireFormatId = wireFormatId;
- SchemaId = schemaId ?? $"adapter:{typeof(T).FullName}";
CodecHash = codecHash.IsEmpty
? new RpcHash128(0x636f6e6669672d61UL, 0x6461707465722d31UL)
: codecHash;
@@ -1448,8 +1440,6 @@ internal ConfigurableAdapterFactory(
public Type TargetType => typeof(T);
public RpcHash128 CodecHash { get; }
- public string SchemaId { get; }
- public string WireFormatId { get; }
public string AdapterId { get; }
public IRpcCodecAdapter Adapter { get; }
From db67d1a9a0aea20b7232e0b109f9b1a7bad37832 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 16:23:39 +0800
Subject: [PATCH 073/399] Avoid manifest emission after codec diagnostics
---
src/SharpLink.Generator/RpcGenerator.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/SharpLink.Generator/RpcGenerator.cs b/src/SharpLink.Generator/RpcGenerator.cs
index 7f4d91a82..268f69a41 100644
--- a/src/SharpLink.Generator/RpcGenerator.cs
+++ b/src/SharpLink.Generator/RpcGenerator.cs
@@ -313,6 +313,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var manifest = boundInterfaces.Collect().Combine(services.Collect()).Combine(generatedCodecs);
context.RegisterSourceOutput(manifest, static (spc, value) =>
{
+ if (!value.Right.Diagnostics.IsDefaultOrEmpty)
+ return;
+
var interfaces = value.Left.Left;
var services = value.Left.Right;
var codecs = value.Right.Codecs;
From 5d2235e01923c1e56b15c9c4a8d1b5b9ceadd122 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 16:31:16 +0800
Subject: [PATCH 074/399] Fix deterministic identity generator fixtures
---
.../RpcDeterministicIdentityTests.cs | 50 +++++++++----------
1 file changed, 24 insertions(+), 26 deletions(-)
diff --git a/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs b/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
index 2dd15b562..a1fe55191 100644
--- a/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
+++ b/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
@@ -106,26 +106,22 @@ private static string GenerateDtoIdentityManifest(bool includeExtraMember, bool
var extraMember = includeExtraMember
? "public long Extra { get; set; }"
: string.Empty;
- var methodAttribute = idempotent ? "[Idempotent]" : string.Empty;
- var source = $$"""
-using System.Threading;
-using System.Threading.Tasks;
-using SharpLink.Sdk;
-
-[RpcSerializable]
+ var methodAttribute = idempotent ? "[SharpLink.Sdk.Idempotent]" : string.Empty;
+ var source = BuildSource($$"""
+[SharpLink.Sdk.RpcSerializable]
public sealed class DeterministicPayload
{
public int Value { get; set; }
{{extraMember}}
}
-[RpcContract]
-public interface IDeterministicIdentityContract : IService
+[SharpLink.Sdk.RpcContract]
+public interface IDeterministicIdentityContract : SharpLink.Sdk.IService
{
{{methodAttribute}}
ValueTask Echo(DeterministicPayload value, CancellationToken cancellationToken);
}
-""";
+""");
return RunGeneratorAndGetSources(source)
.Single(static generated =>
@@ -137,36 +133,38 @@ private static string GenerateOpaqueIdentityManifest(
ulong semanticHigh,
ulong semanticLow)
{
- var source = $$"""
-using System;
-using System.Buffers;
-using System.Threading;
-using System.Threading.Tasks;
-using SharpLink.Abstractions;
-using SharpLink.Sdk;
+ var source = BuildSource($$"""
+namespace SharpLink.Sdk
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
+ public sealed class RpcCodecSemanticIdentityAttribute : Attribute
+ {
+ public RpcCodecSemanticIdentityAttribute(ulong high, ulong low) { }
+ }
+}
-[RpcSerializable]
-[RpcCodec(typeof(OpaquePayloadCodec))]
+[SharpLink.Sdk.RpcSerializable]
+[SharpLink.Sdk.RpcCodec(typeof(OpaquePayloadCodec))]
public sealed class OpaquePayload
{
public int Value { get; set; }
}
-[RpcCodecSemanticIdentity({{semanticHigh}}UL, {{semanticLow}}UL)]
-public sealed class OpaquePayloadCodec : IRpcCodec
+[SharpLink.Sdk.RpcCodecSemanticIdentity({{semanticHigh}}UL, {{semanticLow}}UL)]
+public sealed class OpaquePayloadCodec : SharpLink.Abstractions.IRpcCodec
{
private const string ImplementationMarker = "{{implementationMarker}}";
- public void Serialize(in OpaquePayload value, IBufferWriter buffer) { _ = ImplementationMarker; }
- public OpaquePayload Deserialize(in ReadOnlySequence buffer) => new();
+ public void Serialize(in OpaquePayload value, System.Buffers.IBufferWriter buffer) { _ = ImplementationMarker; }
+ public OpaquePayload Deserialize(in System.Buffers.ReadOnlySequence buffer) => new();
}
-[RpcContract]
-public interface IOpaqueIdentityContract : IService
+[SharpLink.Sdk.RpcContract]
+public interface IOpaqueIdentityContract : SharpLink.Sdk.IService
{
ValueTask Echo(OpaquePayload value, CancellationToken cancellationToken);
}
-""";
+""");
return RunGeneratorAndGetSources(source)
.Single(static generated =>
From 4c7a324aef48745bc2e2797a1cfc3e487e0a760a Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 16:31:47 +0800
Subject: [PATCH 075/399] Provide deterministic identity test attributes
---
.../RpcDeterministicIdentityTests.cs | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs b/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
index a1fe55191..892be54c4 100644
--- a/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
+++ b/test/SharpLink.Generator.Tests/RpcDeterministicIdentityTests.cs
@@ -108,6 +108,12 @@ private static string GenerateDtoIdentityManifest(bool includeExtraMember, bool
: string.Empty;
var methodAttribute = idempotent ? "[SharpLink.Sdk.Idempotent]" : string.Empty;
var source = BuildSource($$"""
+namespace SharpLink.Sdk
+{
+ [AttributeUsage(AttributeTargets.Method)]
+ public sealed class IdempotentAttribute : Attribute { }
+}
+
[SharpLink.Sdk.RpcSerializable]
public sealed class DeterministicPayload
{
From b1898e65306cf520280ece45d633b51a508fa02c Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 16:39:23 +0800
Subject: [PATCH 076/399] Add current identity test source shim
---
.../RpcIdentityTestSources.cs | 30 +++++++++++++++++++
1 file changed, 30 insertions(+)
create mode 100644 test/SharpLink.Generator.Tests/RpcIdentityTestSources.cs
diff --git a/test/SharpLink.Generator.Tests/RpcIdentityTestSources.cs b/test/SharpLink.Generator.Tests/RpcIdentityTestSources.cs
new file mode 100644
index 000000000..262ae07e9
--- /dev/null
+++ b/test/SharpLink.Generator.Tests/RpcIdentityTestSources.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace SharpLink.Generator.Tests;
+
+public partial class RpcAnalyzerTests
+{
+ private static string UseCurrentIdentitySdk(string source)
+ {
+ source = source.Replace(
+ "public RpcCodecAdapterRegistrationAttribute(Type adapterType, string adapterId, string wireFormatId) { }",
+ "public RpcCodecAdapterRegistrationAttribute(Type adapterType, string adapterId) { }",
+ StringComparison.Ordinal);
+ return source.Replace(
+ """
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
+ public sealed class RpcCodecImplementationAttribute : Attribute
+ {
+ public RpcCodecImplementationAttribute(string wireFormatId, string schemaId) { }
+ }
+""",
+ """
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
+ public sealed class RpcCodecSemanticIdentityAttribute : Attribute
+ {
+ public RpcCodecSemanticIdentityAttribute(ulong high, ulong low) { }
+ }
+""",
+ StringComparison.Ordinal);
+ }
+}
From af7ad0d314e2d4e5d441b1f3987bed70b0210d36 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 16:39:43 +0800
Subject: [PATCH 077/399] Migrate tuple codec fixtures to semantic identity
---
.../RpcCodecFifthReviewRegressionTests.cs | 24 ++++++++++---------
1 file changed, 13 insertions(+), 11 deletions(-)
diff --git a/test/SharpLink.Generator.Tests/RpcCodecFifthReviewRegressionTests.cs b/test/SharpLink.Generator.Tests/RpcCodecFifthReviewRegressionTests.cs
index 736b7bd9f..a62b80ce5 100644
--- a/test/SharpLink.Generator.Tests/RpcCodecFifthReviewRegressionTests.cs
+++ b/test/SharpLink.Generator.Tests/RpcCodecFifthReviewRegressionTests.cs
@@ -9,13 +9,14 @@ public partial class RpcAnalyzerTests
[Test]
public Task CanonicalTupleAliasBindingsShouldDiagnoseConflictingAdapters()
{
- var source = AddAssemblyAttributes(BuildSource("""
+ var source = AddAssemblyAttributes(UseCurrentIdentitySdk(BuildSource("""
[SharpLink.Sdk.RpcContract]
public interface IAliasConflictContract : SharpLink.Sdk.IService
{
ValueTask> Echo(List<(int X, int Y)> value, CancellationToken cancellationToken);
}
+[SharpLink.Sdk.RpcCodecSemanticIdentity(1UL, 1UL)]
public sealed class AliasAdapterA : SharpLink.Abstractions.IRpcCodecAdapter
{
public string AdapterId => "alias-a/v1";
@@ -23,15 +24,16 @@ public sealed class AliasAdapterA : SharpLink.Abstractions.IRpcCodecAdapter
public SharpLink.Abstractions.IRpcCodecAdapterScope CreateScope() => throw new NotImplementedException();
}
+[SharpLink.Sdk.RpcCodecSemanticIdentity(2UL, 2UL)]
public sealed class AliasAdapterB : SharpLink.Abstractions.IRpcCodecAdapter
{
public string AdapterId => "alias-b/v1";
public string WireFormatId => "alias-b-wire/v1";
public SharpLink.Abstractions.IRpcCodecAdapterScope CreateScope() => throw new NotImplementedException();
}
-"""),
- "[assembly: SharpLink.Sdk.RpcCodecAdapterRegistration(typeof(AliasAdapterA), \"alias-a/v1\", \"alias-a-wire/v1\")]",
- "[assembly: SharpLink.Sdk.RpcCodecAdapterRegistration(typeof(AliasAdapterB), \"alias-b/v1\", \"alias-b-wire/v1\")]",
+""")),
+ "[assembly: SharpLink.Sdk.RpcCodecAdapterRegistration(typeof(AliasAdapterA), \"alias-a/v1\")]",
+ "[assembly: SharpLink.Sdk.RpcCodecAdapterRegistration(typeof(AliasAdapterB), \"alias-b/v1\")]",
"[assembly: SharpLink.Sdk.RpcCodecAdapter(typeof(List<(int X, int Y)>), typeof(AliasAdapterA))]",
"[assembly: SharpLink.Sdk.RpcCodecAdapter(typeof(List>), typeof(AliasAdapterB))]");
@@ -44,8 +46,8 @@ public sealed class AliasAdapterB : SharpLink.Abstractions.IRpcCodecAdapter
[Test]
public Task CanonicalTupleAliasCustomCodecShouldValidateAgainstClrIdentity()
{
- var source = AddAssemblyAttributes(BuildSource("""
-[SharpLink.Sdk.RpcCodecImplementation("alias-custom-wire/v1", "alias-custom-schema/v1")]
+ var source = AddAssemblyAttributes(UseCurrentIdentitySdk(BuildSource("""
+[SharpLink.Sdk.RpcCodecSemanticIdentity(3UL, 3UL)]
public sealed class AliasCustomCodec : SharpLink.Abstractions.IRpcCodec>
{
}
@@ -55,7 +57,7 @@ public interface IAliasCustomContract : SharpLink.Sdk.IService
{
ValueTask> Echo(List<(int X, int Y)> value, CancellationToken cancellationToken);
}
-"""),
+""")),
"[assembly: SharpLink.Sdk.RpcCodec(typeof(List>), typeof(AliasCustomCodec))]");
var diagnostics = RunGenerator(source);
@@ -70,11 +72,11 @@ public interface IAliasCustomContract : SharpLink.Sdk.IService
[Test]
public Task CanonicalTupleAliasBindingsShouldDiagnoseConflictingCustomCodecs()
{
- var source = AddAssemblyAttributes(BuildSource("""
-[SharpLink.Sdk.RpcCodecImplementation("alias-custom-a/v1", "alias-custom-a-schema/v1")]
+ var source = AddAssemblyAttributes(UseCurrentIdentitySdk(BuildSource("""
+[SharpLink.Sdk.RpcCodecSemanticIdentity(4UL, 4UL)]
public sealed class AliasCustomCodecA : SharpLink.Abstractions.IRpcCodec> { }
-[SharpLink.Sdk.RpcCodecImplementation("alias-custom-b/v1", "alias-custom-b-schema/v1")]
+[SharpLink.Sdk.RpcCodecSemanticIdentity(5UL, 5UL)]
public sealed class AliasCustomCodecB : SharpLink.Abstractions.IRpcCodec>> { }
[SharpLink.Sdk.RpcContract]
@@ -82,7 +84,7 @@ public interface IAliasCustomConflictContract : SharpLink.Sdk.IService
{
ValueTask> Echo(List<(int X, int Y)> value, CancellationToken cancellationToken);
}
-"""),
+""")),
"[assembly: SharpLink.Sdk.RpcCodec(typeof(List<(int X, int Y)>), typeof(AliasCustomCodecA))]",
"[assembly: SharpLink.Sdk.RpcCodec(typeof(List>), typeof(AliasCustomCodecB))]");
From 012cd73243769b05cb6f4aba673400953b731b8d Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 16:40:13 +0800
Subject: [PATCH 078/399] Migrate codec policy fixtures to semantic identity
---
.../RpcCodecPolicyRegressionTests.cs | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/test/SharpLink.Generator.Tests/RpcCodecPolicyRegressionTests.cs b/test/SharpLink.Generator.Tests/RpcCodecPolicyRegressionTests.cs
index ce920804e..717599b51 100644
--- a/test/SharpLink.Generator.Tests/RpcCodecPolicyRegressionTests.cs
+++ b/test/SharpLink.Generator.Tests/RpcCodecPolicyRegressionTests.cs
@@ -9,7 +9,7 @@ public partial class RpcAnalyzerTests
[Test]
public Task ExplicitBindingMatchingSelectorShouldStillBeContractOwned()
{
- var source = AddAssemblyAttributes(BuildRouteSource("""
+ var source = AddAssemblyAttributes(UseCurrentIdentitySdk(BuildRouteSource("""
[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)]
public sealed class SelectorAttribute : System.Attribute { }
@@ -26,13 +26,14 @@ public interface ISelectorExplicitContract : SharpLink.Sdk.IService
ValueTask Echo(SelectorPayload value, CancellationToken cancellationToken);
}
+[SharpLink.Sdk.RpcCodecSemanticIdentity(0x1001UL, 0x2001UL)]
public sealed class SelectorAdapter : TestRouteAdapterBase
{
public override string AdapterId => "selector.explicit/v1";
public override string WireFormatId => "selector-explicit-wire/v1";
}
-"""),
- "[assembly: SharpLink.Sdk.RpcCodecAdapterRegistration(typeof(SelectorAdapter), \"selector.explicit/v1\", \"selector-explicit-wire/v1\", SelectorAttributeType = typeof(SelectorAttribute))]");
+""")),
+ "[assembly: SharpLink.Sdk.RpcCodecAdapterRegistration(typeof(SelectorAdapter), \"selector.explicit/v1\", SelectorAttributeType = typeof(SelectorAttribute))]");
var sources = RunGeneratorAndGetSources(source);
var generated = string.Join("\n", sources);
@@ -57,7 +58,7 @@ public sealed class SelectorAdapter : TestRouteAdapterBase
[Test]
public Task AllRouteShouldNotCaptureFrameworkEnum()
{
- var source = AddAssemblyAttributes(BuildRouteSource("""
+ var source = AddAssemblyAttributes(UseCurrentIdentitySdk(BuildRouteSource("""
public enum RouteEnum : short
{
Zero,
@@ -70,13 +71,14 @@ public interface IEnumRouteContract : SharpLink.Sdk.IService
ValueTask Echo(RouteEnum value, CancellationToken cancellationToken);
}
+[SharpLink.Sdk.RpcCodecSemanticIdentity(0x1002UL, 0x2002UL)]
public sealed class EnumAdapter : TestRouteAdapterBase
{
public override string AdapterId => "route.enum/v1";
public override string WireFormatId => "route-enum-safe/v1";
}
-"""),
- "[assembly: SharpLink.Sdk.RpcCodecAdapterRegistration(typeof(EnumAdapter), \"route.enum/v1\", \"route-enum-safe/v1\")]",
+""")),
+ "[assembly: SharpLink.Sdk.RpcCodecAdapterRegistration(typeof(EnumAdapter), \"route.enum/v1\")]",
"[assembly: SharpLink.Sdk.RpcCodecRoute(SharpLink.Sdk.RpcCodecScope.All, typeof(EnumAdapter))]");
var diagnostics = RunGenerator(source);
From cd59ff7804fea1d3dc992116ee7e68e03311e481 Mon Sep 17 00:00:00 2001
From: SunSi12138 <54728594+SunSi12138@users.noreply.github.com>
Date: Sun, 30 Aug 2026 16:40:42 +0800
Subject: [PATCH 079/399] Migrate custom codec review fixtures to semantic
identity
---
.../RpcCodecReviewRegressionTests.cs | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/test/SharpLink.Generator.Tests/RpcCodecReviewRegressionTests.cs b/test/SharpLink.Generator.Tests/RpcCodecReviewRegressionTests.cs
index 8b34f41f9..aafed6e0b 100644
--- a/test/SharpLink.Generator.Tests/RpcCodecReviewRegressionTests.cs
+++ b/test/SharpLink.Generator.Tests/RpcCodecReviewRegressionTests.cs
@@ -9,13 +9,13 @@ public partial class RpcAnalyzerTests
[Test]
public Task ContractOnlyCustomCodecShouldBeOwnedWithoutChangingStandaloneCustomCodecPublication()
{
- var contractSource = AddAssemblyAttribute(BuildSource("""
+ var contractSource = AddAssemblyAttribute(UseCurrentIdentitySdk(BuildSource("""
public sealed class ContractOnlyPayload
{
public int Value { get; set; }
}
-[SharpLink.Sdk.RpcCodecImplementation("contract-only-wire/v1", "contract-only-schema/v1")]
+[SharpLink.Sdk.RpcCodecSemanticIdentity(0x3001UL, 0x4001UL)]
public sealed class ContractOnlyPayloadCodec : SharpLink.Abstractions.IRpcCodec
{
}
@@ -25,7 +25,7 @@ public interface IContractOnlyCodecService : SharpLink.Sdk.IService
{
ValueTask Echo(ContractOnlyPayload value, CancellationToken cancellationToken);
}
-"""),
+""")),
"[assembly: SharpLink.Sdk.RpcCodec(typeof(ContractOnlyPayload), typeof(ContractOnlyPayloadCodec))]");
var contractManifest = RunGeneratorAndGetSources(contractSource)
@@ -36,7 +36,7 @@ public interface IContractOnlyCodecService : SharpLink.Sdk.IService
Ensure(contractSections.Contract.Contains(".Factory(),", StringComparison.Ordinal),
"a Contract-only explicit custom Codec must be published in the assembly-owned Contract Codec table");
- var standaloneSource = BuildSource("""
+ var standaloneSource = UseCurrentIdentitySdk(BuildSource("""
[SharpLink.Sdk.RpcSerializable]
[SharpLink.Sdk.RpcCodec(typeof(StandalonePayloadCodec))]
public sealed class StandalonePayload
@@ -44,11 +44,11 @@ public sealed class StandalonePayload
public int Value { get; set; }
}
-[SharpLink.Sdk.RpcCodecImplementation("standalone-wire/v1", "standalone-schema/v1")]
+[SharpLink.Sdk.RpcCodecSemanticIdentity(0x3002UL, 0x4002UL)]
public sealed class StandalonePayloadCodec : SharpLink.Abstractions.IRpcCodec
{
}
-""");
+"""));
var standaloneManifest = RunGeneratorAndGetSources(standaloneSource)
.Single(static text => text.Contains("ISharpLinkGeneratedAssemblyManifest", StringComparison.Ordinal));
@@ -63,7 +63,7 @@ public sealed class StandalonePayloadCodec : SharpLink.Abstractions.IRpcCodec