This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new a12e77695 feat(c#): use sealed record for c# union (#3710)
a12e77695 is described below
commit a12e776951a263f3596a5cf6e4057ae33bc220b5
Author: Shawn Yang <[email protected]>
AuthorDate: Wed May 27 16:53:59 2026 +0800
feat(c#): use sealed record for c# union (#3710)
## Why?
## What does this PR do?
## Related issues
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence of the final clean AI review results
from both fresh reviewers on the current PR diff or current HEAD after
the latest code changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
compiler/fory_compiler/generators/csharp.py | 80 +--
.../fory_compiler/tests/test_csharp_generator.py | 28 +
.../Fory.Generator/AnalyzerReleases.Unshipped.md | 2 +
csharp/src/Fory.Generator/ForyModelGenerator.cs | 646 ++++++++++++++++++++-
csharp/src/Fory/AnySerializer.cs | 19 +-
csharp/src/Fory/Attributes.cs | 29 +-
csharp/src/Fory/TypeInfo.cs | 3 +-
csharp/tests/Fory.Tests/ForyGeneratorTests.cs | 19 +-
csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 52 ++
docs/compiler/generated-code.md | 22 +-
docs/guide/csharp/schema-metadata.md | 29 +-
docs/guide/csharp/supported-types.md | 2 +-
docs/specification/xlang_type_mapping.md | 2 +-
.../idl_tests/csharp/IdlTests/RoundtripTests.cs | 175 +++---
14 files changed, 909 insertions(+), 199 deletions(-)
diff --git a/compiler/fory_compiler/generators/csharp.py
b/compiler/fory_compiler/generators/csharp.py
index c9d1716fa..303feb1bd 100644
--- a/compiler/fory_compiler/generators/csharp.py
+++ b/compiler/fory_compiler/generators/csharp.py
@@ -545,6 +545,9 @@ class CSharpGenerator(BaseGenerator):
raise ValueError(f"Unknown field type: {field_type}")
+ def _union_case_type_name(self, field: Field) -> str:
+ return self.safe_identifier(self.to_pascal_case(field.name))
+
def _default_initializer(
self, field: Field, parent_stack: List[Message]
) -> Optional[str]:
@@ -697,7 +700,6 @@ class CSharpGenerator(BaseGenerator):
lines: List[str] = []
ind = self.indent_str * indent
type_name = self.safe_type_identifier(union.name)
- case_enum = self.safe_type_identifier(f"{union.name}Case")
registration_class = self.get_registration_class_name()
full_type_ref = self._type_reference_for_local(union)
@@ -705,88 +707,38 @@ class CSharpGenerator(BaseGenerator):
if comment:
lines.append(comment)
lines.append(f"{ind}[ForyUnion]")
- lines.append(f"{ind}public sealed class {type_name} : Union")
+ lines.append(f"{ind}public abstract partial record {type_name}")
lines.append(f"{ind}{{")
- lines.append(f"{ind}{self.indent_str}public enum {case_enum}")
- lines.append(f"{ind}{self.indent_str}{{")
- lines.append(f"{ind}{self.indent_str * 2}Unknown = 0,")
- for i, field in enumerate(union.fields):
- comma = "," if i < len(union.fields) - 1 else ""
- case_name = self.safe_identifier(self.to_pascal_case(field.name))
- lines.append(
- f"{ind}{self.indent_str * 2}{case_name} =
{field.number}{comma}"
- )
- lines.append(f"{ind}{self.indent_str}}}")
- lines.append("")
-
- lines.append(
- f"{ind}{self.indent_str}private {type_name}(int index, object?
value) : base(index, value)"
- )
+ lines.append(f"{ind}{self.indent_str}private {type_name}()")
lines.append(f"{ind}{self.indent_str}{{")
lines.append(f"{ind}{self.indent_str}}}")
lines.append("")
+ lines.append(f"{ind}{self.indent_str}[ForyCase(0)]")
lines.append(
- f"{ind}{self.indent_str}public static {type_name} Of(int index,
object? value)"
+ f"{ind}{self.indent_str}public sealed partial record
UnknownCase(int CaseId, object? Value) : {type_name};"
)
- lines.append(f"{ind}{self.indent_str}{{")
- lines.append(f"{ind}{self.indent_str * 2}return new {type_name}(index,
value);")
- lines.append(f"{ind}{self.indent_str}}}")
lines.append("")
for field in union.fields:
- case_name = self.safe_identifier(self.to_pascal_case(field.name))
+ case_name = self._union_case_type_name(field)
case_type = self.generate_type(
field.field_type,
nullable=False,
parent_stack=parent_stack,
)
+ schema_type = self._schema_type_hint(field.field_type)
+ if schema_type:
+ lines.append(
+ f"{ind}{self.indent_str}[ForyCase({field.number}, Type =
typeof({schema_type}))]"
+ )
+ else:
+
lines.append(f"{ind}{self.indent_str}[ForyCase({field.number})]")
lines.append(
- f"{ind}{self.indent_str}public static {type_name}
{case_name}({case_type} value)"
- )
- lines.append(f"{ind}{self.indent_str}{{")
- lines.append(
- f"{ind}{self.indent_str * 2}return new
{type_name}({field.number}, value);"
- )
- lines.append(f"{ind}{self.indent_str}}}")
- lines.append("")
-
- lines.append(
- f"{ind}{self.indent_str}public bool Is{case_name} => Index ==
{field.number};"
- )
- lines.append("")
- lines.append(f"{ind}{self.indent_str}public {case_type}
{case_name}Value()")
- lines.append(f"{ind}{self.indent_str}{{")
- lines.append(f"{ind}{self.indent_str * 2}if (!Is{case_name})")
- lines.append(f"{ind}{self.indent_str * 2}{{")
- lines.append(
- f'{ind}{self.indent_str * 3}throw new
InvalidOperationException("Union does not hold case {case_name}");'
+ f"{ind}{self.indent_str}public sealed partial record
{case_name}({case_type} Value) : {type_name};"
)
- lines.append(f"{ind}{self.indent_str * 2}}}")
- lines.append(f"{ind}{self.indent_str * 2}return
GetValue<{case_type}>();")
- lines.append(f"{ind}{self.indent_str}}}")
lines.append("")
- lines.append(f"{ind}{self.indent_str}public {case_enum} Case()")
- lines.append(f"{ind}{self.indent_str}{{")
- lines.append(f"{ind}{self.indent_str * 2}return Index switch")
- lines.append(f"{ind}{self.indent_str * 2}{{")
- for field in union.fields:
- case_name = self.safe_identifier(self.to_pascal_case(field.name))
- lines.append(
- f"{ind}{self.indent_str * 3}{field.number} =>
{case_enum}.{case_name},"
- )
- lines.append(f"{ind}{self.indent_str * 3}_ => {case_enum}.Unknown,")
- lines.append(f"{ind}{self.indent_str * 2}}};")
- lines.append(f"{ind}{self.indent_str}}}")
- lines.append("")
-
- lines.append(f"{ind}{self.indent_str}public int CaseId()")
- lines.append(f"{ind}{self.indent_str}{{")
- lines.append(f"{ind}{self.indent_str * 2}return Index;")
- lines.append(f"{ind}{self.indent_str}}}")
- lines.append("")
-
lines.append(f"{ind}{self.indent_str}public byte[] ToBytes()")
lines.append(f"{ind}{self.indent_str}{{")
lines.append(
diff --git a/compiler/fory_compiler/tests/test_csharp_generator.py
b/compiler/fory_compiler/tests/test_csharp_generator.py
index 0c1c78a9b..85ac252fe 100644
--- a/compiler/fory_compiler/tests/test_csharp_generator.py
+++ b/compiler/fory_compiler/tests/test_csharp_generator.py
@@ -78,8 +78,14 @@ def test_csharp_semantic_model_attributes():
READY = 1;
}
+ message Item {
+ string name = 1;
+ }
+
union Choice {
string text = 1;
+ fixed int32 code = 2;
+ Item item = 3;
}
message Envelope {
@@ -91,6 +97,28 @@ def test_csharp_semantic_model_attributes():
assert "[ForyEnum]" in file.content
assert "[ForyUnion]" in file.content
+ assert "public abstract partial record Choice" in file.content
+ assert "[ForyCase(0)]" in file.content
+ assert (
+ "public sealed partial record UnknownCase(int CaseId, object? Value) :
Choice;"
+ in file.content
+ )
+ assert "[ForyCase(1)]" in file.content
+ assert "public sealed partial record Text(string Value) : Choice;" in
file.content
+ assert "[ForyCase(2, Type = typeof(S.Fixed<S.Int32>))]" in file.content
+ assert "public sealed partial record Code(int Value) : Choice;" in
file.content
+ assert "[ForyCase(3)]" in file.content
+ assert (
+ "public sealed partial record Item(global::example.Item Value) :
Choice;"
+ in file.content
+ )
+ assert ": Union" not in file.content
+ assert "public enum ChoiceCase" not in file.content
+ assert "public static Choice Text" not in file.content
+ assert "public bool IsText" not in file.content
+ assert "TextValue()" not in file.content
+ assert "Case()" not in file.content
+ assert "GetCaseId()" not in file.content
assert "[ForyStruct]" in file.content
assert "[ForyObject]" not in file.content
diff --git a/csharp/src/Fory.Generator/AnalyzerReleases.Unshipped.md
b/csharp/src/Fory.Generator/AnalyzerReleases.Unshipped.md
index d67ff3684..b7b6eff1f 100644
--- a/csharp/src/Fory.Generator/AnalyzerReleases.Unshipped.md
+++ b/csharp/src/Fory.Generator/AnalyzerReleases.Unshipped.md
@@ -7,3 +7,5 @@ FORY002 | Fory | Error | Missing parameterless constructor
FORY003 | Fory | Error | Unsupported Field encoding
FORY004 | Fory | Error | Invalid Fory field id
FORY005 | Fory | Error | Invalid Fory union type
+FORY006 | Fory | Error | Invalid Fory union case
+FORY007 | Fory | Error | Duplicate Fory union case id
diff --git a/csharp/src/Fory.Generator/ForyModelGenerator.cs
b/csharp/src/Fory.Generator/ForyModelGenerator.cs
index 05f5faefb..6248065b7 100644
--- a/csharp/src/Fory.Generator/ForyModelGenerator.cs
+++ b/csharp/src/Fory.Generator/ForyModelGenerator.cs
@@ -66,7 +66,23 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
private static readonly DiagnosticDescriptor InvalidUnionType = new(
id: "FORY005",
title: "Invalid Fory union type",
- messageFormat: "Class '{0}' must derive from Apache.Fory.Union for
[ForyUnion]",
+ messageFormat: "Class '{0}' must declare nested [ForyCase] case types
for [ForyUnion]",
+ category: "Fory",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor InvalidUnionCase = new(
+ id: "FORY006",
+ title: "Invalid Fory union case",
+ messageFormat: "Union case '{0}' is invalid: {1}",
+ category: "Fory",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor DuplicateUnionCaseId = new(
+ id: "FORY007",
+ title: "Duplicate Fory union case id",
+ messageFormat: "Union case id {0} is declared more than once in '{1}'",
category: "Fory",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
@@ -141,6 +157,11 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
EmitObjectSerializer(sb, model);
sb.AppendLine();
}
+ else if (model.Kind == DeclKind.Union)
+ {
+ EmitUnionSerializer(sb, model);
+ sb.AppendLine();
+ }
}
sb.AppendLine("internal static class
__ForyGeneratedModuleInitializer");
@@ -159,7 +180,7 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
else if (model.Kind == DeclKind.Union)
{
sb.AppendLine(
- $"
global::Apache.Fory.TypeResolver.RegisterGenerated<{model.TypeName},
global::Apache.Fory.UnionSerializer<{model.TypeName}>>();");
+ $"
global::Apache.Fory.TypeResolver.RegisterGenerated<{model.TypeName},
{model.SerializerName}>();");
}
else
{
@@ -676,6 +697,284 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
sb.AppendLine("}");
}
+ private static void EmitUnionSerializer(StringBuilder sb, TypeModel model)
+ {
+ sb.AppendLine(
+ $"file sealed class {model.SerializerName} :
global::Apache.Fory.Serializer<{model.TypeName}>");
+ sb.AppendLine("{");
+ sb.AppendLine($" public override {model.TypeName} DefaultValue =>
null!;");
+ sb.AppendLine();
+ sb.AppendLine(" private static global::Apache.Fory.RefMode
__ForyRefMode(bool nullable, bool trackRef)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (trackRef)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" return
global::Apache.Fory.RefMode.Tracking;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" return nullable ?
global::Apache.Fory.RefMode.NullOnly : global::Apache.Fory.RefMode.None;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ foreach (UnionCaseModel unionCase in model.UnionCases.OrderBy(c =>
c.CaseId))
+ {
+ if (unionCase.ValueMember is { HasSchemaType: true } member)
+ {
+ EmitUnionCaseSerializer(sb, unionCase.CaseId, member);
+ }
+ }
+
+ sb.AppendLine(
+ $" public override void
WriteData(global::Apache.Fory.WriteContext context, in {model.TypeName} value,
bool hasGenerics)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" _ = hasGenerics;");
+ sb.AppendLine(" if (value is null)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" throw new
global::Apache.Fory.InvalidDataException(\"union value is null\");");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" switch (value)");
+ sb.AppendLine(" {");
+ UnionCaseModel? unknownCase = null;
+ foreach (UnionCaseModel unionCase in model.UnionCases.OrderBy(c =>
c.CaseId))
+ {
+ if (unionCase.IsUnknown)
+ {
+ unknownCase = unionCase;
+ sb.AppendLine($" case {unionCase.TypeName}
__foryCase:");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (__foryCase.CaseId < 0)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" throw new
global::Apache.Fory.InvalidDataException($\"union case id out of range:
{__foryCase.CaseId}\");");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine("
context.Writer.WriteVarUInt32((uint)__foryCase.CaseId);");
+ sb.AppendLine("
global::Apache.Fory.DynamicAnyCodec.WriteAny(context, __foryCase.Value,
global::Apache.Fory.RefMode.Tracking, true, false);");
+ sb.AppendLine(" return;");
+ sb.AppendLine(" }");
+ continue;
+ }
+
+ sb.AppendLine($" case {unionCase.TypeName}
__foryCase:");
+ sb.AppendLine(" {");
+ sb.AppendLine($"
context.Writer.WriteVarUInt32({unionCase.CaseId}u);");
+ EmitWriteUnionCasePayload(sb, unionCase, "__foryCase.Value", 4);
+ sb.AppendLine(" return;");
+ sb.AppendLine(" }");
+ }
+
+ sb.AppendLine(" default:");
+ sb.AppendLine(" throw new
global::Apache.Fory.InvalidDataException($\"unsupported union case
{value.GetType()}\");");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine($" public override {model.TypeName}
ReadData(global::Apache.Fory.ReadContext context)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" uint rawCaseId =
context.Reader.ReadVarUInt32();");
+ sb.AppendLine(" if (rawCaseId > int.MaxValue)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" throw new
global::Apache.Fory.InvalidDataException($\"union case id out of range:
{rawCaseId}\");");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" int caseId = (int)rawCaseId;");
+ sb.AppendLine(" switch (caseId)");
+ sb.AppendLine(" {");
+ foreach (UnionCaseModel unionCase in model.UnionCases.OrderBy(c =>
c.CaseId))
+ {
+ if (unionCase.IsUnknown)
+ {
+ sb.AppendLine(" case 0:");
+ sb.AppendLine(" {");
+ sb.AppendLine(" object? __foryUnknownValue =
global::Apache.Fory.DynamicAnyCodec.ReadAny(context,
global::Apache.Fory.RefMode.Tracking, true);");
+ sb.AppendLine($" return new
{unionCase.TypeName}(0, __foryUnknownValue);");
+ sb.AppendLine(" }");
+ continue;
+ }
+
+ string valueVar = $"__foryCaseValue{unionCase.CaseId}";
+ sb.AppendLine($" case {unionCase.CaseId}:");
+ sb.AppendLine(" {");
+ EmitReadUnionCasePayload(sb, unionCase, valueVar, 4);
+ sb.AppendLine($" return new
{unionCase.TypeName}({valueVar});");
+ sb.AppendLine(" }");
+ }
+
+ sb.AppendLine(" default:");
+ sb.AppendLine(" {");
+ sb.AppendLine(" object? __foryUnknownValue =
global::Apache.Fory.DynamicAnyCodec.ReadAny(context,
global::Apache.Fory.RefMode.Tracking, true);");
+ if (unknownCase is null)
+ {
+ sb.AppendLine(" throw new
global::Apache.Fory.InvalidDataException($\"unknown union case {caseId}\");");
+ }
+ else
+ {
+ sb.AppendLine($" return new
{unknownCase.TypeName}(caseId, __foryUnknownValue);");
+ }
+
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+ sb.AppendLine("}");
+ }
+
+ private static void EmitUnionCaseSerializer(
+ StringBuilder sb,
+ int caseId,
+ MemberModel member)
+ {
+ sb.AppendLine($" private sealed class __ForyCaseSerializer{caseId}
: global::Apache.Fory.Serializer<{member.TypeName}>");
+ sb.AppendLine(" {");
+ sb.AppendLine($" internal static readonly
__ForyCaseSerializer{caseId} Instance = new();");
+ sb.AppendLine();
+ sb.AppendLine($" public override {member.TypeName} DefaultValue
=> default!;");
+ sb.AppendLine();
+ sb.AppendLine($" public override void
WriteData(global::Apache.Fory.WriteContext context, in {member.TypeName} value,
bool hasGenerics)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" _ = hasGenerics;");
+ EmitWriteUnionTopType(sb, member.TypeMeta, 3);
+ string payloadExpr = member.IsNullableValueType ?
"value.GetValueOrDefault()" : "value";
+ EmitWriteUnionPayload(sb, NonNullableMember(member), payloadExpr, 3);
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine($" public override {member.TypeName}
ReadData(global::Apache.Fory.ReadContext context)");
+ sb.AppendLine(" {");
+ EmitValidateUnionTopType(sb, member.TypeMeta, 3);
+ EmitReadUnionPayload(sb, NonNullableMember(member), "__foryPayload",
3);
+ sb.AppendLine(" return __foryPayload;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ }
+
+ private static void EmitWriteUnionCasePayload(
+ StringBuilder sb,
+ UnionCaseModel unionCase,
+ string valueExpr,
+ int indentLevel)
+ {
+ MemberModel member = unionCase.ValueMember!;
+ string indent = new(' ', indentLevel * 4);
+ string refModeExpr = BuildUnionCaseRefModeExpression(member);
+ string hasGenerics = member.IsCollection ? "true" : "false";
+
+ if (member.DynamicAnyKind == DynamicAnyKind.AnyValue)
+ {
+ sb.AppendLine(
+
$"{indent}global::Apache.Fory.DynamicAnyCodec.WriteAny(context, {valueExpr},
{refModeExpr}, true, false);");
+ return;
+ }
+
+ if (!member.HasSchemaType)
+ {
+ sb.AppendLine(
+
$"{indent}context.TypeResolver.GetSerializer<{member.TypeName}>().Write(context,
{valueExpr}, {refModeExpr}, true, {hasGenerics});");
+ return;
+ }
+
+ sb.AppendLine(
+
$"{indent}__ForyCaseSerializer{unionCase.CaseId}.Instance.Write(context,
{valueExpr}, {refModeExpr}, false, false);");
+ }
+
+ private static void EmitReadUnionCasePayload(
+ StringBuilder sb,
+ UnionCaseModel unionCase,
+ string valueVar,
+ int indentLevel)
+ {
+ MemberModel member = unionCase.ValueMember!;
+ string indent = new(' ', indentLevel * 4);
+ string refModeExpr = BuildUnionCaseRefModeExpression(member);
+
+ if (member.DynamicAnyKind == DynamicAnyKind.AnyValue)
+ {
+ string typeOfTypeName = StripNullableForTypeOf(member.TypeName);
+ sb.AppendLine(
+ $"{indent}{member.TypeName} {valueVar} =
({member.TypeName})global::Apache.Fory.DynamicAnyCodec.CastAnyDynamicValue(global::Apache.Fory.DynamicAnyCodec.ReadAny(context,
{refModeExpr}, true), typeof({typeOfTypeName}))!;");
+ return;
+ }
+
+ if (!member.HasSchemaType)
+ {
+ sb.AppendLine(
+ $"{indent}{member.TypeName} {valueVar} =
context.TypeResolver.GetSerializer<{member.TypeName}>().Read(context,
{refModeExpr}, true);");
+ return;
+ }
+
+ sb.AppendLine(
+ $"{indent}{member.TypeName} {valueVar} =
__ForyCaseSerializer{unionCase.CaseId}.Instance.Read(context, {refModeExpr},
false);");
+ }
+
+ private static void EmitWriteUnionPayload(
+ StringBuilder sb,
+ MemberModel member,
+ string valueExpr,
+ int indentLevel)
+ {
+ int id = 0;
+ if (member.FieldCodec is not null)
+ {
+ EmitWritePayload(sb, member.FieldCodec, valueExpr, indentLevel,
ref id);
+ return;
+ }
+
+ if (TryBuildDirectPayloadWrite(member.Classification.TypeId,
valueExpr, out string? writeCode))
+ {
+ string indent = new(' ', indentLevel * 4);
+ sb.AppendLine($"{indent}{writeCode}");
+ return;
+ }
+
+ string hasGenerics = member.IsCollection ? "true" : "false";
+ string fallbackIndent = new(' ', indentLevel * 4);
+ sb.AppendLine(
+
$"{fallbackIndent}context.TypeResolver.GetSerializer<{member.TypeName}>().WriteData(context,
{valueExpr}, {hasGenerics});");
+ }
+
+ private static void EmitReadUnionPayload(
+ StringBuilder sb,
+ MemberModel member,
+ string valueVar,
+ int indentLevel)
+ {
+ int id = 0;
+ if (member.FieldCodec is not null)
+ {
+ EmitReadPayload(sb, member.FieldCodec, valueVar, indentLevel, ref
id);
+ return;
+ }
+
+ if (TryBuildDirectPayloadRead(member.Classification.TypeId, out
string? readExpr))
+ {
+ string indent = new(' ', indentLevel * 4);
+ sb.AppendLine($"{indent}{member.TypeName} {valueVar} =
{readExpr};");
+ return;
+ }
+
+ string fallbackIndent = new(' ', indentLevel * 4);
+ sb.AppendLine(
+ $"{fallbackIndent}{member.TypeName} {valueVar} =
context.TypeResolver.GetSerializer<{member.TypeName}>().ReadData(context);");
+ }
+
+ private static void EmitWriteUnionTopType(
+ StringBuilder sb,
+ TypeMetaFieldTypeModel model,
+ int indentLevel)
+ {
+ string indent = new(' ', indentLevel * 4);
+
sb.AppendLine($"{indent}context.Writer.WriteUInt8((byte)({model.TypeIdExpr}));");
+ }
+
+ private static void EmitValidateUnionTopType(
+ StringBuilder sb,
+ TypeMetaFieldTypeModel model,
+ int indentLevel)
+ {
+ string indent = new(' ', indentLevel * 4);
+ sb.AppendLine($"{indent}uint __foryTypeId =
context.Reader.ReadUInt8();");
+ sb.AppendLine($"{indent}if (__foryTypeId != ({model.TypeIdExpr}))");
+ sb.AppendLine($"{indent}{{");
+ sb.AppendLine($"{indent} throw new
global::Apache.Fory.TypeMismatchException({model.TypeIdExpr}, __foryTypeId);");
+ sb.AppendLine($"{indent}}}");
+ }
+
private static void EmitFieldCodecMethods(StringBuilder sb, MemberModel
member)
{
FieldCodecModel codec = member.FieldCodec!;
@@ -1471,6 +1770,40 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
codec.Generics);
}
+ private static MemberModel NonNullableMember(MemberModel member)
+ {
+ if (!member.IsNullable)
+ {
+ return member;
+ }
+
+ return new MemberModel(
+ member.Name,
+ member.FieldIdentifier,
+ member.OriginalIndex,
+ member.DeclKind,
+ member.IsNullableValueType && member.TypeName.EndsWith("?",
StringComparison.Ordinal)
+ ? member.TypeName.Substring(0, member.TypeName.Length - 1)
+ : StripNullableForTypeOf(member.TypeName),
+ false,
+ false,
+ member.FieldId,
+ member.Classification,
+ member.Group,
+ member.IsCollection,
+ member.UseDictionaryTypeInfoCache,
+ member.IsRefType,
+ member.NeedsFieldTypeInfo,
+ member.DynamicAnyKind,
+ new TypeMetaFieldTypeModel(
+ member.TypeMeta.TypeIdExpr,
+ false,
+ member.TypeMeta.TrackRefByContext,
+ member.TypeMeta.Generics),
+ member.FieldCodec is null ? null :
NonNullableCodec(member.FieldCodec),
+ member.HasSchemaType);
+ }
+
private static string ElementTypeName(string arrayTypeName)
{
return arrayTypeName.EndsWith("[]", StringComparison.Ordinal)
@@ -2129,6 +2462,13 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
};
}
+ private static string BuildUnionCaseRefModeExpression(MemberModel member)
+ {
+ return member.IsRefType
+ ? "__ForyRefMode(true, context.TrackRef)"
+ : "global::Apache.Fory.RefMode.NullOnly";
+ }
+
private static string BuildFieldTypeInfoLiteral(MemberModel member)
{
return BoolLiteral(member.NeedsFieldTypeInfo);
@@ -2187,7 +2527,9 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
return null;
}
- if (!IsUnionType(typeSymbol))
+ List<Diagnostic> unionDiagnostics = [];
+ ImmutableArray<UnionCaseModel> unionCases =
BuildUnionCases(typeSymbol, unionDiagnostics);
+ if (unionCases.IsEmpty)
{
return new TypeModel(
typeName,
@@ -2207,7 +2549,8 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
DeclKind.Union,
ImmutableArray<MemberModel>.Empty,
ImmutableArray<MemberModel>.Empty,
- ImmutableArray<Diagnostic>.Empty);
+ unionDiagnostics.ToImmutableArray(),
+ unionCases);
}
DeclKind kind = typeSymbol.TypeKind switch
@@ -2300,6 +2643,192 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
return new TypeModel(typeName, serializerName, kind, ordered, sorted,
diagnostics.ToImmutableArray());
}
+ private static ImmutableArray<UnionCaseModel> BuildUnionCases(
+ INamedTypeSymbol unionType,
+ List<Diagnostic> diagnostics)
+ {
+ List<UnionCaseModel> cases = [];
+ HashSet<int> caseIds = [];
+ foreach (INamedTypeSymbol caseType in unionType.GetTypeMembers())
+ {
+ if (!TryGetForyCase(caseType, diagnostics, out int caseId, out
SchemaTypeModel? schemaType))
+ {
+ continue;
+ }
+
+ if (!caseIds.Add(caseId))
+ {
+ diagnostics.Add(Diagnostic.Create(
+ DuplicateUnionCaseId,
+ caseType.Locations.FirstOrDefault(),
+ caseId,
+ unionType.ToDisplayString(FullNameFormat)));
+ continue;
+ }
+
+ if (!SymbolEqualityComparer.Default.Equals(caseType.BaseType,
unionType))
+ {
+ diagnostics.Add(Diagnostic.Create(
+ InvalidUnionCase,
+ caseType.Locations.FirstOrDefault(),
+ caseType.ToDisplayString(FullNameFormat),
+ "case type must directly derive from the annotated union
root"));
+ continue;
+ }
+
+ string caseTypeName = caseType.ToDisplayString(FullNameFormat);
+ if (caseId == 0)
+ {
+ if (!HasProperty(caseType, "CaseId", SpecialType.System_Int32)
||
+ !HasObjectValueProperty(caseType))
+ {
+ diagnostics.Add(Diagnostic.Create(
+ InvalidUnionCase,
+ caseType.Locations.FirstOrDefault(),
+ caseTypeName,
+ "case id 0 must expose CaseId:int and Value:object?"));
+ continue;
+ }
+
+ cases.Add(new UnionCaseModel(caseId, caseTypeName, isUnknown:
true, valueMember: null));
+ continue;
+ }
+
+ IPropertySymbol? valueProperty = FindProperty(caseType, "Value");
+ if (valueProperty is null)
+ {
+ diagnostics.Add(Diagnostic.Create(
+ InvalidUnionCase,
+ caseType.Locations.FirstOrDefault(),
+ caseTypeName,
+ "known cases must expose a Value property"));
+ continue;
+ }
+
+ MemberModel? valueMember = BuildMemberModel(
+ valueProperty.Name,
+ valueProperty.Type,
+ valueProperty,
+ MemberDeclKind.Property,
+ diagnostics,
+ schemaType,
+ parseFieldAttribute: false);
+ if (valueMember is null)
+ {
+ diagnostics.Add(Diagnostic.Create(
+ InvalidUnionCase,
+ valueProperty.Locations.FirstOrDefault(),
+ caseTypeName,
+ "case Value type is not supported"));
+ continue;
+ }
+
+ cases.Add(new UnionCaseModel(caseId, caseTypeName, isUnknown:
false, valueMember));
+ }
+
+ if (!cases.Any(c => c.IsUnknown))
+ {
+ diagnostics.Add(Diagnostic.Create(
+ InvalidUnionCase,
+ unionType.Locations.FirstOrDefault(),
+ unionType.ToDisplayString(FullNameFormat),
+ "union must declare [ForyCase(0)] UnknownCase"));
+ }
+
+ return cases.OrderBy(c => c.CaseId).ToImmutableArray();
+ }
+
+ private static bool TryGetForyCase(
+ INamedTypeSymbol caseType,
+ List<Diagnostic> diagnostics,
+ out int caseId,
+ out SchemaTypeModel? schemaType)
+ {
+ caseId = default;
+ schemaType = null;
+ foreach (AttributeData attribute in caseType.GetAttributes())
+ {
+ string? attrName = attribute.AttributeClass?.ToDisplayString();
+ if (!string.Equals(attrName, "Apache.Fory.ForyCaseAttribute",
StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ if (attribute.ConstructorArguments.Length != 1 ||
+ !TryGetUnionCaseId(attribute.ConstructorArguments[0], out
caseId))
+ {
+ diagnostics.Add(Diagnostic.Create(
+ InvalidUnionCase,
+ caseType.Locations.FirstOrDefault(),
+ caseType.ToDisplayString(FullNameFormat),
+ "case id must be a non-negative int"));
+ return true;
+ }
+
+ foreach (KeyValuePair<string, TypedConstant> namedArg in
attribute.NamedArguments)
+ {
+ if (!string.Equals(namedArg.Key, "Type",
StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ if (namedArg.Value.Value is not ITypeSymbol schemaSymbol ||
+ TryParseSchemaType(schemaSymbol) is not SchemaTypeModel
parsedSchema)
+ {
+ diagnostics.Add(Diagnostic.Create(
+ InvalidUnionCase,
+ caseType.Locations.FirstOrDefault(),
+ caseType.ToDisplayString(FullNameFormat),
+ "ForyCase.Type must be an Apache.Fory.Schema.Types
descriptor"));
+ continue;
+ }
+
+ schemaType = parsedSchema;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool TryGetUnionCaseId(TypedConstant value, out int caseId)
+ {
+ caseId = default;
+ if (value.Value is int id && id >= 0)
+ {
+ caseId = id;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static IPropertySymbol? FindProperty(INamedTypeSymbol type, string
name)
+ {
+ foreach (ISymbol member in type.GetMembers(name))
+ {
+ if (member is IPropertySymbol property && !property.IsStatic)
+ {
+ return property;
+ }
+ }
+
+ return null;
+ }
+
+ private static bool HasProperty(INamedTypeSymbol type, string name,
SpecialType specialType)
+ {
+ IPropertySymbol? property = FindProperty(type, name);
+ return property is not null && property.Type.SpecialType ==
specialType;
+ }
+
+ private static bool HasObjectValueProperty(INamedTypeSymbol type)
+ {
+ IPropertySymbol? property = FindProperty(type, "Value");
+ return property is not null && property.Type.SpecialType ==
SpecialType.System_Object;
+ }
+
private static ForyAttributeKind GetForyAttributeKind(INamedTypeSymbol
typeSymbol)
{
foreach (AttributeData attribute in typeSymbol.GetAttributes())
@@ -2330,44 +2859,66 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
ISymbol memberSymbol,
MemberDeclKind memberDeclKind,
List<Diagnostic> diagnostics)
+ {
+ return BuildMemberModel(
+ name,
+ memberType,
+ memberSymbol,
+ memberDeclKind,
+ diagnostics,
+ schemaTypeOverride: null,
+ parseFieldAttribute: true);
+ }
+
+ private static MemberModel? BuildMemberModel(
+ string name,
+ ITypeSymbol memberType,
+ ISymbol memberSymbol,
+ MemberDeclKind memberDeclKind,
+ List<Diagnostic> diagnostics,
+ SchemaTypeModel? schemaTypeOverride,
+ bool parseFieldAttribute)
{
(bool isOptional, ITypeSymbol unwrappedType) =
UnwrapNullable(memberType);
short? fieldId = null;
- SchemaTypeModel? schemaType = null;
- foreach (AttributeData attribute in memberSymbol.GetAttributes())
+ SchemaTypeModel? schemaType = schemaTypeOverride;
+ if (parseFieldAttribute)
{
- string? attrName = attribute.AttributeClass?.ToDisplayString();
- if (!string.Equals(attrName, "Apache.Fory.ForyFieldAttribute",
StringComparison.Ordinal))
+ foreach (AttributeData attribute in memberSymbol.GetAttributes())
{
- continue;
- }
-
- if (attribute.ConstructorArguments.Length == 1 &&
- TryGetFieldId(attribute.ConstructorArguments[0], memberSymbol,
diagnostics, out short ctorFieldId))
- {
- fieldId = ctorFieldId;
- }
-
- foreach (KeyValuePair<string, TypedConstant> namedArg in
attribute.NamedArguments)
- {
- if (string.Equals(namedArg.Key, "Id",
StringComparison.Ordinal))
+ string? attrName = attribute.AttributeClass?.ToDisplayString();
+ if (!string.Equals(attrName, "Apache.Fory.ForyFieldAttribute",
StringComparison.Ordinal))
{
- if (TryGetFieldId(namedArg.Value, memberSymbol,
diagnostics, out short parsedFieldId))
- {
- fieldId = parsedFieldId;
- }
-
continue;
}
- if (!string.Equals(namedArg.Key, "Type",
StringComparison.Ordinal))
+ if (attribute.ConstructorArguments.Length == 1 &&
+ TryGetFieldId(attribute.ConstructorArguments[0],
memberSymbol, diagnostics, out short ctorFieldId))
{
- continue;
+ fieldId = ctorFieldId;
}
- if (namedArg.Value.Value is ITypeSymbol schemaSymbol)
+ foreach (KeyValuePair<string, TypedConstant> namedArg in
attribute.NamedArguments)
{
- schemaType = TryParseSchemaType(schemaSymbol);
+ if (string.Equals(namedArg.Key, "Id",
StringComparison.Ordinal))
+ {
+ if (TryGetFieldId(namedArg.Value, memberSymbol,
diagnostics, out short parsedFieldId))
+ {
+ fieldId = parsedFieldId;
+ }
+
+ continue;
+ }
+
+ if (!string.Equals(namedArg.Key, "Type",
StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ if (namedArg.Value.Value is ITypeSymbol schemaSymbol)
+ {
+ schemaType = TryParseSchemaType(schemaSymbol);
+ }
}
}
}
@@ -2418,7 +2969,8 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
FieldNeedsTypeInfo(classification, dynamicAnyKind, unwrappedType),
dynamicAnyKind == DynamicAnyKind.None ? DynamicAnyKind.None :
dynamicAnyKind,
typeMeta,
- fieldCodec);
+ fieldCodec,
+ schemaType is not null);
}
private static TypeMetaFieldTypeModel BuildTypeMetaFieldTypeModel(
@@ -3323,6 +3875,12 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
private static bool IsUnionType(ITypeSymbol symbol)
{
+ if (symbol is INamedTypeSymbol namedType &&
+ GetForyAttributeKind(namedType) == ForyAttributeKind.Union)
+ {
+ return true;
+ }
+
INamedTypeSymbol? current = symbol as INamedTypeSymbol;
while (current is not null)
{
@@ -3640,7 +4198,8 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
DeclKind kind,
ImmutableArray<MemberModel> members,
ImmutableArray<MemberModel> sortedMembers,
- ImmutableArray<Diagnostic> diagnostics)
+ ImmutableArray<Diagnostic> diagnostics,
+ ImmutableArray<UnionCaseModel> unionCases = default)
{
TypeName = typeName;
SerializerName = serializerName;
@@ -3648,6 +4207,9 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
Members = members;
SortedMembers = sortedMembers;
Diagnostics = diagnostics;
+ UnionCases = unionCases.IsDefault
+ ? ImmutableArray<UnionCaseModel>.Empty
+ : unionCases;
}
public string TypeName { get; }
@@ -3656,6 +4218,7 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
public ImmutableArray<MemberModel> Members { get; }
public ImmutableArray<MemberModel> SortedMembers { get; }
public ImmutableArray<Diagnostic> Diagnostics { get; }
+ public ImmutableArray<UnionCaseModel> UnionCases { get; }
}
private sealed class MemberModel
@@ -3677,7 +4240,8 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
bool needsFieldTypeInfo,
DynamicAnyKind dynamicAnyKind,
TypeMetaFieldTypeModel typeMeta,
- FieldCodecModel? fieldCodec)
+ FieldCodecModel? fieldCodec,
+ bool hasSchemaType = false)
{
Name = name;
FieldIdentifier = fieldIdentifier;
@@ -3696,6 +4260,7 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
DynamicAnyKind = dynamicAnyKind;
TypeMeta = typeMeta;
FieldCodec = fieldCodec;
+ HasSchemaType = hasSchemaType;
}
public string Name { get; }
@@ -3715,6 +4280,23 @@ public sealed class ForyModelGenerator :
IIncrementalGenerator
public DynamicAnyKind DynamicAnyKind { get; }
public TypeMetaFieldTypeModel TypeMeta { get; }
public FieldCodecModel? FieldCodec { get; }
+ public bool HasSchemaType { get; }
+ }
+
+ private sealed class UnionCaseModel
+ {
+ public UnionCaseModel(int caseId, string typeName, bool isUnknown,
MemberModel? valueMember)
+ {
+ CaseId = caseId;
+ TypeName = typeName;
+ IsUnknown = isUnknown;
+ ValueMember = valueMember;
+ }
+
+ public int CaseId { get; }
+ public string TypeName { get; }
+ public bool IsUnknown { get; }
+ public MemberModel? ValueMember { get; }
}
private enum MemberDeclKind
diff --git a/csharp/src/Fory/AnySerializer.cs b/csharp/src/Fory/AnySerializer.cs
index f6ff5ae69..63c6b9d17 100644
--- a/csharp/src/Fory/AnySerializer.cs
+++ b/csharp/src/Fory/AnySerializer.cs
@@ -118,7 +118,7 @@ public sealed class DynamicAnyObjectSerializer :
Serializer<object?>
private static bool AnyValueIsRefType(object value, TypeResolver
typeResolver)
{
- TypeInfo typeInfo = typeResolver.GetTypeInfo(value.GetType());
+ TypeInfo typeInfo =
typeResolver.GetTypeInfo(DynamicAnyCodec.ResolveRuntimeType(value));
return typeInfo.IsRefType;
}
@@ -169,10 +169,23 @@ public static class DynamicAnyCodec
return;
}
- TypeInfo typeInfo = context.TypeResolver.GetTypeInfo(value.GetType());
+ TypeInfo typeInfo =
context.TypeResolver.GetTypeInfo(ResolveRuntimeType(value));
context.TypeResolver.WriteTypeInfo(typeInfo, context);
}
+ internal static Type ResolveRuntimeType(object value)
+ {
+ Type runtimeType = value.GetType();
+ Type? baseType = runtimeType.BaseType;
+ if (baseType is not null &&
+ Attribute.IsDefined(baseType, typeof(ForyUnionAttribute), inherit:
false))
+ {
+ return baseType;
+ }
+
+ return runtimeType;
+ }
+
public static object? CastAnyDynamicValue(object? value, Type targetType)
{
if (value is null)
@@ -220,7 +233,7 @@ public static class DynamicAnyCodec
return;
}
- TypeInfo typeInfo = context.TypeResolver.GetTypeInfo(value.GetType());
+ TypeInfo typeInfo =
context.TypeResolver.GetTypeInfo(ResolveRuntimeType(value));
context.TypeResolver.WriteDataObject(typeInfo, context, value,
hasGenerics);
}
diff --git a/csharp/src/Fory/Attributes.cs b/csharp/src/Fory/Attributes.cs
index ec3553c28..37f3e5275 100644
--- a/csharp/src/Fory/Attributes.cs
+++ b/csharp/src/Fory/Attributes.cs
@@ -38,13 +38,40 @@ public sealed class ForyEnumAttribute : Attribute
}
/// <summary>
-/// Marks a <see cref="Union"/> subclass as a generated Fory union type.
+/// Marks a generated Fory union type.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class ForyUnionAttribute : Attribute
{
}
+/// <summary>
+/// Marks a nested case type within a generated Fory union.
+/// </summary>
+[AttributeUsage(AttributeTargets.Class)]
+public sealed class ForyCaseAttribute : Attribute
+{
+ public ForyCaseAttribute(int id)
+ {
+ if (id < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(id));
+ }
+
+ Id = id;
+ }
+
+ /// <summary>
+ /// Stable union case id written on the wire.
+ /// </summary>
+ public int Id { get; }
+
+ /// <summary>
+ /// Optional Fory schema descriptor type from
<c>Apache.Fory.Schema.Types</c>.
+ /// </summary>
+ public Type? Type { get; set; }
+}
+
/// <summary>
/// Overrides generated serializer behavior for a field or property.
/// </summary>
diff --git a/csharp/src/Fory/TypeInfo.cs b/csharp/src/Fory/TypeInfo.cs
index 65a040a21..99972bdb6 100644
--- a/csharp/src/Fory/TypeInfo.cs
+++ b/csharp/src/Fory/TypeInfo.cs
@@ -230,7 +230,8 @@ public sealed class TypeInfo
return (null, Apache.Fory.UserTypeKind.Enum, false);
}
- if (typeof(Union).IsAssignableFrom(type))
+ if (type.GetCustomAttribute<ForyUnionAttribute>() is not null ||
+ typeof(Union).IsAssignableFrom(type))
{
return (null, Apache.Fory.UserTypeKind.TypedUnion, false);
}
diff --git a/csharp/tests/Fory.Tests/ForyGeneratorTests.cs
b/csharp/tests/Fory.Tests/ForyGeneratorTests.cs
index efca15158..840af25cc 100644
--- a/csharp/tests/Fory.Tests/ForyGeneratorTests.cs
+++ b/csharp/tests/Fory.Tests/ForyGeneratorTests.cs
@@ -40,29 +40,24 @@ public sealed class ForyGeneratorTests
}
[ForyUnion]
- public sealed class Choice : Union
+ public abstract partial record Choice
{
- private Choice(int index, object? value)
- : base(index, value)
+ private Choice()
{
}
- public static Choice Of(int index, object? value)
- {
- return new Choice(index, value);
- }
+ [ForyCase(0)]
+ public sealed partial record UnknownCase(int CaseId, object?
Value) : Choice;
- public static Choice Text(string value)
- {
- return new Choice(1, value);
- }
+ [ForyCase(1)]
+ public sealed partial record Text(string Value) : Choice;
}
[ForyStruct]
public sealed class Envelope
{
public Status Status { get; set; }
- public Choice Choice { get; set; } = Choice.Text(string.Empty);
+ public Choice Choice { get; set; } = new
Choice.Text(string.Empty);
}
""";
diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
index 0fcee13b3..5006506b4 100644
--- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
+++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
@@ -287,6 +287,29 @@ public sealed class StructWithUnion2
public Union2<string, long> Union { get; set; } = Union2<string,
long>.OfT1(string.Empty);
}
+[ForyUnion]
+public abstract partial record SourceGeneratedShape
+{
+ private SourceGeneratedShape()
+ {
+ }
+
+ [ForyCase(0)]
+ public sealed partial record UnknownCase(int CaseId, object? Value) :
SourceGeneratedShape;
+
+ [ForyCase(1)]
+ public sealed partial record Text(string Value) : SourceGeneratedShape;
+
+ [ForyCase(2, Type = typeof(S.Fixed<S.Int32>))]
+ public sealed partial record Number(int Value) : SourceGeneratedShape;
+}
+
+[ForyStruct]
+public sealed class SourceGeneratedUnionHolder
+{
+ public SourceGeneratedShape Shape { get; set; } = new
SourceGeneratedShape.UnknownCase(0, null);
+}
+
[ForyStruct]
public sealed class DynamicAnyHolder
{
@@ -1427,6 +1450,35 @@ public sealed class ForyRuntimeTests
Assert.Throws<InvalidDataException>(() => { _ =
reader.Deserialize<TwoStringField>(payload); });
}
+ [Fact]
+ public void SourceGeneratedUnionRoundTrip()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().Compatible(false).Build();
+ fory.Register<SourceGeneratedShape>(302);
+ fory.Register<SourceGeneratedUnionHolder>(303);
+
+ SourceGeneratedUnionHolder known = new()
+ {
+ Shape = new SourceGeneratedShape.Number(42),
+ };
+ SourceGeneratedUnionHolder unknown = new()
+ {
+ Shape = new SourceGeneratedShape.UnknownCase(99, "future"),
+ };
+
+ SourceGeneratedUnionHolder knownDecoded =
+
fory.Deserialize<SourceGeneratedUnionHolder>(fory.Serialize(known));
+ SourceGeneratedUnionHolder unknownDecoded =
+
fory.Deserialize<SourceGeneratedUnionHolder>(fory.Serialize(unknown));
+
+ SourceGeneratedShape.Number number =
Assert.IsType<SourceGeneratedShape.Number>(knownDecoded.Shape);
+ Assert.Equal(42, number.Value);
+ SourceGeneratedShape.UnknownCase unknownCase =
+
Assert.IsType<SourceGeneratedShape.UnknownCase>(unknownDecoded.Shape);
+ Assert.Equal(99, unknownCase.CaseId);
+ Assert.Equal("future", unknownCase.Value);
+ }
+
[Fact]
public void UnionFieldRoundTripCompatible()
{
diff --git a/docs/compiler/generated-code.md b/docs/compiler/generated-code.md
index 3c9d8899f..089bf5ae3 100644
--- a/docs/compiler/generated-code.md
+++ b/docs/compiler/generated-code.md
@@ -787,16 +787,26 @@ public sealed partial class Person
}
```
-Unions generate `[ForyUnion]` `Union` subclasses with typed case helpers:
+Unions generate `[ForyUnion]` ADTs. Case ID `0` is the unknown-case
+carrier; schema-defined cases use positive `[ForyCase]` IDs. If a case needs
+non-default schema encoding, the generated `[ForyCase]` carries `Type`. Known
+case record names are PascalCase FDL case names; payload types are emitted as
+qualified references when needed to avoid name conflicts.
```csharp
[ForyUnion]
-public sealed class Animal : Union
+public abstract partial record Animal
{
- public static Animal Dog(Dog value) { ... }
- public static Animal Cat(Cat value) { ... }
- public bool IsDog => ...;
- public Dog DogValue() { ... }
+ private Animal() {}
+
+ [ForyCase(0)]
+ public sealed partial record UnknownCase(int CaseId, object? Value) :
Animal;
+
+ [ForyCase(1)]
+ public sealed partial record Dog(global::addressbook.Dog Value) : Animal;
+
+ [ForyCase(2)]
+ public sealed partial record Cat(global::addressbook.Cat Value) : Animal;
}
```
diff --git a/docs/guide/csharp/schema-metadata.md
b/docs/guide/csharp/schema-metadata.md
index 37ebd33a7..52ae4cea8 100644
--- a/docs/guide/csharp/schema-metadata.md
+++ b/docs/guide/csharp/schema-metadata.md
@@ -19,7 +19,7 @@ license: |
limitations under the License.
---
-This page covers field-level serializer configuration for C# generated
serializers.
+This page covers schema metadata for C# generated serializers.
## `[ForyStruct]` and `[ForyField]`
@@ -81,6 +81,33 @@ Dense array fields use `S.Array<TElement>`, for example
`S.Array<S.Int32>` or `S
Nullability comes from the C# carrier type. Use `List<ulong?>` for nullable
list elements and `NullableKeyDictionary<TKey, TValue>` when a map needs
nullable keys.
+## `[ForyUnion]` and `[ForyCase]`
+
+Generated union cases use `[ForyCase]` for both the stable case ID and optional
+case payload schema type. Do not put `[ForyField]` on union case payload
+members. Known case record names use PascalCase FDL case names; payload types
+use qualified references when needed to avoid name conflicts.
+
+```csharp
+using Apache.Fory;
+using S = Apache.Fory.Schema.Types;
+
+[ForyUnion]
+public abstract partial record Shape
+{
+ private Shape() {}
+
+ [ForyCase(0)]
+ public sealed partial record UnknownCase(int CaseId, object? Value) :
Shape;
+
+ [ForyCase(1)]
+ public sealed partial record Circle(global::example.Circle Value) : Shape;
+
+ [ForyCase(2, Type = typeof(S.Fixed<S.Int32>))]
+ public sealed partial record Code(int Value) : Shape;
+}
+```
+
## Nullability and Reference Tracking
- Field nullability comes from C# type nullability (`string?`, nullable value
types, etc.).
diff --git a/docs/guide/csharp/supported-types.md
b/docs/guide/csharp/supported-types.md
index e93b59221..3148694ad 100644
--- a/docs/guide/csharp/supported-types.md
+++ b/docs/guide/csharp/supported-types.md
@@ -76,7 +76,7 @@ This page summarizes built-in and generated type support in
Apache Fory™ C#.
## User Types
-- `[ForyStruct]` classes/structs via source-generated serializers, plus
`[ForyEnum]` enums and `[ForyUnion]` union subclasses
+- `[ForyStruct]` classes/structs via source-generated serializers, plus
`[ForyEnum]` enums and `[ForyUnion]` ADT records
- Custom serializer types registered through `Register<T, TSerializer>(...)`
- `Union` / `Union2<...>` typed union support
diff --git a/docs/specification/xlang_type_mapping.md
b/docs/specification/xlang_type_mapping.md
index 9894bbb1f..ba29e27d6 100644
--- a/docs/specification/xlang_type_mapping.md
+++ b/docs/specification/xlang_type_mapping.md
@@ -88,7 +88,7 @@ FDL spells them as an encoding modifier plus a semantic
integer type.
| named_compatible_struct | 30 | pojo/record
| data class | object
| struct/class |
struct | struct
| `[ForyStruct]` class/struct | @ForyStruct struct/class |
@ForyStruct class | case class/class | data
class/class |
| ext | 31 | pojo/record
| data class | object
| struct/class |
struct | struct
| `[ForyStruct]` class/struct | @ForyStruct struct/class |
@ForyStruct class | case class/class | data
class/class |
| named_ext | 32 | pojo/record
| data class | object
| struct/class |
struct | struct
| `[ForyStruct]` class/struct | @ForyStruct struct/class |
@ForyStruct class | case class/class | data
class/class |
-| union | 33 | Union
| typing.Union | /
| `std::variant<Ts...>` | /
| tagged union enum
| `[ForyUnion]` Union subclass | tagged enum | @ForyUnion
class | ADT enum | sealed class |
+| union | 33 | Union
| typing.Union | /
| `std::variant<Ts...>` | /
| tagged union enum
| `[ForyUnion]` ADT record | tagged enum | @ForyUnion
class | ADT enum | sealed class |
| none | 36 | null
| None | null
| `std::monostate` |
nil | `()`
| null | nil | null
| null | null |
| duration | 37 | Duration
| timedelta | Number
| duration |
Duration | Duration
| TimeSpan | Duration | Duration
| java.time.Duration | kotlin.time.Duration |
| timestamp | 38 | Instant
| datetime | Number
| std::chrono::nanoseconds |
Time | Timestamp
| DateTime/DateTimeOffset | Date | Timestamp
| java.time.Instant | java.time.Instant |
diff --git a/integration_tests/idl_tests/csharp/IdlTests/RoundtripTests.cs
b/integration_tests/idl_tests/csharp/IdlTests/RoundtripTests.cs
index 92b5bb089..f292ae05f 100644
--- a/integration_tests/idl_tests/csharp/IdlTests/RoundtripTests.cs
+++ b/integration_tests/idl_tests/csharp/IdlTests/RoundtripTests.cs
@@ -47,14 +47,15 @@ public sealed class RoundtripTests
auto_id.AutoIdForyRegistration.Register(fory);
auto_id.Envelope envelope = BuildEnvelope();
- auto_id.Wrapper wrapper = auto_id.Wrapper.Envelope(envelope);
+ auto_id.Wrapper wrapper = new auto_id.Wrapper.Envelope(envelope);
auto_id.Envelope envelopeDecoded =
fory.Deserialize<auto_id.Envelope>(fory.Serialize(envelope));
AssertEnvelope(envelope, envelopeDecoded);
auto_id.Wrapper wrapperDecoded =
fory.Deserialize<auto_id.Wrapper>(fory.Serialize(wrapper));
- Assert.True(wrapperDecoded.IsEnvelope);
- AssertEnvelope(envelope, wrapperDecoded.EnvelopeValue());
+ auto_id.Wrapper.Envelope wrapperEnvelope =
+ Assert.IsType<auto_id.Wrapper.Envelope>(wrapperDecoded);
+ AssertEnvelope(envelope, wrapperEnvelope.Value);
RoundTripFile(fory, "DATA_FILE_AUTO_ID", envelope, AssertEnvelope);
}
@@ -344,15 +345,15 @@ public sealed class RoundtripTests
addressbook.AddressBook decodedBook =
addressbook.AddressBook.FromBytes(book.ToBytes());
AssertAddressBook(book, decodedBook);
- addressbook.Animal animal = addressbook.Animal.Dog(new addressbook.Dog
+ addressbook.Animal animal = new addressbook.Animal.Dog(new
addressbook.Dog
{
Name = "Rex",
BarkVolume = 5,
});
addressbook.Animal decodedAnimal =
addressbook.Animal.FromBytes(animal.ToBytes());
- Assert.True(decodedAnimal.IsDog);
- Assert.Equal("Rex", decodedAnimal.DogValue().Name);
- Assert.Equal(5, decodedAnimal.DogValue().BarkVolume);
+ addressbook.Animal.Dog dog =
Assert.IsType<addressbook.Animal.Dog>(decodedAnimal);
+ Assert.Equal("Rex", dog.Value.Name);
+ Assert.Equal(5, dog.Value.BarkVolume);
}
private static ForyRuntime BuildFory(bool compatible, bool trackRef)
@@ -414,12 +415,12 @@ public sealed class RoundtripTests
PhoneType = addressbook.Person.PhoneType.Work,
};
- addressbook.Animal pet = addressbook.Animal.Dog(new addressbook.Dog
+ addressbook.Animal pet = new addressbook.Animal.Dog(new addressbook.Dog
{
Name = "Rex",
BarkVolume = 5,
});
- pet = addressbook.Animal.Cat(new addressbook.Cat
+ pet = new addressbook.Animal.Cat(new addressbook.Cat
{
Name = "Mimi",
Lives = 9,
@@ -462,7 +463,7 @@ public sealed class RoundtripTests
{
Id = "env-1",
PayloadValue = payload,
- DetailValue = auto_id.Envelope.Detail.Payload(payload),
+ DetailValue = new auto_id.Envelope.Detail.Payload(payload),
Status = auto_id.Status.Ok,
};
}
@@ -488,7 +489,7 @@ public sealed class RoundtripTests
TaggedU64Value = 2222222222,
Float32Value = 2.5f,
Float64Value = 3.5,
- ContactValue = complex_pb.PrimitiveTypes.Contact.Phone(12345),
+ ContactValue = new complex_pb.PrimitiveTypes.Contact.Phone(12345),
};
}
@@ -511,7 +512,7 @@ public sealed class RoundtripTests
private static collection.NumericCollectionUnion
BuildNumericCollectionUnion()
{
- return collection.NumericCollectionUnion.Int32Values([7, 8, 9]);
+ return new collection.NumericCollectionUnion.Int32Values([7, 8, 9]);
}
private static collection.NumericCollectionsArray
BuildNumericCollectionsArray()
@@ -533,7 +534,7 @@ public sealed class RoundtripTests
private static collection.NumericCollectionArrayUnion
BuildNumericCollectionArrayUnion()
{
- return collection.NumericCollectionArrayUnion.Uint16Values([1000,
2000, 3000]);
+ return new collection.NumericCollectionArrayUnion.Uint16Values([1000,
2000, 3000]);
}
private static example.ExampleMessage BuildExampleMessage()
@@ -548,7 +549,7 @@ public sealed class RoundtripTests
Label = "other",
Count = 8,
};
- example.ExampleLeafUnion leafUnion =
example.ExampleLeafUnion.Leaf(otherLeaf);
+ example.ExampleLeafUnion leafUnion = new
example.ExampleLeafUnion.Leaf(otherLeaf);
return new example.ExampleMessage
{
@@ -622,7 +623,7 @@ public sealed class RoundtripTests
DecimalList = [1.25m, 2.50m],
EnumList = [example.ExampleState.Unknown,
example.ExampleState.Failed],
MessageList = [leaf, otherLeaf],
- UnionList = [example.ExampleLeafUnion.Note("note"), leafUnion],
+ UnionList = [new example.ExampleLeafUnion.Note("note"), leafUnion],
MaybeFixedI32List = [1, null, 3],
MaybeUint64List = [10, null, 30],
BoolArray = [true, false],
@@ -681,7 +682,7 @@ public sealed class RoundtripTests
MessageValuesByName = new Dictionary<string, example.ExampleLeaf>
{ ["leaf"] = leaf },
UnionValuesByName = new Dictionary<string,
example.ExampleLeafUnion>
{
- ["union"] = example.ExampleLeafUnion.Code(42),
+ ["union"] = new example.ExampleLeafUnion.Code(42),
},
Uint8ArrayValuesByName = new Dictionary<string, byte[]>
{
@@ -730,12 +731,12 @@ public sealed class RoundtripTests
private static example.ExampleMessageUnion BuildExampleMessageUnion()
{
- return example.ExampleMessageUnion.Int32ArrayList([[11, 12], [13,
14]]);
+ return new example.ExampleMessageUnion.Int32ArrayList([[11, 12], [13,
14]]);
}
private static example.ExampleMessageUnion BuildExampleArrayListUnion()
{
- return example.ExampleMessageUnion.Uint8ArrayList([[4, 5], [6]]);
+ return new example.ExampleMessageUnion.Uint8ArrayList([[4, 5], [6]]);
}
private static optional_types.OptionalHolder BuildOptionalHolder()
@@ -781,7 +782,7 @@ public sealed class RoundtripTests
return new optional_types.OptionalHolder
{
AllTypes = all,
- Choice = optional_types.OptionalUnion.Note("optional"),
+ Choice = new optional_types.OptionalUnion.Note("optional"),
};
}
@@ -797,7 +798,7 @@ public sealed class RoundtripTests
{
Name = "inner",
},
- UnionValue = any_example.AnyUnion.Text("union"),
+ UnionValue = new any_example.AnyUnion.Text("union"),
ListValue = new List<string>
{
"alpha",
@@ -815,7 +816,7 @@ public sealed class RoundtripTests
{
any_example_pb.AnyUnion union = new()
{
- KindValue = any_example_pb.AnyUnion.Kind.Text("proto-union"),
+ KindValue = new any_example_pb.AnyUnion.Kind.Text("proto-union"),
};
return new any_example_pb.AnyHolder
@@ -885,7 +886,7 @@ public sealed class RoundtripTests
},
Names = ["alpha", "beta"],
Flags = [true, false],
- Payload = complex_fbs.Payload.Metric(new complex_fbs.Metric
+ Payload = new complex_fbs.Payload.Metric(new complex_fbs.Metric
{
Value = 42.0,
}),
@@ -986,9 +987,9 @@ public sealed class RoundtripTests
Assert.Equal(expectedPerson.Phones.Count, actualPerson.Phones.Count);
Assert.Equal(expectedPerson.Phones[0].Number,
actualPerson.Phones[0].Number);
Assert.Equal(expectedPerson.Phones[0].PhoneType,
actualPerson.Phones[0].PhoneType);
- Assert.True(actualPerson.Pet.IsCat);
- Assert.Equal("Mimi", actualPerson.Pet.CatValue().Name);
- Assert.Equal(9, actualPerson.Pet.CatValue().Lives);
+ addressbook.Animal.Cat cat =
Assert.IsType<addressbook.Animal.Cat>(actualPerson.Pet);
+ Assert.Equal("Mimi", cat.Value.Name);
+ Assert.Equal(9, cat.Value.Lives);
}
private static void AssertEnvelope(auto_id.Envelope expected,
auto_id.Envelope actual)
@@ -1000,8 +1001,11 @@ public sealed class RoundtripTests
Assert.Equal(expected.Status, actual.Status);
Assert.NotNull(actual.DetailValue);
- Assert.True(actual.DetailValue.IsPayload);
- Assert.Equal(expected.DetailValue.PayloadValue().Value,
actual.DetailValue.PayloadValue().Value);
+ auto_id.Envelope.Detail.Payload expectedPayload =
+
Assert.IsType<auto_id.Envelope.Detail.Payload>(expected.DetailValue);
+ auto_id.Envelope.Detail.Payload actualPayload =
+ Assert.IsType<auto_id.Envelope.Detail.Payload>(actual.DetailValue);
+ Assert.Equal(expectedPayload.Value.Value, actualPayload.Value.Value);
}
private static void AssertPrimitiveTypes(complex_pb.PrimitiveTypes
expected, complex_pb.PrimitiveTypes actual)
@@ -1026,9 +1030,11 @@ public sealed class RoundtripTests
Assert.NotNull(expected.ContactValue);
Assert.NotNull(actual.ContactValue);
- Assert.Equal(expected.ContactValue.CaseId(),
actual.ContactValue.CaseId());
- Assert.True(actual.ContactValue.IsPhone);
- Assert.Equal(expected.ContactValue.PhoneValue(),
actual.ContactValue.PhoneValue());
+ complex_pb.PrimitiveTypes.Contact.Phone expectedPhone =
+
Assert.IsType<complex_pb.PrimitiveTypes.Contact.Phone>(expected.ContactValue);
+ complex_pb.PrimitiveTypes.Contact.Phone actualPhone =
+
Assert.IsType<complex_pb.PrimitiveTypes.Contact.Phone>(actual.ContactValue);
+ Assert.Equal(expectedPhone.Value, actualPhone.Value);
}
private static void AssertNumericCollections(
@@ -1051,9 +1057,11 @@ public sealed class RoundtripTests
collection.NumericCollectionUnion expected,
collection.NumericCollectionUnion actual)
{
- Assert.Equal(expected.CaseId(), actual.CaseId());
- Assert.True(actual.IsInt32Values);
- Assert.Equal(expected.Int32ValuesValue(), actual.Int32ValuesValue());
+ collection.NumericCollectionUnion.Int32Values expectedValues =
+
Assert.IsType<collection.NumericCollectionUnion.Int32Values>(expected);
+ collection.NumericCollectionUnion.Int32Values actualValues =
+
Assert.IsType<collection.NumericCollectionUnion.Int32Values>(actual);
+ Assert.Equal(expectedValues.Value, actualValues.Value);
}
private static void AssertNumericCollectionsArray(
@@ -1076,9 +1084,11 @@ public sealed class RoundtripTests
collection.NumericCollectionArrayUnion expected,
collection.NumericCollectionArrayUnion actual)
{
- Assert.Equal(expected.CaseId(), actual.CaseId());
- Assert.True(actual.IsUint16Values);
- Assert.Equal(expected.Uint16ValuesValue(), actual.Uint16ValuesValue());
+ collection.NumericCollectionArrayUnion.Uint16Values expectedValues =
+
Assert.IsType<collection.NumericCollectionArrayUnion.Uint16Values>(expected);
+ collection.NumericCollectionArrayUnion.Uint16Values actualValues =
+
Assert.IsType<collection.NumericCollectionArrayUnion.Uint16Values>(actual);
+ Assert.Equal(expectedValues.Value, actualValues.Value);
}
private static void AssertExampleMessage(example.ExampleMessage expected,
example.ExampleMessage actual)
@@ -1173,23 +1183,25 @@ public sealed class RoundtripTests
{
Assert.NotNull(expected);
Assert.NotNull(actual);
- Assert.Equal(expected.CaseId(), actual.CaseId());
- switch (expected.Case())
+ switch (expected)
{
- case example.ExampleLeafUnion.ExampleLeafUnionCase.Note:
- Assert.True(actual.IsNote);
- Assert.Equal(expected.NoteValue(), actual.NoteValue());
+ case example.ExampleLeafUnion.Note expectedNote:
+ example.ExampleLeafUnion.Note actualNote =
+ Assert.IsType<example.ExampleLeafUnion.Note>(actual);
+ Assert.Equal(expectedNote.Value, actualNote.Value);
break;
- case example.ExampleLeafUnion.ExampleLeafUnionCase.Code:
- Assert.True(actual.IsCode);
- Assert.Equal(expected.CodeValue(), actual.CodeValue());
+ case example.ExampleLeafUnion.Code expectedCode:
+ example.ExampleLeafUnion.Code actualCode =
+ Assert.IsType<example.ExampleLeafUnion.Code>(actual);
+ Assert.Equal(expectedCode.Value, actualCode.Value);
break;
- case example.ExampleLeafUnion.ExampleLeafUnionCase.Leaf:
- Assert.True(actual.IsLeaf);
- AssertExampleLeaf(expected.LeafValue(), actual.LeafValue());
+ case example.ExampleLeafUnion.Leaf expectedLeaf:
+ example.ExampleLeafUnion.Leaf actualLeaf =
+ Assert.IsType<example.ExampleLeafUnion.Leaf>(actual);
+ AssertExampleLeaf(expectedLeaf.Value, actualLeaf.Value);
break;
default:
- Assert.Fail($"Unexpected ExampleLeafUnion case
{expected.CaseId()}");
+ Assert.Fail($"Unexpected ExampleLeafUnion case
{expected.GetType()}");
break;
}
}
@@ -1200,27 +1212,30 @@ public sealed class RoundtripTests
{
Assert.NotNull(expected);
Assert.NotNull(actual);
- Assert.Equal(expected.CaseId(), actual.CaseId());
- switch (expected.Case())
+ switch (expected)
{
- case
example.ExampleMessageUnion.ExampleMessageUnionCase.Int32ArrayList:
- Assert.True(actual.IsInt32ArrayList);
- AssertArrayList(expected.Int32ArrayListValue(),
actual.Int32ArrayListValue());
+ case example.ExampleMessageUnion.Int32ArrayList expectedIntList:
+ example.ExampleMessageUnion.Int32ArrayList actualIntList =
+
Assert.IsType<example.ExampleMessageUnion.Int32ArrayList>(actual);
+ AssertArrayList(expectedIntList.Value, actualIntList.Value);
break;
- case
example.ExampleMessageUnion.ExampleMessageUnionCase.Uint8ArrayList:
- Assert.True(actual.IsUint8ArrayList);
- AssertArrayList(expected.Uint8ArrayListValue(),
actual.Uint8ArrayListValue());
+ case example.ExampleMessageUnion.Uint8ArrayList expectedByteList:
+ example.ExampleMessageUnion.Uint8ArrayList actualByteList =
+
Assert.IsType<example.ExampleMessageUnion.Uint8ArrayList>(actual);
+ AssertArrayList(expectedByteList.Value, actualByteList.Value);
break;
- case
example.ExampleMessageUnion.ExampleMessageUnionCase.Uint8ArrayValuesByName:
- Assert.True(actual.IsUint8ArrayValuesByName);
- AssertArrayMap(expected.Uint8ArrayValuesByNameValue(),
actual.Uint8ArrayValuesByNameValue());
+ case example.ExampleMessageUnion.Uint8ArrayValuesByName
expectedByteMap:
+ example.ExampleMessageUnion.Uint8ArrayValuesByName
actualByteMap =
+
Assert.IsType<example.ExampleMessageUnion.Uint8ArrayValuesByName>(actual);
+ AssertArrayMap(expectedByteMap.Value, actualByteMap.Value);
break;
- case
example.ExampleMessageUnion.ExampleMessageUnionCase.Int32ArrayValuesByName:
- Assert.True(actual.IsInt32ArrayValuesByName);
- AssertArrayMap(expected.Int32ArrayValuesByNameValue(),
actual.Int32ArrayValuesByNameValue());
+ case example.ExampleMessageUnion.Int32ArrayValuesByName
expectedIntMap:
+ example.ExampleMessageUnion.Int32ArrayValuesByName
actualIntMap =
+
Assert.IsType<example.ExampleMessageUnion.Int32ArrayValuesByName>(actual);
+ AssertArrayMap(expectedIntMap.Value, actualIntMap.Value);
break;
default:
- Assert.Fail($"Unexpected ExampleMessageUnion case
{expected.CaseId()}");
+ Assert.Fail($"Unexpected ExampleMessageUnion case
{expected.GetType()}");
break;
}
}
@@ -1266,9 +1281,11 @@ public sealed class RoundtripTests
Assert.NotNull(actual.Choice);
Assert.NotNull(expected.Choice);
- Assert.Equal(expected.Choice.CaseId(), actual.Choice.CaseId());
- Assert.True(actual.Choice.IsNote);
- Assert.Equal(expected.Choice.NoteValue(), actual.Choice.NoteValue());
+ optional_types.OptionalUnion.Note expectedChoice =
+ Assert.IsType<optional_types.OptionalUnion.Note>(expected.Choice);
+ optional_types.OptionalUnion.Note actualChoice =
+ Assert.IsType<optional_types.OptionalUnion.Note>(actual.Choice);
+ Assert.Equal(expectedChoice.Value, actualChoice.Value);
}
private static void AssertAnyHolder(any_example.AnyHolder expected,
any_example.AnyHolder actual)
@@ -1293,11 +1310,11 @@ public sealed class RoundtripTests
Assert.True(TryAnyInnerName(actual.MessageValue, out string?
actualInnerName));
Assert.Equal(expectedInnerName, actualInnerName);
- any_example.AnyUnion actualUnion =
Assert.IsType<any_example.AnyUnion>(actual.UnionValue);
- any_example.AnyUnion expectedUnion =
Assert.IsType<any_example.AnyUnion>(expected.UnionValue);
- Assert.Equal(expectedUnion.CaseId(), actualUnion.CaseId());
- Assert.True(actualUnion.IsText);
- Assert.Equal(expectedUnion.TextValue(), actualUnion.TextValue());
+ any_example.AnyUnion.Text actualUnion =
+ Assert.IsType<any_example.AnyUnion.Text>(actual.UnionValue);
+ any_example.AnyUnion.Text expectedUnion =
+ Assert.IsType<any_example.AnyUnion.Text>(expected.UnionValue);
+ Assert.Equal(expectedUnion.Value, actualUnion.Value);
Assert.True(TryStringList(expected.ListValue, out List<string>
expectedList));
Assert.True(TryStringList(actual.ListValue, out List<string>
actualList));
@@ -1334,9 +1351,11 @@ public sealed class RoundtripTests
any_example_pb.AnyUnion actualUnion =
Assert.IsType<any_example_pb.AnyUnion>(actual.UnionValue);
Assert.NotNull(expectedUnion.KindValue);
Assert.NotNull(actualUnion.KindValue);
- Assert.Equal(expectedUnion.KindValue!.CaseId(),
actualUnion.KindValue!.CaseId());
- Assert.True(actualUnion.KindValue.IsText);
- Assert.Equal(expectedUnion.KindValue.TextValue(),
actualUnion.KindValue.TextValue());
+ any_example_pb.AnyUnion.Kind.Text expectedKind =
+
Assert.IsType<any_example_pb.AnyUnion.Kind.Text>(expectedUnion.KindValue);
+ any_example_pb.AnyUnion.Kind.Text actualKind =
+
Assert.IsType<any_example_pb.AnyUnion.Kind.Text>(actualUnion.KindValue);
+ Assert.Equal(expectedKind.Value, actualKind.Value);
Assert.True(TryStringList(expected.ListValue, out List<string>
expectedList));
Assert.True(TryStringList(actual.ListValue, out List<string>
actualList));
@@ -1386,9 +1405,11 @@ public sealed class RoundtripTests
Assert.NotNull(expected.Payload);
Assert.NotNull(actual.Payload);
- Assert.Equal(expected.Payload.CaseId(), actual.Payload.CaseId());
- Assert.True(actual.Payload.IsMetric);
- Assert.Equal(expected.Payload.MetricValue().Value,
actual.Payload.MetricValue().Value);
+ complex_fbs.Payload.Metric expectedMetric =
+ Assert.IsType<complex_fbs.Payload.Metric>(expected.Payload);
+ complex_fbs.Payload.Metric actualMetric =
+ Assert.IsType<complex_fbs.Payload.Metric>(actual.Payload);
+ Assert.Equal(expectedMetric.Value.Value, actualMetric.Value.Value);
}
private static void AssertTree(tree.TreeNode root)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]