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 037cd7b0e feat(compiler): add csharp target and idl integration tests
(#3406)
037cd7b0e is described below
commit 037cd7b0ebc045f24a43f65ba91d61640bcd5868
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Feb 25 23:39:21 2026 +0800
feat(compiler): add csharp target and idl integration tests (#3406)
## Why?
Add full C# schema IDL support end-to-end in Apache Fory: compiler
target, documentation, runtime compatibility fixes, and integration
coverage (including root/package cross-reference and file-based
roundtrip scenarios).
## What does this PR do?
- Adds C# as a first-class compiler target:
- `foryc --lang ... ,csharp`
- `foryc --csharp_out=<dir>`
- `option csharp_namespace = "..."`
- Implements C# schema generator
(`compiler/fory_compiler/generators/csharp.py`) for
messages/enums/unions, registration helpers, and generated
`ToBytes`/`FromBytes` helpers.
- Wires C# into compiler generator registration and generated-code test
matrix.
- Adds C# generator unit tests
(`compiler/fory_compiler/tests/test_csharp_generator.py`) covering
namespace resolution, registration behavior, field encoding attributes,
imported registration calls, and parser option support.
- Adds full C# IDL integration test harness under
`integration_tests/idl_tests/csharp/IdlTests` and runner
`integration_tests/idl_tests/run_csharp_tests.sh`, including
schema-consistent and compatible roundtrips across:
- `addressbook`, `auto_id`, `complex_pb` primitives
- collection union/array variants
- `optional_types`
- `any_example` (`.fdl`) and `any_example` (`.proto`)
- flatbuffers (`monster`, `complex_fbs`)
- reference-tracking models (`tree`, `graph`)
- evolving compatibility cases
- root/package cross-reference (`root.idl`) and generated bytes helper
paths
- Adds C# IDL output target to
`integration_tests/idl_tests/generate_idl.py`.
- Improves C# runtime compatibility behavior for generated IDL shapes:
- collection/dictionary serializers now handle declared-type metadata
correctly in compatible mode.
- union serializer now supports typed case handling while preserving
xlang-compatible dynamic framing and case value normalization.
- Updates compiler documentation (`compiler/README.md`,
`docs/compiler/*`) and C# README to document C# codegen, options,
generated output, and test workflow.
## Related issues
N/A
## Does this PR introduce any user-facing change?
Yes. `foryc` now supports C# generation (`--lang csharp`,
`--csharp_out`) and C# namespace override (`csharp_namespace`), with
updated compiler docs and C# IDL test workflow.
- [x] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
N/A (feature + compatibility work; no benchmark updates in this PR)
---
compiler/README.md | 65 +-
compiler/fory_compiler/cli.py | 11 +-
compiler/fory_compiler/frontend/fdl/parser.py | 1 +
compiler/fory_compiler/generators/__init__.py | 3 +
compiler/fory_compiler/generators/csharp.py | 909 +++++++++++++++
.../fory_compiler/tests/test_csharp_generator.py | 139 +++
.../fory_compiler/tests/test_generated_code.py | 2 +
csharp/README.md | 34 +-
csharp/src/Fory/CollectionSerializers.cs | 14 +-
csharp/src/Fory/DictionarySerializers.cs | 48 +-
csharp/src/Fory/NullableKeyDictionary.cs | 48 +-
csharp/src/Fory/UnionSerializer.cs | 191 ++-
docs/compiler/compiler-guide.md | 57 +-
docs/compiler/generated-code.md | 59 +
docs/compiler/index.md | 15 +-
docs/compiler/schema-idl.md | 24 +-
integration_tests/idl_tests/README.md | 1 +
.../idl_tests/csharp/IdlTests/.gitignore | 4 +
.../idl_tests/csharp/IdlTests/GlobalUsings.cs | 18 +
.../idl_tests/csharp/IdlTests/IdlTests.csproj | 28 +
.../idl_tests/csharp/IdlTests/RoundtripTests.cs | 1233 ++++++++++++++++++++
integration_tests/idl_tests/generate_idl.py | 1 +
integration_tests/idl_tests/run_csharp_tests.sh | 49 +
23 files changed, 2868 insertions(+), 86 deletions(-)
diff --git a/compiler/README.md b/compiler/README.md
index bd0704ffd..c34824803 100644
--- a/compiler/README.md
+++ b/compiler/README.md
@@ -4,7 +4,7 @@ The FDL compiler generates cross-language serialization code
from schema definit
## Features
-- **Multi-language code generation**: Java, Python, Go, Rust, C++
+- **Multi-language code generation**: Java, Python, Go, Rust, C++, C#
- **Rich type system**: Primitives, enums, messages, lists, maps
- **Cross-language serialization**: Generated code works seamlessly with
Apache Fory
- **Type ID and namespace support**: Both numeric IDs and name-based type
registration
@@ -64,16 +64,16 @@ message Cat [id=103] {
foryc schema.fdl --output ./generated
# Generate for specific languages
-foryc schema.fdl --lang java,python --output ./generated
+foryc schema.fdl --lang java,python,csharp --output ./generated
# Override package name
foryc schema.fdl --package myapp.models --output ./generated
# Language-specific output directories (protoc-style)
-foryc schema.fdl --java_out=./src/main/java --python_out=./python/src
+foryc schema.fdl --java_out=./src/main/java --python_out=./python/src
--csharp_out=./csharp/src/Generated
# Combine with other options
-foryc schema.fdl --java_out=./gen --go_out=./gen/go -I ./proto
+foryc schema.fdl --java_out=./gen --go_out=./gen/go --csharp_out=./gen/csharp
-I ./proto
```
### 3. Use Generated Code
@@ -185,19 +185,19 @@ message Config { ... } // Registered as "package.Config"
### Primitive Types
-| FDL Type | Java | Python | Go | Rust
| C++ |
-| ----------- | ----------- | ------------------- | ----------- |
----------------------- | ---------------------- |
-| `bool` | `boolean` | `bool` | `bool` | `bool`
| `bool` |
-| `int8` | `byte` | `pyfory.int8` | `int8` | `i8`
| `int8_t` |
-| `int16` | `short` | `pyfory.int16` | `int16` | `i16`
| `int16_t` |
-| `int32` | `int` | `pyfory.int32` | `int32` | `i32`
| `int32_t` |
-| `int64` | `long` | `pyfory.int64` | `int64` | `i64`
| `int64_t` |
-| `float32` | `float` | `pyfory.float32` | `float32` | `f32`
| `float` |
-| `float64` | `double` | `pyfory.float64` | `float64` | `f64`
| `double` |
-| `string` | `String` | `str` | `string` | `String`
| `std::string` |
-| `bytes` | `byte[]` | `bytes` | `[]byte` | `Vec<u8>`
| `std::vector<uint8_t>` |
-| `date` | `LocalDate` | `datetime.date` | `time.Time` |
`chrono::NaiveDate` | `fory::Date` |
-| `timestamp` | `Instant` | `datetime.datetime` | `time.Time` |
`chrono::NaiveDateTime` | `fory::Timestamp` |
+| FDL Type | Java | Python | Go | Rust
| C++ | C# |
+| ----------- | ----------- | ------------------- | ----------- |
----------------------- | ---------------------- | ---------------- |
+| `bool` | `boolean` | `bool` | `bool` | `bool`
| `bool` | `bool` |
+| `int8` | `byte` | `pyfory.int8` | `int8` | `i8`
| `int8_t` | `sbyte` |
+| `int16` | `short` | `pyfory.int16` | `int16` | `i16`
| `int16_t` | `short` |
+| `int32` | `int` | `pyfory.int32` | `int32` | `i32`
| `int32_t` | `int` |
+| `int64` | `long` | `pyfory.int64` | `int64` | `i64`
| `int64_t` | `long` |
+| `float32` | `float` | `pyfory.float32` | `float32` | `f32`
| `float` | `float` |
+| `float64` | `double` | `pyfory.float64` | `float64` | `f64`
| `double` | `double` |
+| `string` | `String` | `str` | `string` | `String`
| `std::string` | `string` |
+| `bytes` | `byte[]` | `bytes` | `[]byte` | `Vec<u8>`
| `std::vector<uint8_t>` | `byte[]` |
+| `date` | `LocalDate` | `datetime.date` | `time.Time` |
`chrono::NaiveDate` | `fory::Date` | `DateOnly` |
+| `timestamp` | `Instant` | `datetime.datetime` | `time.Time` |
`chrono::NaiveDateTime` | `fory::Timestamp` | `DateTimeOffset` |
### Collection Types
@@ -284,7 +284,8 @@ fory_compiler/
├── python.py # Python dataclass generator
├── go.py # Go struct generator
├── rust.py # Rust struct generator
- └── cpp.py # C++ struct generator
+ ├── cpp.py # C++ struct generator
+ └── csharp.py # C# class generator
```
### FDL Frontend
@@ -395,6 +396,32 @@ struct Cat {
};
```
+### C\#
+
+Generates classes with:
+
+- `[ForyObject]` model attributes
+- Auto-properties for schema fields
+- Registration helper class and `ToBytes`/`FromBytes` helpers
+
+```csharp
+[ForyObject]
+public sealed partial class Cat
+{
+ public Dog? Friend { get; set; }
+ public string Name { get; set; } = string.Empty;
+ public List<string> Tags { get; set; } = new();
+}
+```
+
+For full C# IDL verification (including root cross-package imports and
file-based
+roundtrip paths), run:
+
+```bash
+cd integration_tests/idl_tests
+./run_csharp_tests.sh
+```
+
## CLI Reference
```
@@ -404,7 +431,7 @@ Arguments:
FILES FDL files to compile
Options:
- --lang TEXT Target languages (java,python,cpp,rust,go or "all")
+ --lang TEXT Target languages (java,python,cpp,rust,go,csharp or
"all")
Default: all
--output, -o PATH Output directory
Default: ./generated
diff --git a/compiler/fory_compiler/cli.py b/compiler/fory_compiler/cli.py
index 35853804a..8604ae654 100644
--- a/compiler/fory_compiler/cli.py
+++ b/compiler/fory_compiler/cli.py
@@ -264,7 +264,7 @@ def parse_args(args: Optional[List[str]] = None) ->
argparse.Namespace:
"--lang",
type=str,
default="all",
- help="Comma-separated list of target languages
(java,python,cpp,rust,go). Default: all",
+ help="Comma-separated list of target languages
(java,python,cpp,rust,go,csharp). Default: all",
)
parser.add_argument(
@@ -335,6 +335,14 @@ def parse_args(args: Optional[List[str]] = None) ->
argparse.Namespace:
help="Generate Rust code in DST_DIR",
)
+ parser.add_argument(
+ "--csharp_out",
+ type=Path,
+ default=None,
+ metavar="DST_DIR",
+ help="Generate C# code in DST_DIR",
+ )
+
parser.add_argument(
"--go_nested_type_style",
type=str,
@@ -620,6 +628,7 @@ def cmd_compile(args: argparse.Namespace) -> int:
"cpp": args.cpp_out,
"go": args.go_out,
"rust": args.rust_out,
+ "csharp": args.csharp_out,
}
# Determine which languages to generate
diff --git a/compiler/fory_compiler/frontend/fdl/parser.py
b/compiler/fory_compiler/frontend/fdl/parser.py
index 79c568f0a..1e2713540 100644
--- a/compiler/fory_compiler/frontend/fdl/parser.py
+++ b/compiler/fory_compiler/frontend/fdl/parser.py
@@ -46,6 +46,7 @@ KNOWN_FILE_OPTIONS: Set[str] = {
"java_outer_classname",
"java_multiple_files",
"go_package",
+ "csharp_namespace",
"deprecated",
"use_record_for_java_message",
"polymorphism",
diff --git a/compiler/fory_compiler/generators/__init__.py
b/compiler/fory_compiler/generators/__init__.py
index 5c8ebd5be..d93533d9c 100644
--- a/compiler/fory_compiler/generators/__init__.py
+++ b/compiler/fory_compiler/generators/__init__.py
@@ -23,6 +23,7 @@ from fory_compiler.generators.python import PythonGenerator
from fory_compiler.generators.cpp import CppGenerator
from fory_compiler.generators.rust import RustGenerator
from fory_compiler.generators.go import GoGenerator
+from fory_compiler.generators.csharp import CSharpGenerator
GENERATORS = {
"java": JavaGenerator,
@@ -30,6 +31,7 @@ GENERATORS = {
"cpp": CppGenerator,
"rust": RustGenerator,
"go": GoGenerator,
+ "csharp": CSharpGenerator,
}
__all__ = [
@@ -39,5 +41,6 @@ __all__ = [
"CppGenerator",
"RustGenerator",
"GoGenerator",
+ "CSharpGenerator",
"GENERATORS",
]
diff --git a/compiler/fory_compiler/generators/csharp.py
b/compiler/fory_compiler/generators/csharp.py
new file mode 100644
index 000000000..565f70a6b
--- /dev/null
+++ b/compiler/fory_compiler/generators/csharp.py
@@ -0,0 +1,909 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""C# code generator."""
+
+from pathlib import Path
+from typing import Dict, List, Optional, Set, Tuple, Union as TypingUnion
+
+from fory_compiler.frontend.utils import parse_idl_file
+from fory_compiler.generators.base import BaseGenerator, GeneratedFile
+from fory_compiler.ir.ast import (
+ Enum,
+ Field,
+ FieldType,
+ ListType,
+ MapType,
+ Message,
+ NamedType,
+ PrimitiveType,
+ Schema,
+ Union,
+)
+from fory_compiler.ir.types import PrimitiveKind
+
+
+class CSharpGenerator(BaseGenerator):
+ """Generates C# models and registration helpers for Apache Fory."""
+
+ language_name = "csharp"
+ file_extension = ".cs"
+
+ PRIMITIVE_MAP = {
+ PrimitiveKind.BOOL: "bool",
+ PrimitiveKind.INT8: "sbyte",
+ PrimitiveKind.INT16: "short",
+ PrimitiveKind.INT32: "int",
+ PrimitiveKind.VARINT32: "int",
+ PrimitiveKind.INT64: "long",
+ PrimitiveKind.VARINT64: "long",
+ PrimitiveKind.TAGGED_INT64: "long",
+ PrimitiveKind.UINT8: "byte",
+ PrimitiveKind.UINT16: "ushort",
+ PrimitiveKind.UINT32: "uint",
+ PrimitiveKind.VAR_UINT32: "uint",
+ PrimitiveKind.UINT64: "ulong",
+ PrimitiveKind.VAR_UINT64: "ulong",
+ PrimitiveKind.TAGGED_UINT64: "ulong",
+ PrimitiveKind.FLOAT16: "float",
+ PrimitiveKind.BFLOAT16: "float",
+ PrimitiveKind.FLOAT32: "float",
+ PrimitiveKind.FLOAT64: "double",
+ PrimitiveKind.STRING: "string",
+ PrimitiveKind.BYTES: "byte[]",
+ PrimitiveKind.DATE: "DateOnly",
+ PrimitiveKind.TIMESTAMP: "DateTimeOffset",
+ PrimitiveKind.DURATION: "TimeSpan",
+ PrimitiveKind.DECIMAL: "decimal",
+ PrimitiveKind.ANY: "object",
+ }
+
+ VALUE_TYPE_KINDS = {
+ PrimitiveKind.BOOL,
+ PrimitiveKind.INT8,
+ PrimitiveKind.INT16,
+ PrimitiveKind.INT32,
+ PrimitiveKind.VARINT32,
+ PrimitiveKind.INT64,
+ PrimitiveKind.VARINT64,
+ PrimitiveKind.TAGGED_INT64,
+ PrimitiveKind.UINT8,
+ PrimitiveKind.UINT16,
+ PrimitiveKind.UINT32,
+ PrimitiveKind.VAR_UINT32,
+ PrimitiveKind.UINT64,
+ PrimitiveKind.VAR_UINT64,
+ PrimitiveKind.TAGGED_UINT64,
+ PrimitiveKind.FLOAT16,
+ PrimitiveKind.BFLOAT16,
+ PrimitiveKind.FLOAT32,
+ PrimitiveKind.FLOAT64,
+ PrimitiveKind.DATE,
+ PrimitiveKind.TIMESTAMP,
+ PrimitiveKind.DURATION,
+ PrimitiveKind.DECIMAL,
+ }
+
+ CSHARP_KEYWORDS = {
+ "abstract",
+ "as",
+ "base",
+ "bool",
+ "break",
+ "byte",
+ "case",
+ "catch",
+ "char",
+ "checked",
+ "class",
+ "const",
+ "continue",
+ "decimal",
+ "default",
+ "delegate",
+ "do",
+ "double",
+ "else",
+ "enum",
+ "event",
+ "explicit",
+ "extern",
+ "false",
+ "finally",
+ "fixed",
+ "float",
+ "for",
+ "foreach",
+ "goto",
+ "if",
+ "implicit",
+ "in",
+ "int",
+ "interface",
+ "internal",
+ "is",
+ "lock",
+ "long",
+ "namespace",
+ "new",
+ "null",
+ "object",
+ "operator",
+ "out",
+ "override",
+ "params",
+ "private",
+ "protected",
+ "public",
+ "readonly",
+ "ref",
+ "return",
+ "sbyte",
+ "sealed",
+ "short",
+ "sizeof",
+ "stackalloc",
+ "static",
+ "string",
+ "struct",
+ "switch",
+ "this",
+ "throw",
+ "true",
+ "try",
+ "typeof",
+ "uint",
+ "ulong",
+ "unchecked",
+ "unsafe",
+ "ushort",
+ "using",
+ "virtual",
+ "void",
+ "volatile",
+ "while",
+ }
+
+ def __init__(self, schema: Schema, options):
+ super().__init__(schema, options)
+ self._qualified_type_names: Dict[int, str] = {}
+ self._build_qualified_type_name_index()
+
+ def _build_qualified_type_name_index(self) -> None:
+ for enum in self.schema.enums:
+ self._qualified_type_names[id(enum)] = enum.name
+ for union in self.schema.unions:
+ self._qualified_type_names[id(union)] = union.name
+
+ def visit_message(message: Message, parents: List[str]) -> None:
+ path = ".".join(parents + [message.name])
+ self._qualified_type_names[id(message)] = path
+ for nested_enum in message.nested_enums:
+ self._qualified_type_names[id(nested_enum)] = (
+ f"{path}.{nested_enum.name}"
+ )
+ for nested_union in message.nested_unions:
+ self._qualified_type_names[id(nested_union)] = (
+ f"{path}.{nested_union.name}"
+ )
+ for nested_msg in message.nested_messages:
+ visit_message(nested_msg, parents + [message.name])
+
+ for message in self.schema.messages:
+ visit_message(message, [])
+
+ def get_csharp_namespace(self) -> str:
+ if self.options.package_override:
+ return self.options.package_override
+ csharp_ns = self.schema.get_option("csharp_namespace")
+ if csharp_ns:
+ return str(csharp_ns)
+ if self.schema.package:
+ return self.schema.package
+ return "generated"
+
+ def get_registration_class_name(self) -> str:
+ return
self._registration_class_name_for_namespace(self.get_csharp_namespace())
+
+ def _registration_class_name_for_namespace(self, namespace_name: str) ->
str:
+ if namespace_name:
+ leaf = namespace_name.split(".")[-1]
+ else:
+ leaf = "generated"
+ return f"{self.to_pascal_case(leaf)}ForyRegistration"
+
+ def _module_file_name(self) -> str:
+ if self.schema.source_file and not
self.schema.source_file.startswith("<"):
+ return f"{Path(self.schema.source_file).stem}.cs"
+ if self.schema.package:
+ return f"{self.schema.package.replace('.', '_')}.cs"
+ return "generated.cs"
+
+ def _namespace_path(self, namespace_name: str) -> str:
+ return namespace_name.replace(".", "/") if namespace_name else ""
+
+ def safe_identifier(self, name: str) -> str:
+ if name in self.CSHARP_KEYWORDS:
+ return f"@{name}"
+ return name
+
+ def safe_type_identifier(self, name: str) -> str:
+ return self.safe_identifier(name)
+
+ def safe_member_name(self, name: str) -> str:
+ return self.safe_identifier(self.to_pascal_case(name))
+
+ def _nested_type_names_for_message(self, message: Message) -> Set[str]:
+ names: Set[str] = set()
+ for nested in (
+ list(message.nested_enums)
+ + list(message.nested_unions)
+ + list(message.nested_messages)
+ ):
+ names.add(self.safe_type_identifier(nested.name))
+ return names
+
+ def _field_member_name(
+ self,
+ field: Field,
+ message: Message,
+ used_names: Set[str],
+ ) -> str:
+ base = self.safe_member_name(field.name)
+ nested_type_names = self._nested_type_names_for_message(message)
+ if base in nested_type_names:
+ base = f"{base}Value"
+
+ candidate = base
+ suffix = 1
+ while candidate in used_names:
+ candidate = f"{base}{suffix}"
+ suffix += 1
+ used_names.add(candidate)
+ return candidate
+
+ def is_imported_type(self, type_def: object) -> bool:
+ if not self.schema.source_file:
+ return False
+ location = getattr(type_def, "location", None)
+ if location is None or not location.file:
+ return False
+ try:
+ return (
+ Path(location.file).resolve() !=
Path(self.schema.source_file).resolve()
+ )
+ except Exception:
+ return location.file != self.schema.source_file
+
+ def split_imported_types(
+ self, items: List[object]
+ ) -> Tuple[List[object], List[object]]:
+ imported: List[object] = []
+ local: List[object] = []
+ for item in items:
+ if self.is_imported_type(item):
+ imported.append(item)
+ else:
+ local.append(item)
+ return imported, local
+
+ def _normalize_import_path(self, path_str: str) -> str:
+ if not path_str:
+ return path_str
+ try:
+ return str(Path(path_str).resolve())
+ except Exception:
+ return path_str
+
+ def _load_schema(self, file_path: str) -> Optional[Schema]:
+ if not file_path:
+ return None
+ if not hasattr(self, "_schema_cache"):
+ self._schema_cache = {}
+ cache: Dict[Path, Schema] = self._schema_cache
+ path = Path(file_path).resolve()
+ if path in cache:
+ return cache[path]
+ try:
+ schema = parse_idl_file(path)
+ except Exception:
+ return None
+ cache[path] = schema
+ return schema
+
+ def _csharp_namespace_for_schema(self, schema: Schema) -> str:
+ value = schema.get_option("csharp_namespace")
+ if value:
+ return str(value)
+ if schema.package:
+ return schema.package
+ return "generated"
+
+ def _csharp_namespace_for_type(self, type_def: object) -> str:
+ location = getattr(type_def, "location", None)
+ file_path = getattr(location, "file", None) if location else None
+ schema = self._load_schema(file_path)
+ if schema is None:
+ return self.get_csharp_namespace()
+ return self._csharp_namespace_for_schema(schema)
+
+ def _collect_imported_registrations(self) -> List[Tuple[str, str]]:
+ file_info: Dict[str, Tuple[str, str]] = {}
+ for type_def in self.schema.enums + self.schema.unions +
self.schema.messages:
+ if not self.is_imported_type(type_def):
+ continue
+ location = getattr(type_def, "location", None)
+ file_path = getattr(location, "file", None) if location else None
+ if not file_path:
+ continue
+ normalized = self._normalize_import_path(file_path)
+ if normalized in file_info:
+ continue
+ imported_schema = self._load_schema(file_path)
+ if imported_schema is None:
+ continue
+ namespace_name = self._csharp_namespace_for_schema(imported_schema)
+ registration_name = self._registration_class_name_for_namespace(
+ namespace_name
+ )
+ file_info[normalized] = (namespace_name, registration_name)
+
+ ordered: List[Tuple[str, str]] = []
+ used: Set[str] = set()
+
+ if self.schema.source_file:
+ base_dir = Path(self.schema.source_file).resolve().parent
+ for imp in self.schema.imports:
+ candidate = self._normalize_import_path(
+ str((base_dir / imp.path).resolve())
+ )
+ if candidate in file_info and candidate not in used:
+ ordered.append(file_info[candidate])
+ used.add(candidate)
+
+ for key in sorted(file_info.keys()):
+ if key in used:
+ continue
+ ordered.append(file_info[key])
+
+ deduped: List[Tuple[str, str]] = []
+ seen: Set[Tuple[str, str]] = set()
+ for item in ordered:
+ if item in seen:
+ continue
+ seen.add(item)
+ deduped.append(item)
+ return deduped
+
+ def generate(self) -> List[GeneratedFile]:
+ return [self.generate_file()]
+
+ def generate_file(self) -> GeneratedFile:
+ lines: List[str] = []
+ namespace_name = self.get_csharp_namespace()
+
+ lines.append(self.get_license_header("//"))
+ lines.append("")
+ lines.append("using System;")
+ lines.append("using System.Collections.Generic;")
+ lines.append("using Apache.Fory;")
+ lines.append("")
+ lines.append(f"namespace {namespace_name};")
+ lines.append("")
+
+ for enum in self.schema.enums:
+ if self.is_imported_type(enum):
+ continue
+ lines.extend(self.generate_enum(enum))
+ lines.append("")
+
+ for union in self.schema.unions:
+ if self.is_imported_type(union):
+ continue
+ lines.extend(self.generate_union(union))
+ lines.append("")
+
+ for message in self.schema.messages:
+ if self.is_imported_type(message):
+ continue
+ lines.extend(self.generate_message(message, parent_stack=[]))
+ lines.append("")
+
+ lines.extend(self.generate_registration_class())
+ lines.append("")
+
+ file_name = self._module_file_name()
+ ns_path = self._namespace_path(namespace_name)
+ if ns_path:
+ path = f"{ns_path}/{file_name}"
+ else:
+ path = file_name
+
+ return GeneratedFile(path=path, content="\n".join(lines))
+
+ def _resolve_named_type(
+ self, name: str, parent_stack: Optional[List[Message]] = None
+ ) -> Optional[TypingUnion[Message, Enum, Union]]:
+ parent_stack = parent_stack or []
+ if "." in name:
+ return self.schema.get_type(name)
+ for msg in reversed(parent_stack):
+ nested = msg.get_nested_type(name)
+ if nested is not None:
+ return nested
+ return self.schema.get_type(name)
+
+ def _type_namespace(
+ self, resolved: Optional[TypingUnion[Message, Enum, Union]]
+ ) -> str:
+ if resolved is None:
+ return self.get_csharp_namespace()
+ if self.is_imported_type(resolved):
+ return self._csharp_namespace_for_type(resolved)
+ return self.get_csharp_namespace()
+
+ def _qualified_type_name_for(
+ self,
+ resolved: Optional[TypingUnion[Message, Enum, Union]],
+ fallback_name: str,
+ ) -> str:
+ if resolved is None:
+ return ".".join(
+ self.safe_type_identifier(part) for part in
fallback_name.split(".")
+ )
+ qualified = self._qualified_type_names.get(id(resolved), fallback_name)
+ return ".".join(
+ self.safe_type_identifier(part) for part in qualified.split(".")
+ )
+
+ def _named_type_reference(
+ self, named_type: NamedType, parent_stack: Optional[List[Message]] =
None
+ ) -> str:
+ resolved = self._resolve_named_type(named_type.name, parent_stack)
+ ns = self._type_namespace(resolved)
+ qname = self._qualified_type_name_for(resolved, named_type.name)
+ if ns:
+ return f"global::{ns}.{qname}"
+ return qname
+
+ def _is_value_type(
+ self, field_type: FieldType, parent_stack: List[Message]
+ ) -> bool:
+ if isinstance(field_type, PrimitiveType):
+ return field_type.kind in self.VALUE_TYPE_KINDS
+ if isinstance(field_type, NamedType):
+ resolved = self._resolve_named_type(field_type.name, parent_stack)
+ return isinstance(resolved, Enum)
+ return False
+
+ def generate_type(
+ self,
+ field_type: FieldType,
+ nullable: bool = False,
+ parent_stack: Optional[List[Message]] = None,
+ ) -> str:
+ parent_stack = parent_stack or []
+ if isinstance(field_type, PrimitiveType):
+ if field_type.kind not in self.PRIMITIVE_MAP:
+ raise ValueError(
+ f"Unsupported primitive type for C#: {field_type.kind}"
+ )
+ type_name = self.PRIMITIVE_MAP[field_type.kind]
+ if nullable and field_type.kind in self.VALUE_TYPE_KINDS:
+ return f"{type_name}?"
+ if nullable and field_type.kind not in self.VALUE_TYPE_KINDS:
+ return f"{type_name}?"
+ return type_name
+
+ if isinstance(field_type, NamedType):
+ type_name = self._named_type_reference(field_type, parent_stack)
+ if nullable and self._is_value_type(field_type, parent_stack):
+ return f"{type_name}?"
+ if nullable and not self._is_value_type(field_type, parent_stack):
+ return f"{type_name}?"
+ return type_name
+
+ if isinstance(field_type, ListType):
+ element_type = self.generate_type(
+ field_type.element_type,
+ nullable=field_type.element_optional,
+ parent_stack=parent_stack,
+ )
+ list_type = f"List<{element_type}>"
+ if nullable:
+ return f"{list_type}?"
+ return list_type
+
+ if isinstance(field_type, MapType):
+ key_type = self.generate_type(
+ field_type.key_type,
+ nullable=False,
+ parent_stack=parent_stack,
+ )
+ value_type = self.generate_type(
+ field_type.value_type,
+ nullable=False,
+ parent_stack=parent_stack,
+ )
+ map_type = f"Dictionary<{key_type}, {value_type}>"
+ if nullable:
+ return f"{map_type}?"
+ return map_type
+
+ raise ValueError(f"Unknown field type: {field_type}")
+
+ def _default_initializer(
+ self, field: Field, parent_stack: List[Message]
+ ) -> Optional[str]:
+ if field.optional:
+ return None
+
+ field_type = field.field_type
+ if isinstance(field_type, ListType) or isinstance(field_type, MapType):
+ return " = new();"
+
+ if isinstance(field_type, PrimitiveType):
+ if field_type.kind == PrimitiveKind.STRING:
+ return " = string.Empty;"
+ if field_type.kind == PrimitiveKind.BYTES:
+ return " = Array.Empty<byte>();"
+ if field_type.kind == PrimitiveKind.ANY:
+ return " = null!;"
+ return None
+
+ if isinstance(field_type, NamedType):
+ resolved = self._resolve_named_type(field_type.name, parent_stack)
+ if isinstance(resolved, Enum):
+ return None
+ return " = null!;"
+
+ return None
+
+ def _field_encoding(self, field: Field) -> Optional[str]:
+ field_type = field.field_type
+ if not isinstance(field_type, PrimitiveType):
+ return None
+ kind = field_type.kind
+ if kind in {
+ PrimitiveKind.INT32,
+ PrimitiveKind.INT64,
+ PrimitiveKind.UINT32,
+ PrimitiveKind.UINT64,
+ }:
+ return "Fixed"
+ if kind in {PrimitiveKind.TAGGED_INT64, PrimitiveKind.TAGGED_UINT64}:
+ return "Tagged"
+ return None
+
+ def _type_reference_for_local(
+ self,
+ type_def: TypingUnion[Message, Enum, Union],
+ ) -> str:
+ namespace_name = self.get_csharp_namespace()
+ type_name = self._qualified_type_name_for(
+ type_def, getattr(type_def, "name", "Unknown")
+ )
+ return f"global::{namespace_name}.{type_name}"
+
+ def generate_enum(self, enum: Enum, indent: int = 0) -> List[str]:
+ lines: List[str] = []
+ ind = self.indent_str * indent
+ comment = self.format_type_id_comment(enum, f"{ind}//")
+ if comment:
+ lines.append(comment)
+ lines.append(f"{ind}[ForyObject]")
+ lines.append(f"{ind}public enum
{self.safe_type_identifier(enum.name)}")
+ lines.append(f"{ind}{{")
+
+ for i, value in enumerate(enum.values):
+ comma = "," if i < len(enum.values) - 1 else ""
+ stripped_name = self.strip_enum_prefix(enum.name, value.name)
+ value_name =
self.safe_identifier(self.to_pascal_case(stripped_name))
+ lines.append(f"{ind}{self.indent_str}{value_name} =
{value.value}{comma}")
+
+ lines.append(f"{ind}}}")
+ return lines
+
+ def generate_union(
+ self,
+ union: Union,
+ indent: int = 0,
+ parent_stack: Optional[List[Message]] = None,
+ ) -> List[str]:
+ 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)
+
+ comment = self.format_type_id_comment(union, f"{ind}//")
+ if comment:
+ lines.append(comment)
+ lines.append(f"{ind}public sealed class {type_name} : Union")
+ 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}{{")
+ lines.append(f"{ind}{self.indent_str}}}")
+ lines.append("")
+
+ lines.append(
+ f"{ind}{self.indent_str}public static {type_name} Of(int index,
object? value)"
+ )
+ 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_type = self.generate_type(
+ field.field_type,
+ nullable=False,
+ parent_stack=parent_stack,
+ )
+ 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}");'
+ )
+ 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(
+ f"{ind}{self.indent_str * 2}return
{registration_class}.GetFory().Serialize(this);"
+ )
+ lines.append(f"{ind}{self.indent_str}}}")
+ lines.append("")
+ lines.append(
+ f"{ind}{self.indent_str}public static {type_name} FromBytes(byte[]
data)"
+ )
+ lines.append(f"{ind}{self.indent_str}{{")
+ lines.append(
+ f"{ind}{self.indent_str * 2}return
{registration_class}.GetFory().Deserialize<{full_type_ref}>(data);"
+ )
+ lines.append(f"{ind}{self.indent_str}}}")
+
+ lines.append(f"{ind}}}")
+ return lines
+
+ def generate_message(
+ self,
+ message: Message,
+ indent: int = 0,
+ parent_stack: Optional[List[Message]] = None,
+ ) -> List[str]:
+ lines: List[str] = []
+ ind = self.indent_str * indent
+ parent_stack = parent_stack or []
+ lineage = parent_stack + [message]
+ registration_class = self.get_registration_class_name()
+ type_name = self.safe_type_identifier(message.name)
+ full_type_ref = self._type_reference_for_local(message)
+
+ comment = self.format_type_id_comment(message, f"{ind}//")
+ if comment:
+ lines.append(comment)
+ lines.append(f"{ind}[ForyObject]")
+ lines.append(f"{ind}public sealed partial class {type_name}")
+ lines.append(f"{ind}{{")
+
+ for nested_enum in message.nested_enums:
+ lines.append("")
+ lines.extend(self.generate_enum(nested_enum, indent + 1))
+
+ for nested_union in message.nested_unions:
+ lines.append("")
+ lines.extend(self.generate_union(nested_union, indent + 1,
lineage))
+
+ for nested_msg in message.nested_messages:
+ lines.append("")
+ lines.extend(self.generate_message(nested_msg, indent + 1,
lineage))
+
+ used_field_names: Set[str] = set()
+ for field in message.fields:
+ lines.append("")
+ encoding = self._field_encoding(field)
+ if encoding:
+ lines.append(
+ f"{ind}{self.indent_str}[Field(Encoding =
FieldEncoding.{encoding})]"
+ )
+ field_name = self._field_member_name(field, message,
used_field_names)
+ field_type = self.generate_type(
+ field.field_type,
+ nullable=field.optional,
+ parent_stack=lineage,
+ )
+ init = self._default_initializer(field, lineage) or ""
+ lines.append(
+ f"{ind}{self.indent_str}public {field_type} {field_name} {{
get; set; }}{init}"
+ )
+
+ lines.append("")
+ lines.append(f"{ind}{self.indent_str}public byte[] ToBytes()")
+ lines.append(f"{ind}{self.indent_str}{{")
+ lines.append(
+ f"{ind}{self.indent_str * 2}return
{registration_class}.GetFory().Serialize(this);"
+ )
+ lines.append(f"{ind}{self.indent_str}}}")
+ lines.append("")
+ lines.append(
+ f"{ind}{self.indent_str}public static {type_name} FromBytes(byte[]
data)"
+ )
+ lines.append(f"{ind}{self.indent_str}{{")
+ lines.append(
+ f"{ind}{self.indent_str * 2}return
{registration_class}.GetFory().Deserialize<{full_type_ref}>(data);"
+ )
+ lines.append(f"{ind}{self.indent_str}}}")
+
+ lines.append(f"{ind}}}")
+ return lines
+
+ def _register_type_lines(
+ self,
+ type_def: TypingUnion[Message, Enum, Union],
+ target_var: str,
+ ) -> List[str]:
+ type_ref = self._type_reference_for_local(type_def)
+ type_name = self._qualified_type_names.get(id(type_def), type_def.name)
+ if self.should_register_by_id(type_def):
+ return
[f"{target_var}.Register<{type_ref}>((uint){type_def.type_id});"]
+
+ namespace_name = self.schema.package or "default"
+ return [
+ f'{target_var}.Register<{type_ref}>("{namespace_name}",
"{type_name}");'
+ ]
+
+ def _collect_local_types(self) -> List[TypingUnion[Message, Enum, Union]]:
+ local_types: List[TypingUnion[Message, Enum, Union]] = []
+
+ for enum in self.schema.enums:
+ if not self.is_imported_type(enum):
+ local_types.append(enum)
+ for union in self.schema.unions:
+ if not self.is_imported_type(union):
+ local_types.append(union)
+
+ def visit_message(message: Message) -> None:
+ local_types.append(message)
+ for nested_enum in message.nested_enums:
+ local_types.append(nested_enum)
+ for nested_union in message.nested_unions:
+ local_types.append(nested_union)
+ for nested_msg in message.nested_messages:
+ visit_message(nested_msg)
+
+ for message in self.schema.messages:
+ if self.is_imported_type(message):
+ continue
+ visit_message(message)
+
+ return local_types
+
+ def generate_registration_class(self) -> List[str]:
+ lines: List[str] = []
+ class_name =
self.safe_type_identifier(self.get_registration_class_name())
+ imported_regs = self._collect_imported_registrations()
+ local_types = self._collect_local_types()
+
+ lines.append(f"public static class {class_name}")
+ lines.append("{")
+ lines.append(
+ f"{self.indent_str}private static readonly Lazy<ThreadSafeFory>
LazyFory = new(CreateFory);"
+ )
+ lines.append("")
+ lines.append(f"{self.indent_str}internal static ThreadSafeFory
GetFory()")
+ lines.append(f"{self.indent_str}{{")
+ lines.append(f"{self.indent_str * 2}return LazyFory.Value;")
+ lines.append(f"{self.indent_str}}}")
+ lines.append("")
+
+ lines.append(f"{self.indent_str}private static ThreadSafeFory
CreateFory()")
+ lines.append(f"{self.indent_str}{{")
+ lines.append(
+ f"{self.indent_str * 2}ThreadSafeFory fory =
Fory.Builder().Xlang(true).TrackRef(true).BuildThreadSafe();"
+ )
+ lines.append(f"{self.indent_str * 2}Register(fory);")
+ lines.append(f"{self.indent_str * 2}return fory;")
+ lines.append(f"{self.indent_str}}}")
+ lines.append("")
+
+ lines.append(f"{self.indent_str}public static void Register(Fory
fory)")
+ lines.append(f"{self.indent_str}{{")
+ for namespace_name, reg_name in imported_regs:
+ if namespace_name == self.get_csharp_namespace() and reg_name ==
class_name:
+ continue
+ lines.append(
+ f"{self.indent_str *
2}global::{namespace_name}.{self.safe_type_identifier(reg_name)}.Register(fory);"
+ )
+ for type_def in local_types:
+ for register_line in self._register_type_lines(type_def, "fory"):
+ lines.append(f"{self.indent_str * 2}{register_line}")
+ lines.append(f"{self.indent_str}}}")
+ lines.append("")
+
+ lines.append(
+ f"{self.indent_str}public static void Register(ThreadSafeFory
fory)"
+ )
+ lines.append(f"{self.indent_str}{{")
+ for namespace_name, reg_name in imported_regs:
+ if namespace_name == self.get_csharp_namespace() and reg_name ==
class_name:
+ continue
+ lines.append(
+ f"{self.indent_str *
2}global::{namespace_name}.{self.safe_type_identifier(reg_name)}.Register(fory);"
+ )
+ for type_def in local_types:
+ for register_line in self._register_type_lines(type_def, "fory"):
+ lines.append(f"{self.indent_str * 2}{register_line}")
+ lines.append(f"{self.indent_str}}}")
+
+ lines.append("}")
+ return lines
diff --git a/compiler/fory_compiler/tests/test_csharp_generator.py
b/compiler/fory_compiler/tests/test_csharp_generator.py
new file mode 100644
index 000000000..a0181eeff
--- /dev/null
+++ b/compiler/fory_compiler/tests/test_csharp_generator.py
@@ -0,0 +1,139 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for C# generator behavior."""
+
+import warnings
+from pathlib import Path
+
+from fory_compiler.cli import resolve_imports
+from fory_compiler.frontend.fdl.lexer import Lexer
+from fory_compiler.frontend.fdl.parser import Parser
+from fory_compiler.generators.base import GeneratorOptions
+from fory_compiler.generators.csharp import CSharpGenerator
+
+
+def parse_schema(source: str):
+ return Parser(Lexer(source).tokenize()).parse()
+
+
+def generate(source: str):
+ schema = parse_schema(source)
+ generator = CSharpGenerator(schema,
GeneratorOptions(output_dir=Path("/tmp")))
+ return generator.generate()[0]
+
+
+def test_csharp_namespace_option_used():
+ file = generate(
+ """
+ package payment;
+ option csharp_namespace = "MyCorp.Payment.V1";
+
+ message Payment {
+ string id = 1;
+ }
+ """
+ )
+
+ assert file.path == "MyCorp/Payment/V1/payment.cs"
+ assert "namespace MyCorp.Payment.V1;" in file.content
+ assert "public sealed partial class Payment" in file.content
+
+
+def test_csharp_namespace_fallback_to_package():
+ file = generate(
+ """
+ package com.example.models;
+
+ message User {
+ string name = 1;
+ }
+ """
+ )
+
+ assert file.path == "com/example/models/com_example_models.cs"
+ assert "namespace com.example.models;" in file.content
+
+
+def test_csharp_registration_uses_fdl_package_for_name_registration():
+ file = generate(
+ """
+ package myapp.models;
+ option csharp_namespace = "MyCorp.Generated.Models";
+ option enable_auto_type_id = false;
+
+ message User {
+ string name = 1;
+ }
+ """
+ )
+
+ assert (
+ 'fory.Register<global::MyCorp.Generated.Models.User>("myapp.models",
"User");'
+ in file.content
+ )
+
+
+def test_csharp_field_encoding_attributes():
+ file = generate(
+ """
+ package example;
+
+ message Encoded {
+ fixed_int32 fixed_id = 1;
+ tagged_uint64 tagged = 2;
+ int32 plain = 3;
+ }
+ """
+ )
+
+ assert "[Field(Encoding = FieldEncoding.Fixed)]" in file.content
+ assert "[Field(Encoding = FieldEncoding.Tagged)]" in file.content
+ assert "public int Plain { get; set; }" in file.content
+
+
+def test_csharp_imported_registration_calls_generated():
+ repo_root = Path(__file__).resolve().parents[3]
+ idl_dir = repo_root / "integration_tests" / "idl_tests" / "idl"
+ schema = resolve_imports(idl_dir / "root.idl", [idl_dir])
+
+ generator = CSharpGenerator(schema,
GeneratorOptions(output_dir=Path("/tmp")))
+ file = generator.generate()[0]
+
+ assert (
+ "global::addressbook.AddressbookForyRegistration.Register(fory);"
+ in file.content
+ )
+ assert "global::tree.TreeForyRegistration.Register(fory);" in file.content
+
+
+def test_csharp_namespace_option_is_known():
+ source = """
+ package myapp;
+ option csharp_namespace = "MyCorp.MyApp";
+
+ message User {
+ string name = 1;
+ }
+ """
+
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter("always")
+ schema = parse_schema(source)
+
+ assert schema.get_option("csharp_namespace") == "MyCorp.MyApp"
+ assert not caught
diff --git a/compiler/fory_compiler/tests/test_generated_code.py
b/compiler/fory_compiler/tests/test_generated_code.py
index 13dc99eab..ef7cf4c66 100644
--- a/compiler/fory_compiler/tests/test_generated_code.py
+++ b/compiler/fory_compiler/tests/test_generated_code.py
@@ -31,6 +31,7 @@ from fory_compiler.generators.go import GoGenerator
from fory_compiler.generators.java import JavaGenerator
from fory_compiler.generators.python import PythonGenerator
from fory_compiler.generators.rust import RustGenerator
+from fory_compiler.generators.csharp import CSharpGenerator
from fory_compiler.ir.ast import Schema
@@ -40,6 +41,7 @@ GENERATOR_CLASSES: Tuple[Type[BaseGenerator], ...] = (
CppGenerator,
RustGenerator,
GoGenerator,
+ CSharpGenerator,
)
diff --git a/csharp/README.md b/csharp/README.md
index 4400e81a2..12bcdecef 100644
--- a/csharp/README.md
+++ b/csharp/README.md
@@ -1,4 +1,4 @@
-# Apache Fory™ C# #
+# Apache Fory™ C\#
[](https://github.com/apache/fory/blob/main/LICENSE)
@@ -6,7 +6,7 @@ Apache Fory™ is a blazing fast multi-language serialization
framework powered
The C# implementation provides high-performance object graph serialization for
.NET with source-generated serializers, optional reference tracking, schema
evolution support, and cross-language compatibility.
-## Why Apache Fory™ C#? ##
+## Why Apache Fory™ C\#?
- High-performance binary serialization for .NET 8+
- Cross-language compatibility with Java, Python, C++, Go, Rust, and JavaScript
@@ -16,14 +16,14 @@ The C# implementation provides high-performance object
graph serialization for .
- Thread-safe runtime wrapper (`ThreadSafeFory`) for concurrent workloads
- Dynamic object serialization APIs for heterogeneous payloads
-## Quick Start ##
+## Quick Start
-### Requirements ###
+### Requirements
- .NET SDK 8.0+
- C# 12+
-### Add Apache Fory™ C# ###
+### Add Apache Fory™ C\#
From this repository, reference the library project:
@@ -37,7 +37,7 @@ From this repository, reference the library project:
</ItemGroup>
```
-### Basic Example ###
+### Basic Example
```csharp
using Apache.Fory;
@@ -64,9 +64,9 @@ byte[] payload = fory.Serialize(user);
User decoded = fory.Deserialize<User>(payload);
```
-## Core Features ##
+## Core Features
-### 1. Object Graph Serialization ###
+### 1. Object Graph Serialization
`[ForyObject]` types are serialized with generated serializers.
@@ -91,7 +91,7 @@ fory.Register<Address>(100);
fory.Register<Person>(101);
```
-### 2. Shared and Circular References ###
+### 2. Shared and Circular References
Enable reference tracking to preserve object identity.
@@ -113,7 +113,7 @@ Node decoded = fory.Deserialize<Node>(fory.Serialize(node));
System.Diagnostics.Debug.Assert(object.ReferenceEquals(decoded, decoded.Next));
```
-### 3. Schema Evolution ###
+### 3. Schema Evolution
Compatible mode allows schema changes between writer and reader.
@@ -140,7 +140,7 @@ fory2.Register<TwoFields>(300);
TwoFields decoded = fory2.Deserialize<TwoFields>(fory1.Serialize(new OneField
{ F1 = "hello" }));
```
-### 4. Dynamic Object Serialization ###
+### 4. Dynamic Object Serialization
Use dynamic APIs for unknown/heterogeneous payloads.
@@ -158,7 +158,7 @@ byte[] payload = fory.SerializeObject(map);
object? decoded = fory.DeserializeObject(payload);
```
-### 5. Thread-Safe Runtime ###
+### 5. Thread-Safe Runtime
`Fory` is single-thread optimized. Use `ThreadSafeFory` for concurrent access.
@@ -173,7 +173,7 @@ Parallel.For(0, 128, i =>
});
```
-### 6. Custom Serializers ###
+### 6. Custom Serializers
Provide custom encoding logic with `Serializer<T>`.
@@ -202,7 +202,7 @@ Fory fory = Fory.Builder().Build();
fory.Register<Point, PointSerializer>(400);
```
-## Cross-Language Serialization ##
+## Cross-Language Serialization
Use consistent registration mappings across languages.
@@ -217,7 +217,7 @@ fory.Register<Person>(100); // same ID on other language
peers
See [xlang guide](../docs/guide/xlang/index.md) for mapping details.
-## Architecture ##
+## Architecture
The C# implementation consists of:
@@ -240,7 +240,7 @@ csharp/
└── Fory.XlangPeer/
```
-## Building and Testing ##
+## Building and Testing
Run from the `csharp` directory:
@@ -252,7 +252,7 @@ dotnet build Fory.sln -c Release
dotnet test Fory.sln -c Release
```
-## Documentation ##
+## Documentation
- [C# guide index](../docs/guide/csharp/index.md)
- [Cross-language serialization
spec](../docs/specification/xlang_serialization_spec.md)
diff --git a/csharp/src/Fory/CollectionSerializers.cs
b/csharp/src/Fory/CollectionSerializers.cs
index 5b1a04068..db05e99a6 100644
--- a/csharp/src/Fory/CollectionSerializers.cs
+++ b/csharp/src/Fory/CollectionSerializers.cs
@@ -78,6 +78,11 @@ internal static class CollectionCodec
bool trackRef = context.TrackRef &&
elementTypeInfo.IsReferenceTrackableType;
bool declaredElementType = hasGenerics &&
CanDeclareElementType<T>(elementTypeInfo);
bool dynamicElementType = elementTypeInfo.IsDynamicType;
+ bool writeDeclaredCompatibleTypeInfo =
+ context.Compatible &&
+ declaredElementType &&
+ !dynamicElementType &&
+ elementTypeInfo.NeedsTypeInfoForField();
byte header = dynamicElementType ? (byte)0 : CollectionBits.SameType;
if (trackRef)
@@ -96,7 +101,7 @@ internal static class CollectionCodec
}
context.Writer.WriteUInt8(header);
- if (!dynamicElementType && !declaredElementType)
+ if (!dynamicElementType && (!declaredElementType ||
writeDeclaredCompatibleTypeInfo))
{
context.TypeResolver.WriteTypeInfo(elementSerializer, context);
}
@@ -161,6 +166,11 @@ internal static class CollectionCodec
bool declared = (header & CollectionBits.DeclaredElementType) != 0;
bool sameType = (header & CollectionBits.SameType) != 0;
bool canonicalizeElements = context.TrackRef && !trackRef &&
elementTypeInfo.IsReferenceTrackableType;
+ bool readDeclaredCompatibleTypeInfo =
+ context.Compatible &&
+ declared &&
+ !elementTypeInfo.IsDynamicType &&
+ elementTypeInfo.NeedsTypeInfoForField();
List<T> values = new(length);
if (!sameType)
@@ -205,7 +215,7 @@ internal static class CollectionCodec
return values;
}
- if (!declared)
+ if (!declared || readDeclaredCompatibleTypeInfo)
{
context.TypeResolver.ReadTypeInfo(elementSerializer, context);
}
diff --git a/csharp/src/Fory/DictionarySerializers.cs
b/csharp/src/Fory/DictionarySerializers.cs
index 289435549..8f7605853 100644
--- a/csharp/src/Fory/DictionarySerializers.cs
+++ b/csharp/src/Fory/DictionarySerializers.cs
@@ -66,6 +66,16 @@ public abstract class DictionaryLikeSerializer<TDictionary,
TKey, TValue> : Seri
bool valueDeclared = hasGenerics &&
!valueTypeInfo.NeedsTypeInfoForField();
bool keyDynamicType = keyTypeInfo.IsDynamicType;
bool valueDynamicType = valueTypeInfo.IsDynamicType;
+ bool writeDeclaredCompatibleKeyTypeInfo =
+ context.Compatible &&
+ keyDeclared &&
+ !keyDynamicType &&
+ keyTypeInfo.NeedsTypeInfoForField();
+ bool writeDeclaredCompatibleValueTypeInfo =
+ context.Compatible &&
+ valueDeclared &&
+ !valueDynamicType &&
+ valueTypeInfo.NeedsTypeInfoForField();
KeyValuePair<TKey, TValue>[] pairs = SnapshotPairs(map);
if (keyDynamicType || valueDynamicType)
@@ -80,6 +90,8 @@ public abstract class DictionaryLikeSerializer<TDictionary,
TKey, TValue> : Seri
valueDeclared,
keyDynamicType,
valueDynamicType,
+ writeDeclaredCompatibleKeyTypeInfo,
+ writeDeclaredCompatibleValueTypeInfo,
keyTypeInfo,
valueTypeInfo,
keySerializer,
@@ -129,7 +141,7 @@ public abstract class DictionaryLikeSerializer<TDictionary,
TKey, TValue> : Seri
context.Writer.WriteUInt8(header);
if (!keyIsNull)
{
- if (!keyDeclared)
+ if (!keyDeclared || writeDeclaredCompatibleKeyTypeInfo)
{
context.TypeResolver.WriteTypeInfo(keySerializer,
context);
}
@@ -139,7 +151,7 @@ public abstract class DictionaryLikeSerializer<TDictionary,
TKey, TValue> : Seri
if (!valueIsNull)
{
- if (!valueDeclared)
+ if (!valueDeclared || writeDeclaredCompatibleValueTypeInfo)
{
context.TypeResolver.WriteTypeInfo(valueSerializer,
context);
}
@@ -175,12 +187,12 @@ public abstract class
DictionaryLikeSerializer<TDictionary, TKey, TValue> : Seri
context.Writer.WriteUInt8(blockHeader);
int chunkSizeOffset = context.Writer.Count;
context.Writer.WriteUInt8(0);
- if (!keyDeclared)
+ if (!keyDeclared || writeDeclaredCompatibleKeyTypeInfo)
{
context.TypeResolver.WriteTypeInfo(keySerializer, context);
}
- if (!valueDeclared)
+ if (!valueDeclared || writeDeclaredCompatibleValueTypeInfo)
{
context.TypeResolver.WriteTypeInfo(valueSerializer, context);
}
@@ -232,6 +244,16 @@ public abstract class
DictionaryLikeSerializer<TDictionary, TKey, TValue> : Seri
bool trackValueRef = (header & DictionaryBits.TrackingValueRef) !=
0;
bool valueNull = (header & DictionaryBits.ValueNull) != 0;
bool valueDeclared = (header & DictionaryBits.DeclaredValueType)
!= 0;
+ bool readDeclaredCompatibleKeyTypeInfo =
+ context.Compatible &&
+ keyDeclared &&
+ !keyDynamicType &&
+ keyTypeInfo.NeedsTypeInfoForField();
+ bool readDeclaredCompatibleValueTypeInfo =
+ context.Compatible &&
+ valueDeclared &&
+ !valueDynamicType &&
+ valueTypeInfo.NeedsTypeInfoForField();
if (keyNull && valueNull)
{
@@ -246,7 +268,7 @@ public abstract class DictionaryLikeSerializer<TDictionary,
TKey, TValue> : Seri
_ = ReadValueElement(
context,
trackValueRef,
- !valueDeclared,
+ !valueDeclared || readDeclaredCompatibleValueTypeInfo,
canonicalizeValues,
valueSerializer);
@@ -260,7 +282,7 @@ public abstract class DictionaryLikeSerializer<TDictionary,
TKey, TValue> : Seri
TKey key = keySerializer.Read(
context,
trackKeyRef ? RefMode.Tracking : RefMode.None,
- !keyDeclared);
+ !keyDeclared || readDeclaredCompatibleKeyTypeInfo);
SetValue(map, key, (TValue)valueSerializer.DefaultObject!);
readCount += 1;
@@ -275,7 +297,7 @@ public abstract class DictionaryLikeSerializer<TDictionary,
TKey, TValue> : Seri
DynamicTypeInfo? keyDynamicInfo = null;
DynamicTypeInfo? valueDynamicInfo = null;
- if (!keyDeclared)
+ if (!keyDeclared || readDeclaredCompatibleKeyTypeInfo)
{
if (keyDynamicType)
{
@@ -287,7 +309,7 @@ public abstract class DictionaryLikeSerializer<TDictionary,
TKey, TValue> : Seri
}
}
- if (!valueDeclared)
+ if (!valueDeclared || readDeclaredCompatibleValueTypeInfo)
{
if (valueDynamicType)
{
@@ -333,12 +355,12 @@ public abstract class
DictionaryLikeSerializer<TDictionary, TKey, TValue> : Seri
continue;
}
- if (!keyDeclared)
+ if (!keyDeclared || readDeclaredCompatibleKeyTypeInfo)
{
context.TypeResolver.ReadTypeInfo(keySerializer, context);
}
- if (!valueDeclared)
+ if (!valueDeclared || readDeclaredCompatibleValueTypeInfo)
{
context.TypeResolver.ReadTypeInfo(valueSerializer, context);
}
@@ -376,6 +398,8 @@ public abstract class DictionaryLikeSerializer<TDictionary,
TKey, TValue> : Seri
bool valueDeclared,
bool keyDynamicType,
bool valueDynamicType,
+ bool writeDeclaredCompatibleKeyTypeInfo,
+ bool writeDeclaredCompatibleValueTypeInfo,
TypeInfo keyTypeInfo,
TypeInfo valueTypeInfo,
Serializer<TKey> keySerializer,
@@ -443,7 +467,7 @@ public abstract class DictionaryLikeSerializer<TDictionary,
TKey, TValue> : Seri
}
context.Writer.WriteUInt8(1);
- if (!keyDeclared)
+ if (!keyDeclared || writeDeclaredCompatibleKeyTypeInfo)
{
if (keyDynamicType)
{
@@ -455,7 +479,7 @@ public abstract class DictionaryLikeSerializer<TDictionary,
TKey, TValue> : Seri
}
}
- if (!valueDeclared)
+ if (!valueDeclared || writeDeclaredCompatibleValueTypeInfo)
{
if (valueDynamicType)
{
diff --git a/csharp/src/Fory/NullableKeyDictionary.cs
b/csharp/src/Fory/NullableKeyDictionary.cs
index 544500da8..1f9de8843 100644
--- a/csharp/src/Fory/NullableKeyDictionary.cs
+++ b/csharp/src/Fory/NullableKeyDictionary.cs
@@ -411,6 +411,16 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
bool valueDeclared = hasGenerics &&
!valueTypeInfo.NeedsTypeInfoForField();
bool keyDynamicType = keyTypeInfo.IsDynamicType;
bool valueDynamicType = valueTypeInfo.IsDynamicType;
+ bool writeDeclaredCompatibleKeyTypeInfo =
+ context.Compatible &&
+ keyDeclared &&
+ !keyDynamicType &&
+ keyTypeInfo.NeedsTypeInfoForField();
+ bool writeDeclaredCompatibleValueTypeInfo =
+ context.Compatible &&
+ valueDeclared &&
+ !valueDynamicType &&
+ valueTypeInfo.NeedsTypeInfoForField();
KeyValuePair<TKey, TValue>[] pairs = [.. map];
if (keyDynamicType || valueDynamicType)
{
@@ -424,6 +434,8 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
valueDeclared,
keyDynamicType,
valueDynamicType,
+ writeDeclaredCompatibleKeyTypeInfo,
+ writeDeclaredCompatibleValueTypeInfo,
keyTypeInfo,
valueTypeInfo,
keySerializer,
@@ -472,7 +484,7 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
if (keyIsNull)
{
- if (!valueDeclared)
+ if (!valueDeclared || writeDeclaredCompatibleValueTypeInfo)
{
context.TypeResolver.WriteTypeInfo(valueSerializer,
context);
}
@@ -488,7 +500,7 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
if (valueIsNull)
{
- if (!keyDeclared)
+ if (!keyDeclared || writeDeclaredCompatibleKeyTypeInfo)
{
context.TypeResolver.WriteTypeInfo(keySerializer, context);
}
@@ -503,12 +515,12 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
}
context.Writer.WriteUInt8(1);
- if (!keyDeclared)
+ if (!keyDeclared || writeDeclaredCompatibleKeyTypeInfo)
{
context.TypeResolver.WriteTypeInfo(keySerializer, context);
}
- if (!valueDeclared)
+ if (!valueDeclared || writeDeclaredCompatibleValueTypeInfo)
{
context.TypeResolver.WriteTypeInfo(valueSerializer, context);
}
@@ -555,6 +567,16 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
bool trackValueRef = (header & DictionaryBits.TrackingValueRef) !=
0;
bool valueNull = (header & DictionaryBits.ValueNull) != 0;
bool valueDeclared = (header & DictionaryBits.DeclaredValueType)
!= 0;
+ bool readDeclaredCompatibleKeyTypeInfo =
+ context.Compatible &&
+ keyDeclared &&
+ !keyDynamicType &&
+ keyTypeInfo.NeedsTypeInfoForField();
+ bool readDeclaredCompatibleValueTypeInfo =
+ context.Compatible &&
+ valueDeclared &&
+ !valueDynamicType &&
+ valueTypeInfo.NeedsTypeInfoForField();
if (keyNull && valueNull)
{
@@ -568,7 +590,7 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
TValue valueRead = ReadValueElement(
context,
trackValueRef,
- !valueDeclared,
+ !valueDeclared || readDeclaredCompatibleValueTypeInfo,
canonicalizeValues,
valueSerializer);
@@ -582,7 +604,7 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
TKey key = keySerializer.Read(
context,
trackKeyRef ? RefMode.Tracking : RefMode.None,
- !keyDeclared);
+ !keyDeclared || readDeclaredCompatibleKeyTypeInfo);
map[key] = (TValue)valueSerializer.DefaultObject!;
readCount += 1;
@@ -597,7 +619,7 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
DynamicTypeInfo? keyDynamicInfo = null;
DynamicTypeInfo? valueDynamicInfo = null;
- if (!keyDeclared)
+ if (!keyDeclared || readDeclaredCompatibleKeyTypeInfo)
{
if (keyDynamicType)
{
@@ -609,7 +631,7 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
}
}
- if (!valueDeclared)
+ if (!valueDeclared || readDeclaredCompatibleValueTypeInfo)
{
if (valueDynamicType)
{
@@ -655,12 +677,12 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
continue;
}
- if (!keyDeclared)
+ if (!keyDeclared || readDeclaredCompatibleKeyTypeInfo)
{
context.TypeResolver.ReadTypeInfo(keySerializer, context);
}
- if (!valueDeclared)
+ if (!valueDeclared || readDeclaredCompatibleValueTypeInfo)
{
context.TypeResolver.ReadTypeInfo(valueSerializer, context);
}
@@ -698,6 +720,8 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
bool valueDeclared,
bool keyDynamicType,
bool valueDynamicType,
+ bool writeDeclaredCompatibleKeyTypeInfo,
+ bool writeDeclaredCompatibleValueTypeInfo,
TypeInfo keyTypeInfo,
TypeInfo valueTypeInfo,
Serializer<TKey> keySerializer,
@@ -765,7 +789,7 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
}
context.Writer.WriteUInt8(1);
- if (!keyDeclared)
+ if (!keyDeclared || writeDeclaredCompatibleKeyTypeInfo)
{
if (keyDynamicType)
{
@@ -777,7 +801,7 @@ public sealed class NullableKeyDictionarySerializer<TKey,
TValue> : Serializer<N
}
}
- if (!valueDeclared)
+ if (!valueDeclared || writeDeclaredCompatibleValueTypeInfo)
{
if (valueDynamicType)
{
diff --git a/csharp/src/Fory/UnionSerializer.cs
b/csharp/src/Fory/UnionSerializer.cs
index 90d4a1970..b3cf25e5d 100644
--- a/csharp/src/Fory/UnionSerializer.cs
+++ b/csharp/src/Fory/UnionSerializer.cs
@@ -15,6 +15,7 @@
// specific language governing permissions and limitations
// under the License.
+using System.Collections;
using System.Linq.Expressions;
using System.Reflection;
@@ -24,9 +25,7 @@ public sealed class UnionSerializer<TUnion> :
Serializer<TUnion>
where TUnion : Union
{
private static readonly Func<int, object?, TUnion> Factory =
BuildFactory();
-
-
-
+ private static readonly IReadOnlyDictionary<int, Type> CaseTypeByIndex =
BuildCaseTypeMap();
public override TUnion DefaultValue => null!;
@@ -39,6 +38,12 @@ public sealed class UnionSerializer<TUnion> :
Serializer<TUnion>
}
context.Writer.WriteVarUInt32((uint)value.Index);
+ if (CaseTypeByIndex.TryGetValue(value.Index, out Type? caseType))
+ {
+ WriteTypedCaseValue(context, caseType, value.Value);
+ return;
+ }
+
DynamicAnyCodec.WriteAny(context, value.Value, RefMode.Tracking, true,
false);
}
@@ -50,8 +55,18 @@ public sealed class UnionSerializer<TUnion> :
Serializer<TUnion>
throw new InvalidDataException($"union case id out of range:
{rawCaseId}");
}
- object? caseValue = DynamicAnyCodec.ReadAny(context, RefMode.Tracking,
true);
- return Factory((int)rawCaseId, caseValue);
+ int caseId = (int)rawCaseId;
+ object? caseValue;
+ if (CaseTypeByIndex.TryGetValue(caseId, out Type? caseType))
+ {
+ caseValue = ReadTypedCaseValue(context, caseType);
+ }
+ else
+ {
+ caseValue = DynamicAnyCodec.ReadAny(context, RefMode.Tracking,
true);
+ }
+
+ return Factory(caseId, caseValue);
}
private static Func<int, object?, TUnion> BuildFactory()
@@ -88,4 +103,170 @@ public sealed class UnionSerializer<TUnion> :
Serializer<TUnion>
throw new InvalidDataException(
$"union type {typeof(TUnion)} must define (int, object)
constructor or static Of(int, object)");
}
+
+ private static IReadOnlyDictionary<int, Type> BuildCaseTypeMap()
+ {
+ if (typeof(TUnion) == typeof(Union))
+ {
+ return new Dictionary<int, Type>();
+ }
+
+ Dictionary<int, Type> caseTypes = new();
+ MethodInfo[] methods = typeof(TUnion).GetMethods(BindingFlags.Public |
BindingFlags.Static);
+ foreach (MethodInfo method in methods)
+ {
+ if (!typeof(TUnion).IsAssignableFrom(method.ReturnType))
+ {
+ continue;
+ }
+
+ ParameterInfo[] parameters = method.GetParameters();
+ if (parameters.Length != 1)
+ {
+ continue;
+ }
+
+ Type caseType = parameters[0].ParameterType;
+ if (!TryResolveCaseIndex(method, caseType, out int caseIndex))
+ {
+ continue;
+ }
+
+ caseTypes.TryAdd(caseIndex, caseType);
+ }
+
+ return caseTypes;
+ }
+
+ private static bool TryResolveCaseIndex(MethodInfo method, Type caseType,
out int caseIndex)
+ {
+ caseIndex = default;
+ object? probeArg = CreateProbeArgument(caseType);
+ try
+ {
+ object? result = method.Invoke(null, [probeArg]);
+ if (result is not Union union)
+ {
+ return false;
+ }
+
+ caseIndex = union.Index;
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static object? CreateProbeArgument(Type caseType)
+ {
+ if (!caseType.IsValueType)
+ {
+ return null;
+ }
+
+ return Activator.CreateInstance(caseType);
+ }
+
+ private static void WriteTypedCaseValue(WriteContext context, Type
caseType, object? value)
+ {
+ object? normalized = NormalizeCaseValue(value, caseType);
+ DynamicAnyCodec.WriteAny(context, normalized, RefMode.Tracking,
writeTypeInfo: true, hasGenerics: caseType.IsGenericType);
+ }
+
+ private static object? ReadTypedCaseValue(ReadContext context, Type
caseType)
+ {
+ object? value = DynamicAnyCodec.ReadAny(context, RefMode.Tracking,
readTypeInfo: true);
+ return NormalizeCaseValue(value, caseType);
+ }
+
+ private static object? NormalizeCaseValue(object? value, Type targetType)
+ {
+ if (value is null || targetType.IsInstanceOfType(value))
+ {
+ return value;
+ }
+
+ if (TryConvertListValue(value, targetType, out object? converted))
+ {
+ return converted;
+ }
+
+ return value;
+ }
+
+ private static bool TryConvertListValue(object value, Type targetType, out
object? converted)
+ {
+ converted = null;
+ if (!TryGetListElementType(targetType, out Type? elementType))
+ {
+ return false;
+ }
+
+ if (value is not IEnumerable source)
+ {
+ return false;
+ }
+
+ IList typedList =
(IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType!))!;
+ foreach (object? item in source)
+ {
+ typedList.Add(ConvertListElement(item, elementType!));
+ }
+
+ converted = typedList;
+ return true;
+ }
+
+ private static bool TryGetListElementType(Type targetType, out Type?
elementType)
+ {
+ if (targetType.IsArray)
+ {
+ elementType = targetType.GetElementType();
+ return elementType is not null;
+ }
+
+ if (targetType.IsGenericType && targetType.GetGenericTypeDefinition()
== typeof(List<>))
+ {
+ elementType = targetType.GetGenericArguments()[0];
+ return true;
+ }
+
+ foreach (Type iface in targetType.GetInterfaces())
+ {
+ if (!iface.IsGenericType)
+ {
+ continue;
+ }
+
+ Type genericDef = iface.GetGenericTypeDefinition();
+ if (genericDef == typeof(IList<>) || genericDef ==
typeof(IReadOnlyList<>) || genericDef == typeof(IEnumerable<>))
+ {
+ elementType = iface.GetGenericArguments()[0];
+ return true;
+ }
+ }
+
+ elementType = null;
+ return false;
+ }
+
+ private static object? ConvertListElement(object? value, Type elementType)
+ {
+ if (value is null || elementType.IsInstanceOfType(value))
+ {
+ return value;
+ }
+
+ Type target = Nullable.GetUnderlyingType(elementType) ?? elementType;
+ try
+ {
+ return Convert.ChangeType(value, target);
+ }
+ catch
+ {
+ return value;
+ }
+ }
}
diff --git a/docs/compiler/compiler-guide.md b/docs/compiler/compiler-guide.md
index 833ea0d3f..33c8f6bc9 100644
--- a/docs/compiler/compiler-guide.md
+++ b/docs/compiler/compiler-guide.md
@@ -63,6 +63,7 @@ Compile options:
| `--cpp_out=DST_DIR` | Generate C++ code in DST_DIR
| (none) |
| `--go_out=DST_DIR` | Generate Go code in DST_DIR
| (none) |
| `--rust_out=DST_DIR` | Generate Rust code in DST_DIR
| (none) |
+| `--csharp_out=DST_DIR` | Generate C# code in DST_DIR
| (none) |
| `--go_nested_type_style` | Go nested type naming: `camelcase`
or `underscore` | from schema/default |
| `--emit-fdl` | Print translated Fory IDL for
non-`.fdl` inputs | `false` |
| `--emit-fdl-path` | Write translated Fory IDL to a file
or directory | (stdout) |
@@ -110,7 +111,7 @@ foryc schema.fdl
**Compile for specific languages:**
```bash
-foryc schema.fdl --lang java,python
+foryc schema.fdl --lang java,python,csharp
```
**Specify output directory:**
@@ -157,7 +158,7 @@ foryc src/main.fdl -I libs/common,libs/types --proto_path
third_party/
foryc schema.fdl --java_out=./src/main/java
# Generate multiple languages to different directories
-foryc schema.fdl --java_out=./java/gen --python_out=./python/src
--go_out=./go/gen
+foryc schema.fdl --java_out=./java/gen --python_out=./python/src
--go_out=./go/gen --csharp_out=./csharp/gen
# Combine with import paths
foryc schema.fdl --java_out=./gen/java -I proto/ -I common/
@@ -226,13 +227,14 @@ Compiling src/main.fdl...
## Supported Languages
-| Language | Flag | Output Extension | Description |
-| -------- | -------- | ---------------- | --------------------------- |
-| Java | `java` | `.java` | POJOs with Fory annotations |
-| Python | `python` | `.py` | Dataclasses with type hints |
-| Go | `go` | `.go` | Structs with struct tags |
-| Rust | `rust` | `.rs` | Structs with derive macros |
-| C++ | `cpp` | `.h` | Structs with FORY macros |
+| Language | Flag | Output Extension | Description |
+| -------- | -------- | ---------------- | ---------------------------- |
+| Java | `java` | `.java` | POJOs with Fory annotations |
+| Python | `python` | `.py` | Dataclasses with type hints |
+| Go | `go` | `.go` | Structs with struct tags |
+| Rust | `rust` | `.rs` | Structs with derive macros |
+| C++ | `cpp` | `.h` | Structs with FORY macros |
+| C# | `csharp` | `.cs` | Classes with Fory attributes |
## Output Structure
@@ -302,6 +304,43 @@ generated/
- Namespace matches package (dots to `::`)
- Header guards and forward declarations
+### C\#
+
+```
+generated/
+└── csharp/
+ └── example/
+ └── example.cs
+```
+
+- Single `.cs` file per schema
+- Namespace uses `csharp_namespace` (if set) or Fory IDL package
+- Includes registration helper and `ToBytes`/`FromBytes` methods
+- Imported schemas are registered transitively (for example `root.idl`
importing
+ `addressbook.fdl` and `tree.fdl`)
+
+### C# IDL Matrix Verification
+
+Run the end-to-end C# IDL matrix (FDL/IDL/Proto/FBS generation plus roundtrip
tests):
+
+```bash
+cd integration_tests/idl_tests
+./run_csharp_tests.sh
+```
+
+This runner executes schema-consistent and compatible roundtrips across:
+
+- `addressbook`, `auto_id`, `complex_pb` primitives
+- `collection` and union/list variants
+- `optional_types`
+- `any_example` (`.fdl`) and `any_example` (`.proto`)
+- `tree` and `graph` reference-tracking cases
+- `monster.fbs` and `complex_fbs.fbs`
+- `root.idl` cross-package import coverage
+- evolving schema compatibility cases
+
+The script also sets `DATA_FILE*` variables so file-based roundtrip paths are
exercised.
+
## Build Integration
### Maven (Java)
diff --git a/docs/compiler/generated-code.md b/docs/compiler/generated-code.md
index b433374af..72225f4fd 100644
--- a/docs/compiler/generated-code.md
+++ b/docs/compiler/generated-code.md
@@ -685,6 +685,63 @@ if err := restored.FromBytes(data); err != nil {
}
```
+## C\#
+
+### Output Layout
+
+C# output is one `.cs` file per schema, for example:
+
+- `<csharp_out>/addressbook/addressbook.cs`
+
+### Type Generation
+
+Messages generate `[ForyObject]` classes with C# properties and byte helpers:
+
+```csharp
+[ForyObject]
+public sealed partial class Person
+{
+ public string Name { get; set; } = string.Empty;
+ public int Id { get; set; }
+ public List<Person.PhoneNumber> Phones { get; set; } = new();
+ public Animal Pet { get; set; } = null!;
+
+ public byte[] ToBytes() { ... }
+ public static Person FromBytes(byte[] data) { ... }
+}
+```
+
+Unions generate `Union` subclasses with typed case helpers:
+
+```csharp
+public sealed class Animal : Union
+{
+ public static Animal Dog(Dog value) { ... }
+ public static Animal Cat(Cat value) { ... }
+ public bool IsDog => ...;
+ public Dog DogValue() { ... }
+}
+```
+
+### Registration
+
+Each schema generates a registration helper:
+
+```csharp
+public static class AddressbookForyRegistration
+{
+ public static void Register(Fory fory)
+ {
+ fory.Register<addressbook.Animal>((uint)106);
+ fory.Register<addressbook.Person>((uint)100);
+ // ...
+ }
+}
+```
+
+When explicit type IDs are not provided, generated registration uses computed
+numeric IDs (same behavior as other targets).
+
## Cross-Language Notes
### Type ID Behavior
@@ -702,6 +759,7 @@ if err := restored.FromBytes(data); err != nil {
| Rust | `person::PhoneNumber` |
| C++ | `Person::PhoneNumber` |
| Go | `Person_PhoneNumber` (default) |
+| C# | `Person.PhoneNumber` |
### Byte Helper Naming
@@ -712,3 +770,4 @@ if err := restored.FromBytes(data); err != nil {
| Rust | `to_bytes` / `from_bytes` |
| C++ | `to_bytes` / `from_bytes` |
| Go | `ToBytes` / `FromBytes` |
+| C# | `ToBytes` / `FromBytes` |
diff --git a/docs/compiler/index.md b/docs/compiler/index.md
index 003801879..d6e8bb99b 100644
--- a/docs/compiler/index.md
+++ b/docs/compiler/index.md
@@ -21,7 +21,7 @@ license: |
Fory IDL is a schema definition language for Apache Fory that enables type-safe
cross-language serialization. Define your data structures once and generate
-native data structure code for Java, Python, Go, Rust, and C++.
+native data structure code for Java, Python, Go, Rust, C++, and C#.
## Example Schema
@@ -100,6 +100,7 @@ Generated code uses native language constructs:
- Go: Structs with struct tags
- Rust: Structs with `#[derive(ForyObject)]`
- C++: Structs with `FORY_STRUCT` macros
+- C#: Classes with `[ForyObject]` and registration helpers
## Quick Start
@@ -137,7 +138,7 @@ message Person {
foryc example.fdl --output ./generated
# Generate for specific languages
-foryc example.fdl --lang java,python --output ./generated
+foryc example.fdl --lang java,python,csharp --output ./generated
```
### 4. Use Generated Code
@@ -192,11 +193,11 @@ message Example {
Fory IDL types map to native types in each language:
-| Fory IDL Type | Java | Python | Go | Rust | C++
|
-| ------------- | --------- | -------------- | -------- | -------- |
------------- |
-| `int32` | `int` | `pyfory.int32` | `int32` | `i32` | `int32_t`
|
-| `string` | `String` | `str` | `string` | `String` |
`std::string` |
-| `bool` | `boolean` | `bool` | `bool` | `bool` | `bool`
|
+| Fory IDL Type | Java | Python | Go | Rust | C++
| C# |
+| ------------- | --------- | -------------- | -------- | -------- |
------------- | -------- |
+| `int32` | `int` | `pyfory.int32` | `int32` | `i32` | `int32_t`
| `int` |
+| `string` | `String` | `str` | `string` | `String` |
`std::string` | `string` |
+| `bool` | `boolean` | `bool` | `bool` | `bool` | `bool`
| `bool` |
See [Type System](schema-idl.md#type-system) for complete mappings.
diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md
index 4e297aa77..eb9a96dc4 100644
--- a/docs/compiler/schema-idl.md
+++ b/docs/compiler/schema-idl.md
@@ -100,6 +100,7 @@ package com.example.models alias models_v1;
| Go | Package name (last component) |
| Rust | Module name (dots to underscores) |
| C++ | Namespace (dots to `::`) |
+| C# | Namespace |
## File-Level Options
@@ -151,6 +152,25 @@ message Payment {
- The import path can be used in other Go code
- Type registration still uses the Fory IDL package (`payment`) for
cross-language compatibility
+### C# Namespace Option
+
+Override the C# namespace for generated code:
+
+```protobuf
+package payment;
+option csharp_namespace = "MyCorp.Payment.V1";
+
+message Payment {
+ string id = 1;
+}
+```
+
+**Effect:**
+
+- Generated C# files use `namespace MyCorp.Payment.V1;`
+- Output path follows namespace segments (`MyCorp/Payment/V1/` under
`--csharp_out`)
+- Type registration still uses the Fory IDL package (`payment`) for
cross-language compatibility
+
### Java Outer Classname Option
Generate all types as inner classes of a single outer wrapper class:
@@ -285,10 +305,10 @@ For protobuf extension options, see
### Option Priority
-For language-specific packages:
+For language-specific packages/namespaces:
1. Command-line package override (highest priority)
-2. Language-specific option (`java_package`, `go_package`)
+2. Language-specific option (`java_package`, `go_package`, `csharp_namespace`)
3. Fory IDL package declaration (fallback)
**Example:**
diff --git a/integration_tests/idl_tests/README.md
b/integration_tests/idl_tests/README.md
index 053697e9f..e3c8c34cd 100644
--- a/integration_tests/idl_tests/README.md
+++ b/integration_tests/idl_tests/README.md
@@ -10,3 +10,4 @@ Run tests:
- Go: `./run_go_tests.sh`
- Rust: `./run_rust_tests.sh`
- C++: `./run_cpp_tests.sh`
+- C#: `./run_csharp_tests.sh`
diff --git a/integration_tests/idl_tests/csharp/IdlTests/.gitignore
b/integration_tests/idl_tests/csharp/IdlTests/.gitignore
new file mode 100644
index 000000000..87c106435
--- /dev/null
+++ b/integration_tests/idl_tests/csharp/IdlTests/.gitignore
@@ -0,0 +1,4 @@
+Generated/
+bin/
+obj/
+TestResults/
diff --git a/integration_tests/idl_tests/csharp/IdlTests/GlobalUsings.cs
b/integration_tests/idl_tests/csharp/IdlTests/GlobalUsings.cs
new file mode 100644
index 000000000..32c527781
--- /dev/null
+++ b/integration_tests/idl_tests/csharp/IdlTests/GlobalUsings.cs
@@ -0,0 +1,18 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+global using Xunit;
diff --git a/integration_tests/idl_tests/csharp/IdlTests/IdlTests.csproj
b/integration_tests/idl_tests/csharp/IdlTests/IdlTests.csproj
new file mode 100644
index 000000000..d216646be
--- /dev/null
+++ b/integration_tests/idl_tests/csharp/IdlTests/IdlTests.csproj
@@ -0,0 +1,28 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <LangVersion>12.0</LangVersion>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <IsPackable>false</IsPackable>
+ <IsTestProject>true</IsTestProject>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="coverlet.collector" Version="6.0.0">
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers;
buildtransitive</IncludeAssets>
+ <PrivateAssets>all</PrivateAssets>
+ </PackageReference>
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
+ <PackageReference Include="xunit" Version="2.9.2" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers;
buildtransitive</IncludeAssets>
+ <PrivateAssets>all</PrivateAssets>
+ </PackageReference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="../../../../csharp/src/Fory/Fory.csproj" />
+ <ProjectReference
Include="../../../../csharp/src/Fory.Generator/Fory.Generator.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
+ </ItemGroup>
+</Project>
diff --git a/integration_tests/idl_tests/csharp/IdlTests/RoundtripTests.cs
b/integration_tests/idl_tests/csharp/IdlTests/RoundtripTests.cs
new file mode 100644
index 000000000..79e107d28
--- /dev/null
+++ b/integration_tests/idl_tests/csharp/IdlTests/RoundtripTests.cs
@@ -0,0 +1,1233 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using System.Globalization;
+using Apache.Fory;
+using ForyRuntime = Apache.Fory.Fory;
+
+namespace Apache.Fory.IdlTests;
+
+public sealed class RoundtripTests
+{
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AddressBookRoundTrip(bool compatible)
+ {
+ ForyRuntime fory = BuildFory(compatible, false);
+ addressbook.AddressbookForyRegistration.Register(fory);
+
+ addressbook.AddressBook book = BuildAddressBook();
+ addressbook.AddressBook decoded =
fory.Deserialize<addressbook.AddressBook>(fory.Serialize(book));
+ AssertAddressBook(book, decoded);
+
+ RoundTripFile(fory, "DATA_FILE", book, AssertAddressBook);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AutoIdRoundTrip(bool compatible)
+ {
+ ForyRuntime fory = BuildFory(compatible, false);
+ auto_id.AutoIdForyRegistration.Register(fory);
+
+ auto_id.Envelope envelope = BuildEnvelope();
+ auto_id.Wrapper wrapper = 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());
+
+ RoundTripFile(fory, "DATA_FILE_AUTO_ID", envelope, AssertEnvelope);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void PrimitiveTypesRoundTrip(bool compatible)
+ {
+ ForyRuntime fory = BuildFory(compatible, false);
+ complex_pb.ComplexPbForyRegistration.Register(fory);
+
+ complex_pb.PrimitiveTypes types = BuildPrimitiveTypes();
+ complex_pb.PrimitiveTypes decoded =
fory.Deserialize<complex_pb.PrimitiveTypes>(fory.Serialize(types));
+ AssertPrimitiveTypes(types, decoded);
+
+ RoundTripFile(fory, "DATA_FILE_PRIMITIVES", types,
AssertPrimitiveTypes);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void CollectionRoundTrip(bool compatible)
+ {
+ ForyRuntime fory = BuildFory(compatible, false);
+ collection.CollectionForyRegistration.Register(fory);
+
+ collection.NumericCollections collections = BuildNumericCollections();
+ collection.NumericCollectionUnion unionValue =
BuildNumericCollectionUnion();
+ collection.NumericCollectionsArray collectionsArray =
BuildNumericCollectionsArray();
+ collection.NumericCollectionArrayUnion arrayUnion =
BuildNumericCollectionArrayUnion();
+
+ collection.NumericCollections collectionsDecoded =
+
fory.Deserialize<collection.NumericCollections>(fory.Serialize(collections));
+ AssertNumericCollections(collections, collectionsDecoded);
+
+ collection.NumericCollectionUnion unionDecoded =
+
fory.Deserialize<collection.NumericCollectionUnion>(fory.Serialize(unionValue));
+ AssertNumericCollectionUnion(unionValue, unionDecoded);
+
+ collection.NumericCollectionsArray arrayDecoded =
+
fory.Deserialize<collection.NumericCollectionsArray>(fory.Serialize(collectionsArray));
+ AssertNumericCollectionsArray(collectionsArray, arrayDecoded);
+
+ collection.NumericCollectionArrayUnion arrayUnionDecoded =
+
fory.Deserialize<collection.NumericCollectionArrayUnion>(fory.Serialize(arrayUnion));
+ AssertNumericCollectionArrayUnion(arrayUnion, arrayUnionDecoded);
+
+ RoundTripFile(fory, "DATA_FILE_COLLECTION", collections,
AssertNumericCollections);
+ RoundTripFile(fory, "DATA_FILE_COLLECTION_UNION", unionValue,
AssertNumericCollectionUnion);
+ RoundTripFile(fory, "DATA_FILE_COLLECTION_ARRAY", collectionsArray,
AssertNumericCollectionsArray);
+ RoundTripFile(fory, "DATA_FILE_COLLECTION_ARRAY_UNION", arrayUnion,
AssertNumericCollectionArrayUnion);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void OptionalTypesRoundTrip(bool compatible)
+ {
+ ForyRuntime fory = BuildFory(compatible, false);
+ optional_types.OptionalTypesForyRegistration.Register(fory);
+
+ optional_types.OptionalHolder holder = BuildOptionalHolder();
+ optional_types.OptionalHolder decoded =
fory.Deserialize<optional_types.OptionalHolder>(fory.Serialize(holder));
+ AssertOptionalHolder(holder, decoded);
+
+ RoundTripFile(fory, "DATA_FILE_OPTIONAL_TYPES", holder,
AssertOptionalHolder);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AnyRoundTrip(bool compatible)
+ {
+ ForyRuntime fory = BuildFory(compatible, false);
+ any_example.AnyExampleForyRegistration.Register(fory);
+
+ any_example.AnyHolder holder = BuildAnyHolder();
+ any_example.AnyHolder decoded =
fory.Deserialize<any_example.AnyHolder>(fory.Serialize(holder));
+ AssertAnyHolder(holder, decoded);
+
+ RoundTripFile(fory, "DATA_FILE_ANY", holder, AssertAnyHolder);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AnyProtoRoundTrip(bool compatible)
+ {
+ ForyRuntime fory = BuildFory(compatible, false);
+ any_example_pb.AnyExamplePbForyRegistration.Register(fory);
+
+ any_example_pb.AnyHolder holder = BuildAnyProtoHolder();
+ any_example_pb.AnyHolder decoded =
fory.Deserialize<any_example_pb.AnyHolder>(fory.Serialize(holder));
+ AssertAnyProtoHolder(holder, decoded);
+
+ RoundTripFile(fory, "DATA_FILE_ANY_PROTO", holder,
AssertAnyProtoHolder);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void FlatbuffersRoundTrip(bool compatible)
+ {
+ ForyRuntime fory = BuildFory(compatible, false);
+ monster.MonsterForyRegistration.Register(fory);
+ complex_fbs.ComplexFbsForyRegistration.Register(fory);
+
+ monster.Monster monsterValue = BuildMonster();
+ monster.Monster monsterDecoded =
fory.Deserialize<monster.Monster>(fory.Serialize(monsterValue));
+ AssertMonster(monsterValue, monsterDecoded);
+
+ complex_fbs.Container container = BuildContainer();
+ complex_fbs.Container containerDecoded =
fory.Deserialize<complex_fbs.Container>(fory.Serialize(container));
+ AssertContainer(container, containerDecoded);
+
+ RoundTripFile(fory, "DATA_FILE_FLATBUFFERS_MONSTER", monsterValue,
AssertMonster);
+ RoundTripFile(fory, "DATA_FILE_FLATBUFFERS_TEST2", container,
AssertContainer);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void TreeRoundTrip(bool compatible)
+ {
+ ForyRuntime fory = BuildFory(compatible, true);
+ tree.TreeForyRegistration.Register(fory);
+
+ tree.TreeNode treeRoot = BuildTree();
+ tree.TreeNode decoded =
fory.Deserialize<tree.TreeNode>(fory.Serialize(treeRoot));
+ AssertTree(decoded);
+
+ RoundTripFile(fory, "DATA_FILE_TREE", treeRoot, (_, actual) =>
AssertTree(actual));
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void GraphRoundTrip(bool compatible)
+ {
+ ForyRuntime fory = BuildFory(compatible, true);
+ graph.GraphForyRegistration.Register(fory);
+
+ graph.Graph graphValue = BuildGraph();
+ graph.Graph decoded =
fory.Deserialize<graph.Graph>(fory.Serialize(graphValue));
+ AssertGraph(decoded);
+
+ RoundTripFile(fory, "DATA_FILE_GRAPH", graphValue, (_, actual) =>
AssertGraph(actual));
+ }
+
+ [Fact]
+ public void EvolvingRoundTrip()
+ {
+ ForyRuntime foryV1 = BuildFory(true, false);
+ ForyRuntime foryV2 = BuildFory(true, false);
+ evolving1.Evolving1ForyRegistration.Register(foryV1);
+ evolving2.Evolving2ForyRegistration.Register(foryV2);
+
+ evolving1.EvolvingMessage messageV1 = new()
+ {
+ Id = 1,
+ Name = "Alice",
+ City = "NYC",
+ };
+
+ evolving2.EvolvingMessage messageV2 =
foryV2.Deserialize<evolving2.EvolvingMessage>(foryV1.Serialize(messageV1));
+ Assert.Equal(messageV1.Id, messageV2.Id);
+ Assert.Equal(messageV1.Name, messageV2.Name);
+ Assert.Equal(messageV1.City, messageV2.City);
+
+ messageV2.Email = "[email protected]";
+ evolving1.EvolvingMessage messageV1Round =
+
foryV1.Deserialize<evolving1.EvolvingMessage>(foryV2.Serialize(messageV2));
+ Assert.Equal(messageV1.Id, messageV1Round.Id);
+ Assert.Equal(messageV1.Name, messageV1Round.Name);
+ Assert.Equal(messageV1.City, messageV1Round.City);
+
+ evolving1.FixedMessage fixedV1 = new()
+ {
+ Id = 10,
+ Name = "Bob",
+ Score = 90,
+ Note = "note",
+ };
+
+ bool fixedRoundTripMatches = false;
+ try
+ {
+ evolving2.FixedMessage fixedV2 =
foryV2.Deserialize<evolving2.FixedMessage>(foryV1.Serialize(fixedV1));
+ evolving1.FixedMessage fixedV1Round =
+
foryV1.Deserialize<evolving1.FixedMessage>(foryV2.Serialize(fixedV2));
+ fixedRoundTripMatches =
+ fixedV1Round.Id == fixedV1.Id &&
+ fixedV1Round.Name == fixedV1.Name &&
+ fixedV1Round.Score == fixedV1.Score &&
+ fixedV1Round.Note == fixedV1.Note;
+ }
+ catch
+ {
+ fixedRoundTripMatches = false;
+ }
+
+ Assert.False(fixedRoundTripMatches);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void RootRoundTrip(bool compatible)
+ {
+ ForyRuntime fory = BuildFory(compatible, true);
+ root.RootForyRegistration.Register(fory);
+
+ root.MultiHolder holder = BuildRootHolder();
+ root.MultiHolder decoded =
fory.Deserialize<root.MultiHolder>(fory.Serialize(holder));
+ AssertRootHolder(holder, decoded);
+
+ RoundTripFile(fory, "DATA_FILE_ROOT", holder, AssertRootHolder);
+ }
+
+ [Fact]
+ public void RootToBytesFromBytes()
+ {
+ root.MultiHolder holder = BuildRootHolder();
+ byte[] payload = holder.ToBytes();
+ root.MultiHolder decoded = root.MultiHolder.FromBytes(payload);
+ AssertRootHolder(holder, decoded);
+ }
+
+ [Fact]
+ public void ToBytesFromBytesHelpers()
+ {
+ addressbook.AddressBook book = BuildAddressBook();
+ addressbook.AddressBook decodedBook =
addressbook.AddressBook.FromBytes(book.ToBytes());
+ AssertAddressBook(book, decodedBook);
+
+ addressbook.Animal animal = 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);
+ }
+
+ private static ForyRuntime BuildFory(bool compatible, bool trackRef)
+ {
+ return ForyRuntime.Builder()
+ .Xlang(true)
+ .Compatible(compatible)
+ .TrackRef(trackRef)
+ .Build();
+ }
+
+ private static void RoundTripFile<T>(
+ ForyRuntime fory,
+ string envName,
+ T expected,
+ Action<T, T> assertRoundTrip)
+ {
+ string? dataFile = Environment.GetEnvironmentVariable(envName);
+ if (string.IsNullOrWhiteSpace(dataFile))
+ {
+ return;
+ }
+
+ string modeFile = dataFile +
+ (fory.Config.Compatible ? ".compatible" :
".schema_consistent") +
+ (fory.Config.TrackRef ? ".track_ref" : ".no_ref");
+
+ if (!File.Exists(modeFile) || new FileInfo(modeFile).Length == 0)
+ {
+ File.WriteAllBytes(modeFile, fory.Serialize(expected));
+ }
+
+ byte[] peerPayload = File.ReadAllBytes(modeFile);
+ T decoded = fory.Deserialize<T>(peerPayload);
+ assertRoundTrip(expected, decoded);
+
+ byte[] output = fory.Serialize(decoded);
+ File.WriteAllBytes(modeFile, output);
+ }
+
+ private static addressbook.AddressBook BuildAddressBook()
+ {
+ addressbook.Person.PhoneNumber mobile = new()
+ {
+ Number = "555-0100",
+ PhoneType = addressbook.Person.PhoneType.Mobile,
+ };
+ addressbook.Person.PhoneNumber work = new()
+ {
+ Number = "555-0111",
+ PhoneType = addressbook.Person.PhoneType.Work,
+ };
+
+ addressbook.Animal pet = addressbook.Animal.Dog(new addressbook.Dog
+ {
+ Name = "Rex",
+ BarkVolume = 5,
+ });
+ pet = addressbook.Animal.Cat(new addressbook.Cat
+ {
+ Name = "Mimi",
+ Lives = 9,
+ });
+
+ addressbook.Person person = new()
+ {
+ Name = "Alice",
+ Id = 123,
+ Email = "[email protected]",
+ Tags = ["friend", "colleague"],
+ Scores = new Dictionary<string, int>
+ {
+ ["math"] = 100,
+ ["science"] = 98,
+ },
+ Salary = 120000.5,
+ Phones = [mobile, work],
+ Pet = pet,
+ };
+
+ return new addressbook.AddressBook
+ {
+ People = [person],
+ PeopleByName = new Dictionary<string, addressbook.Person>
+ {
+ [person.Name] = person,
+ },
+ };
+ }
+
+ private static auto_id.Envelope BuildEnvelope()
+ {
+ auto_id.Envelope.Payload payload = new()
+ {
+ Value = 42,
+ };
+
+ return new auto_id.Envelope
+ {
+ Id = "env-1",
+ PayloadValue = payload,
+ DetailValue = auto_id.Envelope.Detail.Payload(payload),
+ Status = auto_id.Status.Ok,
+ };
+ }
+
+ private static complex_pb.PrimitiveTypes BuildPrimitiveTypes()
+ {
+ return new complex_pb.PrimitiveTypes
+ {
+ BoolValue = true,
+ Int8Value = 12,
+ Int16Value = 1234,
+ Int32Value = -123456,
+ Varint32Value = -12345,
+ Int64Value = -123456789,
+ Varint64Value = -987654321,
+ TaggedInt64Value = 123456789,
+ Uint8Value = 200,
+ Uint16Value = 60000,
+ Uint32Value = 1234567890,
+ VarUint32Value = 1234567890,
+ Uint64Value = 9876543210,
+ VarUint64Value = 12345678901,
+ TaggedUint64Value = 2222222222,
+ Float32Value = 2.5f,
+ Float64Value = 3.5,
+ ContactValue = complex_pb.PrimitiveTypes.Contact.Phone(12345),
+ };
+ }
+
+ private static collection.NumericCollections BuildNumericCollections()
+ {
+ return new collection.NumericCollections
+ {
+ Int8Values = [1, -2, 3],
+ Int16Values = [100, -200, 300],
+ Int32Values = [1000, -2000, 3000],
+ Int64Values = [10000, -20000, 30000],
+ Uint8Values = [200, 250],
+ Uint16Values = [50000, 60000],
+ Uint32Values = [2000000000, 2100000000],
+ Uint64Values = [9000000000, 12000000000],
+ Float32Values = [1.5f, 2.5f],
+ Float64Values = [3.5, 4.5],
+ };
+ }
+
+ private static collection.NumericCollectionUnion
BuildNumericCollectionUnion()
+ {
+ return collection.NumericCollectionUnion.Int32Values([7, 8, 9]);
+ }
+
+ private static collection.NumericCollectionsArray
BuildNumericCollectionsArray()
+ {
+ return new collection.NumericCollectionsArray
+ {
+ Int8Values = [1, -2, 3],
+ Int16Values = [100, -200, 300],
+ Int32Values = [1000, -2000, 3000],
+ Int64Values = [10000, -20000, 30000],
+ Uint8Values = [200, 250],
+ Uint16Values = [50000, 60000],
+ Uint32Values = [2000000000, 2100000000],
+ Uint64Values = [9000000000, 12000000000],
+ Float32Values = [1.5f, 2.5f],
+ Float64Values = [3.5, 4.5],
+ };
+ }
+
+ private static collection.NumericCollectionArrayUnion
BuildNumericCollectionArrayUnion()
+ {
+ return collection.NumericCollectionArrayUnion.Uint16Values([1000,
2000, 3000]);
+ }
+
+ private static optional_types.OptionalHolder BuildOptionalHolder()
+ {
+ DateOnly date = new(2024, 1, 2);
+ DateTimeOffset timestamp =
DateTimeOffset.FromUnixTimeSeconds(1704164645);
+ optional_types.AllOptionalTypes all = new()
+ {
+ BoolValue = true,
+ Int8Value = 12,
+ Int16Value = 1234,
+ Int32Value = -123456,
+ FixedInt32Value = -123456,
+ Varint32Value = -12345,
+ Int64Value = -123456789,
+ FixedInt64Value = -123456789,
+ Varint64Value = -987654321,
+ TaggedInt64Value = 123456789,
+ Uint8Value = 200,
+ Uint16Value = 60000,
+ Uint32Value = 1234567890,
+ FixedUint32Value = 1234567890,
+ VarUint32Value = 1234567890,
+ Uint64Value = 9876543210,
+ FixedUint64Value = 9876543210,
+ VarUint64Value = 12345678901,
+ TaggedUint64Value = 2222222222,
+ Float32Value = 2.5f,
+ Float64Value = 3.5,
+ StringValue = "optional",
+ BytesValue = [1, 2, 3],
+ DateValue = date,
+ TimestampValue = timestamp,
+ Int32List = [1, 2, 3],
+ StringList = ["alpha", "beta"],
+ Int64Map = new Dictionary<string, long>
+ {
+ ["alpha"] = 10,
+ ["beta"] = 20,
+ },
+ };
+
+ return new optional_types.OptionalHolder
+ {
+ AllTypes = all,
+ Choice = optional_types.OptionalUnion.Note("optional"),
+ };
+ }
+
+ private static any_example.AnyHolder BuildAnyHolder()
+ {
+ return new any_example.AnyHolder
+ {
+ BoolValue = true,
+ StringValue = "hello",
+ DateValue = new DateOnly(2024, 1, 2),
+ TimestampValue = DateTimeOffset.FromUnixTimeSeconds(1704164645),
+ MessageValue = new any_example.AnyInner
+ {
+ Name = "inner",
+ },
+ UnionValue = any_example.AnyUnion.Text("union"),
+ ListValue = new List<string>
+ {
+ "alpha",
+ "beta",
+ },
+ MapValue = new Dictionary<string, string>
+ {
+ ["k1"] = "v1",
+ ["k2"] = "v2",
+ },
+ };
+ }
+
+ private static any_example_pb.AnyHolder BuildAnyProtoHolder()
+ {
+ any_example_pb.AnyUnion union = new()
+ {
+ Kind = any_example_pb.AnyUnion.kind.Text("proto-union"),
+ };
+
+ return new any_example_pb.AnyHolder
+ {
+ BoolValue = true,
+ StringValue = "hello",
+ DateValue = new DateOnly(2024, 1, 2),
+ TimestampValue = DateTimeOffset.FromUnixTimeSeconds(1704164645),
+ MessageValue = new any_example_pb.AnyInner
+ {
+ Name = "inner",
+ },
+ UnionValue = union,
+ ListValue = new List<string>
+ {
+ "alpha",
+ "beta",
+ },
+ MapValue = new Dictionary<string, string>
+ {
+ ["k1"] = "v1",
+ ["k2"] = "v2",
+ },
+ };
+ }
+
+ private static monster.Monster BuildMonster()
+ {
+ return new monster.Monster
+ {
+ Pos = new monster.Vec3
+ {
+ X = 1.0f,
+ Y = 2.0f,
+ Z = 3.0f,
+ },
+ Mana = 200,
+ Hp = 80,
+ Name = "Orc",
+ Friendly = true,
+ Inventory = [1, 2, 3],
+ Color = monster.Color.Blue,
+ };
+ }
+
+ private static complex_fbs.Container BuildContainer()
+ {
+ return new complex_fbs.Container
+ {
+ Id = 9876543210,
+ Status = complex_fbs.Status.Started,
+ Bytes = [1, 2, 3],
+ Numbers = [10, 20, 30],
+ Scalars = new complex_fbs.ScalarPack
+ {
+ B = -8,
+ Ub = 200,
+ S = -1234,
+ Us = 40000,
+ I = -123456,
+ Ui = 123456,
+ L = -123456789,
+ Ul = 987654321,
+ F = 1.5f,
+ D = 2.5,
+ Ok = true,
+ },
+ Names = ["alpha", "beta"],
+ Flags = [true, false],
+ Payload = complex_fbs.Payload.Metric(new complex_fbs.Metric
+ {
+ Value = 42.0,
+ }),
+ };
+ }
+
+ private static tree.TreeNode BuildTree()
+ {
+ tree.TreeNode childA = new()
+ {
+ Id = "child-a",
+ Name = "child-a",
+ Children = [],
+ };
+ tree.TreeNode childB = new()
+ {
+ Id = "child-b",
+ Name = "child-b",
+ Children = [],
+ };
+
+ childA.Parent = childB;
+ childB.Parent = childA;
+
+ return new tree.TreeNode
+ {
+ Id = "root",
+ Name = "root",
+ Children = [childA, childA, childB],
+ };
+ }
+
+ private static graph.Graph BuildGraph()
+ {
+ graph.Node node1 = new()
+ {
+ Id = "n1",
+ };
+ graph.Node node2 = new()
+ {
+ Id = "n2",
+ };
+
+ graph.Edge edge12 = new()
+ {
+ Id = "e12",
+ Weight = 1.5f,
+ From = node1,
+ To = node2,
+ };
+ graph.Edge edge21 = new()
+ {
+ Id = "e21",
+ Weight = 2.5f,
+ From = node2,
+ To = node1,
+ };
+
+ node1.OutEdges = [edge12];
+ node1.InEdges = [edge21];
+ node2.OutEdges = [edge21];
+ node2.InEdges = [edge12];
+
+ return new graph.Graph
+ {
+ Nodes = [node1, node2],
+ Edges = [edge12, edge21],
+ };
+ }
+
+ private static root.MultiHolder BuildRootHolder()
+ {
+ addressbook.AddressBook book = BuildAddressBook();
+ addressbook.Person owner = book.People[0];
+
+ tree.TreeNode treeRoot = new()
+ {
+ Id = "root",
+ Name = "root",
+ Children = [],
+ };
+
+ return new root.MultiHolder
+ {
+ Book = book,
+ Root = treeRoot,
+ Owner = owner,
+ };
+ }
+
+ private static void AssertAddressBook(addressbook.AddressBook expected,
addressbook.AddressBook actual)
+ {
+ Assert.Single(actual.People);
+ Assert.Single(actual.PeopleByName);
+
+ addressbook.Person expectedPerson = expected.People[0];
+ addressbook.Person actualPerson = actual.People[0];
+
+ Assert.Equal(expectedPerson.Name, actualPerson.Name);
+ Assert.Equal(expectedPerson.Id, actualPerson.Id);
+ Assert.Equal(expectedPerson.Email, actualPerson.Email);
+ Assert.Equal(expectedPerson.Tags, actualPerson.Tags);
+ AssertMap(expectedPerson.Scores, actualPerson.Scores);
+ Assert.Equal(expectedPerson.Salary, actualPerson.Salary);
+ 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);
+ }
+
+ private static void AssertEnvelope(auto_id.Envelope expected,
auto_id.Envelope actual)
+ {
+ Assert.Equal(expected.Id, actual.Id);
+ Assert.NotNull(actual.PayloadValue);
+ Assert.NotNull(expected.PayloadValue);
+ Assert.Equal(expected.PayloadValue.Value, actual.PayloadValue.Value);
+ 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);
+ }
+
+ private static void AssertPrimitiveTypes(complex_pb.PrimitiveTypes
expected, complex_pb.PrimitiveTypes actual)
+ {
+ Assert.Equal(expected.BoolValue, actual.BoolValue);
+ Assert.Equal(expected.Int8Value, actual.Int8Value);
+ Assert.Equal(expected.Int16Value, actual.Int16Value);
+ Assert.Equal(expected.Int32Value, actual.Int32Value);
+ Assert.Equal(expected.Varint32Value, actual.Varint32Value);
+ Assert.Equal(expected.Int64Value, actual.Int64Value);
+ Assert.Equal(expected.Varint64Value, actual.Varint64Value);
+ Assert.Equal(expected.TaggedInt64Value, actual.TaggedInt64Value);
+ Assert.Equal(expected.Uint8Value, actual.Uint8Value);
+ Assert.Equal(expected.Uint16Value, actual.Uint16Value);
+ Assert.Equal(expected.Uint32Value, actual.Uint32Value);
+ Assert.Equal(expected.VarUint32Value, actual.VarUint32Value);
+ Assert.Equal(expected.Uint64Value, actual.Uint64Value);
+ Assert.Equal(expected.VarUint64Value, actual.VarUint64Value);
+ Assert.Equal(expected.TaggedUint64Value, actual.TaggedUint64Value);
+ Assert.Equal(expected.Float32Value, actual.Float32Value);
+ Assert.Equal(expected.Float64Value, actual.Float64Value);
+
+ 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());
+ }
+
+ private static void AssertNumericCollections(
+ collection.NumericCollections expected,
+ collection.NumericCollections actual)
+ {
+ Assert.Equal(expected.Int8Values, actual.Int8Values);
+ Assert.Equal(expected.Int16Values, actual.Int16Values);
+ Assert.Equal(expected.Int32Values, actual.Int32Values);
+ Assert.Equal(expected.Int64Values, actual.Int64Values);
+ Assert.Equal(expected.Uint8Values, actual.Uint8Values);
+ Assert.Equal(expected.Uint16Values, actual.Uint16Values);
+ Assert.Equal(expected.Uint32Values, actual.Uint32Values);
+ Assert.Equal(expected.Uint64Values, actual.Uint64Values);
+ Assert.Equal(expected.Float32Values, actual.Float32Values);
+ Assert.Equal(expected.Float64Values, actual.Float64Values);
+ }
+
+ private static void AssertNumericCollectionUnion(
+ collection.NumericCollectionUnion expected,
+ collection.NumericCollectionUnion actual)
+ {
+ Assert.Equal(expected.CaseId(), actual.CaseId());
+ Assert.True(actual.IsInt32Values);
+ Assert.Equal(expected.Int32ValuesValue(), actual.Int32ValuesValue());
+ }
+
+ private static void AssertNumericCollectionsArray(
+ collection.NumericCollectionsArray expected,
+ collection.NumericCollectionsArray actual)
+ {
+ Assert.Equal(expected.Int8Values, actual.Int8Values);
+ Assert.Equal(expected.Int16Values, actual.Int16Values);
+ Assert.Equal(expected.Int32Values, actual.Int32Values);
+ Assert.Equal(expected.Int64Values, actual.Int64Values);
+ Assert.Equal(expected.Uint8Values, actual.Uint8Values);
+ Assert.Equal(expected.Uint16Values, actual.Uint16Values);
+ Assert.Equal(expected.Uint32Values, actual.Uint32Values);
+ Assert.Equal(expected.Uint64Values, actual.Uint64Values);
+ Assert.Equal(expected.Float32Values, actual.Float32Values);
+ Assert.Equal(expected.Float64Values, actual.Float64Values);
+ }
+
+ private static void AssertNumericCollectionArrayUnion(
+ collection.NumericCollectionArrayUnion expected,
+ collection.NumericCollectionArrayUnion actual)
+ {
+ Assert.Equal(expected.CaseId(), actual.CaseId());
+ Assert.True(actual.IsUint16Values);
+ Assert.Equal(expected.Uint16ValuesValue(), actual.Uint16ValuesValue());
+ }
+
+ private static void AssertOptionalHolder(
+ optional_types.OptionalHolder expected,
+ optional_types.OptionalHolder actual)
+ {
+ Assert.NotNull(actual.AllTypes);
+ Assert.NotNull(expected.AllTypes);
+
+ optional_types.AllOptionalTypes e = expected.AllTypes;
+ optional_types.AllOptionalTypes a = actual.AllTypes;
+
+ Assert.Equal(e.BoolValue, a.BoolValue);
+ Assert.Equal(e.Int8Value, a.Int8Value);
+ Assert.Equal(e.Int16Value, a.Int16Value);
+ Assert.Equal(e.Int32Value, a.Int32Value);
+ Assert.Equal(e.FixedInt32Value, a.FixedInt32Value);
+ Assert.Equal(e.Varint32Value, a.Varint32Value);
+ Assert.Equal(e.Int64Value, a.Int64Value);
+ Assert.Equal(e.FixedInt64Value, a.FixedInt64Value);
+ Assert.Equal(e.Varint64Value, a.Varint64Value);
+ Assert.Equal(e.TaggedInt64Value, a.TaggedInt64Value);
+ Assert.Equal(e.Uint8Value, a.Uint8Value);
+ Assert.Equal(e.Uint16Value, a.Uint16Value);
+ Assert.Equal(e.Uint32Value, a.Uint32Value);
+ Assert.Equal(e.FixedUint32Value, a.FixedUint32Value);
+ Assert.Equal(e.VarUint32Value, a.VarUint32Value);
+ Assert.Equal(e.Uint64Value, a.Uint64Value);
+ Assert.Equal(e.FixedUint64Value, a.FixedUint64Value);
+ Assert.Equal(e.VarUint64Value, a.VarUint64Value);
+ Assert.Equal(e.TaggedUint64Value, a.TaggedUint64Value);
+ Assert.Equal(e.Float32Value, a.Float32Value);
+ Assert.Equal(e.Float64Value, a.Float64Value);
+ Assert.Equal(e.StringValue, a.StringValue);
+ Assert.Equal(e.BytesValue, a.BytesValue);
+ Assert.Equal(e.DateValue, a.DateValue);
+ Assert.Equal(e.TimestampValue, a.TimestampValue);
+ Assert.Equal(e.Int32List, a.Int32List);
+ Assert.Equal(e.StringList, a.StringList);
+ AssertNullableMap(e.Int64Map, a.Int64Map);
+
+ 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());
+ }
+
+ private static void AssertAnyHolder(any_example.AnyHolder expected,
any_example.AnyHolder actual)
+ {
+ Assert.True(TryAsBool(expected.BoolValue, out bool expectedBool));
+ Assert.True(TryAsBool(actual.BoolValue, out bool actualBool));
+ Assert.Equal(expectedBool, actualBool);
+
+ Assert.True(TryAsString(expected.StringValue, out string?
expectedString));
+ Assert.True(TryAsString(actual.StringValue, out string? actualString));
+ Assert.Equal(expectedString, actualString);
+
+ Assert.True(TryAsDateOnly(expected.DateValue, out DateOnly
expectedDate));
+ Assert.True(TryAsDateOnly(actual.DateValue, out DateOnly actualDate));
+ Assert.Equal(expectedDate, actualDate);
+
+ Assert.True(TryAsTimestamp(expected.TimestampValue, out DateTimeOffset
expectedTimestamp));
+ Assert.True(TryAsTimestamp(actual.TimestampValue, out DateTimeOffset
actualTimestamp));
+ Assert.Equal(expectedTimestamp, actualTimestamp);
+
+ Assert.True(TryAnyInnerName(expected.MessageValue, out string?
expectedInnerName));
+ 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());
+
+ Assert.True(TryStringList(expected.ListValue, out List<string>
expectedList));
+ Assert.True(TryStringList(actual.ListValue, out List<string>
actualList));
+ Assert.Equal(expectedList, actualList);
+
+ Assert.True(TryStringMap(expected.MapValue, out Dictionary<string,
string> expectedMap));
+ Assert.True(TryStringMap(actual.MapValue, out Dictionary<string,
string> actualMap));
+ AssertMap(expectedMap, actualMap);
+ }
+
+ private static void AssertAnyProtoHolder(any_example_pb.AnyHolder
expected, any_example_pb.AnyHolder actual)
+ {
+ Assert.True(TryAsBool(expected.BoolValue, out bool expectedBool));
+ Assert.True(TryAsBool(actual.BoolValue, out bool actualBool));
+ Assert.Equal(expectedBool, actualBool);
+
+ Assert.True(TryAsString(expected.StringValue, out string?
expectedString));
+ Assert.True(TryAsString(actual.StringValue, out string? actualString));
+ Assert.Equal(expectedString, actualString);
+
+ Assert.True(TryAsDateOnly(expected.DateValue, out DateOnly
expectedDate));
+ Assert.True(TryAsDateOnly(actual.DateValue, out DateOnly actualDate));
+ Assert.Equal(expectedDate, actualDate);
+
+ Assert.True(TryAsTimestamp(expected.TimestampValue, out DateTimeOffset
expectedTimestamp));
+ Assert.True(TryAsTimestamp(actual.TimestampValue, out DateTimeOffset
actualTimestamp));
+ Assert.Equal(expectedTimestamp, actualTimestamp);
+
+ Assert.True(TryAnyInnerName(expected.MessageValue, out string?
expectedInnerName));
+ Assert.True(TryAnyInnerName(actual.MessageValue, out string?
actualInnerName));
+ Assert.Equal(expectedInnerName, actualInnerName);
+
+ any_example_pb.AnyUnion expectedUnion =
Assert.IsType<any_example_pb.AnyUnion>(expected.UnionValue);
+ any_example_pb.AnyUnion actualUnion =
Assert.IsType<any_example_pb.AnyUnion>(actual.UnionValue);
+ Assert.NotNull(expectedUnion.Kind);
+ Assert.NotNull(actualUnion.Kind);
+ Assert.Equal(expectedUnion.Kind.CaseId(), actualUnion.Kind.CaseId());
+ Assert.True(actualUnion.Kind.IsText);
+ Assert.Equal(expectedUnion.Kind.TextValue(),
actualUnion.Kind.TextValue());
+
+ Assert.True(TryStringList(expected.ListValue, out List<string>
expectedList));
+ Assert.True(TryStringList(actual.ListValue, out List<string>
actualList));
+ Assert.Equal(expectedList, actualList);
+
+ Assert.True(TryStringMap(expected.MapValue, out Dictionary<string,
string> expectedMap));
+ Assert.True(TryStringMap(actual.MapValue, out Dictionary<string,
string> actualMap));
+ AssertMap(expectedMap, actualMap);
+ }
+
+ private static void AssertMonster(monster.Monster expected,
monster.Monster actual)
+ {
+ Assert.NotNull(actual.Pos);
+ Assert.NotNull(expected.Pos);
+ Assert.Equal(expected.Pos.X, actual.Pos.X);
+ Assert.Equal(expected.Pos.Y, actual.Pos.Y);
+ Assert.Equal(expected.Pos.Z, actual.Pos.Z);
+ Assert.Equal(expected.Mana, actual.Mana);
+ Assert.Equal(expected.Hp, actual.Hp);
+ Assert.Equal(expected.Name, actual.Name);
+ Assert.Equal(expected.Friendly, actual.Friendly);
+ Assert.Equal(expected.Inventory, actual.Inventory);
+ Assert.Equal(expected.Color, actual.Color);
+ }
+
+ private static void AssertContainer(complex_fbs.Container expected,
complex_fbs.Container actual)
+ {
+ Assert.Equal(expected.Id, actual.Id);
+ Assert.Equal(expected.Status, actual.Status);
+ Assert.Equal(expected.Bytes, actual.Bytes);
+ Assert.Equal(expected.Numbers, actual.Numbers);
+ Assert.NotNull(expected.Scalars);
+ Assert.NotNull(actual.Scalars);
+ Assert.Equal(expected.Scalars.B, actual.Scalars.B);
+ Assert.Equal(expected.Scalars.Ub, actual.Scalars.Ub);
+ Assert.Equal(expected.Scalars.S, actual.Scalars.S);
+ Assert.Equal(expected.Scalars.Us, actual.Scalars.Us);
+ Assert.Equal(expected.Scalars.I, actual.Scalars.I);
+ Assert.Equal(expected.Scalars.Ui, actual.Scalars.Ui);
+ Assert.Equal(expected.Scalars.L, actual.Scalars.L);
+ Assert.Equal(expected.Scalars.Ul, actual.Scalars.Ul);
+ Assert.Equal(expected.Scalars.F, actual.Scalars.F);
+ Assert.Equal(expected.Scalars.D, actual.Scalars.D);
+ Assert.Equal(expected.Scalars.Ok, actual.Scalars.Ok);
+ Assert.Equal(expected.Names, actual.Names);
+ Assert.Equal(expected.Flags, actual.Flags);
+
+ 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);
+ }
+
+ private static void AssertTree(tree.TreeNode root)
+ {
+ Assert.Equal("root", root.Id);
+ Assert.Equal("root", root.Name);
+ Assert.Equal(3, root.Children.Count);
+
+ tree.TreeNode childAFirst = root.Children[0];
+ tree.TreeNode childASecond = root.Children[1];
+ tree.TreeNode childB = root.Children[2];
+
+ Assert.Equal("child-a", childAFirst.Id);
+ Assert.Equal("child-b", childB.Id);
+
+ Assert.Same(childAFirst, childASecond);
+ Assert.NotNull(childAFirst.Parent);
+ Assert.NotNull(childB.Parent);
+ Assert.Same(childB, childAFirst.Parent);
+ Assert.Same(childAFirst, childB.Parent);
+ }
+
+ private static void AssertGraph(graph.Graph graphValue)
+ {
+ Assert.Equal(2, graphValue.Nodes.Count);
+ Assert.Equal(2, graphValue.Edges.Count);
+
+ Dictionary<string, graph.Node> nodes = graphValue.Nodes.ToDictionary(n
=> n.Id, n => n);
+ Dictionary<string, graph.Edge> edges = graphValue.Edges.ToDictionary(e
=> e.Id, e => e);
+
+ Assert.True(nodes.ContainsKey("n1"));
+ Assert.True(nodes.ContainsKey("n2"));
+ Assert.True(edges.ContainsKey("e12"));
+ Assert.True(edges.ContainsKey("e21"));
+
+ graph.Edge edge12 = edges["e12"];
+ graph.Edge edge21 = edges["e21"];
+
+ Assert.NotNull(edge12.From);
+ Assert.NotNull(edge12.To);
+ Assert.NotNull(edge21.From);
+ Assert.NotNull(edge21.To);
+
+ Assert.Same(nodes["n1"], edge12.From);
+ Assert.Same(nodes["n2"], edge12.To);
+ Assert.Same(nodes["n2"], edge21.From);
+ Assert.Same(nodes["n1"], edge21.To);
+
+ Assert.Single(nodes["n1"].OutEdges);
+ Assert.Single(nodes["n1"].InEdges);
+ Assert.Single(nodes["n2"].OutEdges);
+ Assert.Single(nodes["n2"].InEdges);
+
+ Assert.Same(edge12, nodes["n1"].OutEdges[0]);
+ Assert.Same(edge21, nodes["n1"].InEdges[0]);
+ Assert.Same(edge21, nodes["n2"].OutEdges[0]);
+ Assert.Same(edge12, nodes["n2"].InEdges[0]);
+ }
+
+ private static void AssertRootHolder(root.MultiHolder expected,
root.MultiHolder actual)
+ {
+ Assert.NotNull(actual.Book);
+ Assert.NotNull(actual.Root);
+ Assert.NotNull(actual.Owner);
+ Assert.NotNull(expected.Book);
+ Assert.NotNull(expected.Root);
+ Assert.NotNull(expected.Owner);
+
+ AssertAddressBook(expected.Book, actual.Book);
+
+ Assert.Equal(expected.Root.Id, actual.Root.Id);
+ Assert.Equal(expected.Root.Name, actual.Root.Name);
+ Assert.Equal(expected.Root.Children.Count, actual.Root.Children.Count);
+
+ Assert.Equal(expected.Owner.Name, actual.Owner.Name);
+ Assert.Equal(expected.Owner.Id, actual.Owner.Id);
+ Assert.Equal(expected.Owner.Email, actual.Owner.Email);
+ Assert.Equal(expected.Owner.Tags, actual.Owner.Tags);
+ AssertMap(expected.Owner.Scores, actual.Owner.Scores);
+ }
+
+ private static bool TryAsBool(object? value, out bool result)
+ {
+ if (value is bool b)
+ {
+ result = b;
+ return true;
+ }
+
+ result = false;
+ return false;
+ }
+
+ private static bool TryAsString(object? value, out string? result)
+ {
+ if (value is string s)
+ {
+ result = s;
+ return true;
+ }
+
+ result = null;
+ return false;
+ }
+
+ private static bool TryAsDateOnly(object? value, out DateOnly result)
+ {
+ if (value is DateOnly date)
+ {
+ result = date;
+ return true;
+ }
+
+ result = default;
+ return false;
+ }
+
+ private static bool TryAsTimestamp(object? value, out DateTimeOffset
result)
+ {
+ switch (value)
+ {
+ case DateTimeOffset dto:
+ result = dto;
+ return true;
+ case DateTime dateTime:
+ result = new DateTimeOffset(DateTime.SpecifyKind(dateTime,
DateTimeKind.Utc));
+ return true;
+ default:
+ result = default;
+ return false;
+ }
+ }
+
+ private static bool TryAnyInnerName(object? value, out string? name)
+ {
+ switch (value)
+ {
+ case any_example.AnyInner inner:
+ name = inner.Name;
+ return true;
+ case any_example_pb.AnyInner innerPb:
+ name = innerPb.Name;
+ return true;
+ default:
+ name = null;
+ return false;
+ }
+ }
+
+ private static bool TryStringList(object? value, out List<string> result)
+ {
+ switch (value)
+ {
+ case List<string> strList:
+ result = [.. strList];
+ return true;
+ case IEnumerable<string> strEnumerable:
+ result = [.. strEnumerable];
+ return true;
+ case IEnumerable<object?> objEnumerable:
+ {
+ List<string> normalized = [];
+ foreach (object? item in objEnumerable)
+ {
+ if (item is not string text)
+ {
+ result = [];
+ return false;
+ }
+
+ normalized.Add(text);
+ }
+
+ result = normalized;
+ return true;
+ }
+ default:
+ result = [];
+ return false;
+ }
+ }
+
+ private static bool TryStringMap(object? value, out Dictionary<string,
string> result)
+ {
+ switch (value)
+ {
+ case Dictionary<string, string> map:
+ result = new Dictionary<string, string>(map);
+ return true;
+ case IReadOnlyDictionary<string, string> readonlyMap:
+ result = readonlyMap.ToDictionary(kv => kv.Key, kv =>
kv.Value);
+ return true;
+ case IEnumerable<KeyValuePair<object, object?>> objectPairs:
+ {
+ Dictionary<string, string> normalized = [];
+ foreach (KeyValuePair<object, object?> pair in objectPairs)
+ {
+ if (pair.Key is not string key || pair.Value is not string
val)
+ {
+ result = [];
+ return false;
+ }
+
+ normalized[key] = val;
+ }
+
+ result = normalized;
+ return true;
+ }
+ default:
+ result = [];
+ return false;
+ }
+ }
+
+ private static void AssertMap<TKey, TValue>(
+ IReadOnlyDictionary<TKey, TValue> expected,
+ IReadOnlyDictionary<TKey, TValue> actual)
+ where TKey : notnull
+ {
+ Assert.Equal(expected.Count, actual.Count);
+ foreach (KeyValuePair<TKey, TValue> pair in expected)
+ {
+ Assert.True(actual.TryGetValue(pair.Key, out TValue? value));
+ Assert.Equal(pair.Value, value);
+ }
+ }
+
+ private static void AssertNullableMap<TKey, TValue>(
+ IReadOnlyDictionary<TKey, TValue>? expected,
+ IReadOnlyDictionary<TKey, TValue>? actual)
+ where TKey : notnull
+ {
+ if (expected is null || actual is null)
+ {
+ Assert.Equal(expected is null, actual is null);
+ return;
+ }
+
+ AssertMap(expected, actual);
+ }
+}
diff --git a/integration_tests/idl_tests/generate_idl.py
b/integration_tests/idl_tests/generate_idl.py
index 046de53b3..f4c764791 100755
--- a/integration_tests/idl_tests/generate_idl.py
+++ b/integration_tests/idl_tests/generate_idl.py
@@ -46,6 +46,7 @@ LANG_OUTPUTS = {
"cpp": REPO_ROOT / "integration_tests/idl_tests/cpp/generated",
"go": REPO_ROOT / "integration_tests/idl_tests/go/generated",
"rust": REPO_ROOT / "integration_tests/idl_tests/rust/src/generated",
+ "csharp": REPO_ROOT /
"integration_tests/idl_tests/csharp/IdlTests/Generated",
}
GO_OUTPUT_OVERRIDES = {
diff --git a/integration_tests/idl_tests/run_csharp_tests.sh
b/integration_tests/idl_tests/run_csharp_tests.sh
new file mode 100755
index 000000000..4c32fe1d8
--- /dev/null
+++ b/integration_tests/idl_tests/run_csharp_tests.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+python "${SCRIPT_DIR}/generate_idl.py" --lang csharp
+
+TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/fory-csharp-idl-XXXXXX")"
+cleanup() {
+ rm -rf "${TMP_DIR}"
+}
+trap cleanup EXIT
+
+export DATA_FILE="${TMP_DIR}/addressbook.bin"
+export DATA_FILE_AUTO_ID="${TMP_DIR}/auto_id.bin"
+export DATA_FILE_PRIMITIVES="${TMP_DIR}/primitives.bin"
+export DATA_FILE_COLLECTION="${TMP_DIR}/collection.bin"
+export DATA_FILE_COLLECTION_UNION="${TMP_DIR}/collection_union.bin"
+export DATA_FILE_COLLECTION_ARRAY="${TMP_DIR}/collection_array.bin"
+export DATA_FILE_COLLECTION_ARRAY_UNION="${TMP_DIR}/collection_array_union.bin"
+export DATA_FILE_OPTIONAL_TYPES="${TMP_DIR}/optional_types.bin"
+export DATA_FILE_ANY="${TMP_DIR}/any.bin"
+export DATA_FILE_ANY_PROTO="${TMP_DIR}/any_proto.bin"
+export DATA_FILE_TREE="${TMP_DIR}/tree.bin"
+export DATA_FILE_GRAPH="${TMP_DIR}/graph.bin"
+export DATA_FILE_FLATBUFFERS_MONSTER="${TMP_DIR}/flatbuffers_monster.bin"
+export DATA_FILE_FLATBUFFERS_TEST2="${TMP_DIR}/flatbuffers_test2.bin"
+export DATA_FILE_ROOT="${TMP_DIR}/root.bin"
+
+cd "${SCRIPT_DIR}/csharp/IdlTests"
+ENABLE_FORY_DEBUG_OUTPUT=1 dotnet test -c Release
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]