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 5ac50690a feat(c#): add max depth and code lint support (#3428)
5ac50690a is described below
commit 5ac50690a08b47038c95c43ec4319e68e08b9352
Author: Shawn Yang <[email protected]>
AuthorDate: Fri Feb 27 00:06:54 2026 +0800
feat(c#): add max depth and code lint support (#3428)
## Why?
- Add a hard limit for dynamic object nesting depth during
deserialization to prevent unbounded recursive reads.
- Simplify the C# dynamic payload API surface and align C# formatting
checks with CI.
## What does this PR do?
- Adds dynamic read-depth tracking/enforcement in `ReadContext` and
dynamic any deserialization (`AnySerializer`), wired to
`Config.MaxDepth`.
- Removes `SerializeObject` / `DeserializeObject` from `Fory` and
`ThreadSafeFory`; dynamic payloads now use `Serialize<object?>` /
`Deserialize<object?>`.
- Updates C# runtime tests, xlang peer test program, README, and C#
guides to the generic object API.
- Adds depth-limit coverage tests
(`DynamicObjectReadDepthExceededThrows`,
`DynamicObjectReadDepthWithinLimitRoundTrip`).
- Adds C# formatting support/checks via `csharp/.editorconfig`,
`ci/format.sh`, and CI workflow steps (`dotnet format
--verify-no-changes`).
- Adds analyzer release metadata files to `Fory.Generator` and includes
them in the project file.
## Related issues
#3387
## Does this PR introduce any user-facing change?
- [x] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
N/A
---
.github/workflows/ci.yml | 10 +
AGENTS.md | 39 +++-
ci/format.sh | 45 ++++-
csharp/.editorconfig | 51 +++++
csharp/README.md | 4 +-
.../src/Fory.Generator/AnalyzerReleases.Shipped.md | 6 +
.../Fory.Generator/AnalyzerReleases.Unshipped.md | 7 +
csharp/src/Fory.Generator/Fory.Generator.csproj | 5 +
csharp/src/Fory.Generator/ForyObjectGenerator.cs | 8 +-
csharp/src/Fory/AnySerializer.cs | 72 ++++---
csharp/src/Fory/Attributes.cs | 21 ++
csharp/src/Fory/Config.cs | 41 +++-
csharp/src/Fory/Context.cs | 30 ++-
csharp/src/Fory/FieldSkipper.cs | 40 ++--
csharp/src/Fory/Fory.cs | 159 +++++++++-------
csharp/src/Fory/ForyException.cs | 53 +++++-
csharp/src/Fory/OptionalSerializer.cs | 30 +--
csharp/src/Fory/PrimitiveDictionarySerializers.cs | 2 +-
csharp/src/Fory/Serializer.cs | 63 ++++--
csharp/src/Fory/StringSerializer.cs | 6 +-
csharp/src/Fory/ThreadSafeFory.cs | 84 ++++++--
csharp/src/Fory/TypeResolver.cs | 212 ++++++++++-----------
csharp/tests/Fory.Tests/ForyRuntimeTests.cs | 47 +++--
csharp/tests/Fory.XlangPeer/Program.cs | 136 ++++++-------
docs/guide/csharp/basic-serialization.md | 12 +-
docs/guide/csharp/index.md | 4 +-
docs/guide/csharp/supported-types.md | 2 +-
python/setup.py | 81 +++++++-
28 files changed, 885 insertions(+), 385 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b0db1328c..21b99a3a5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -167,6 +167,12 @@ jobs:
run: |
cd csharp
dotnet test tests/Fory.Tests/Fory.Tests.csproj -c Release --no-build
+ - name: Verify C# format
+ run: |
+ cd csharp
+ dotnet format Fory.sln --verify-no-changes \
+ --exclude src/Fory.Generator/AnalyzerReleases.Shipped.md \
+ --exclude src/Fory.Generator/AnalyzerReleases.Unshipped.md
csharp_xlang:
name: C# Xlang Test
@@ -907,6 +913,10 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: 3.8
+ - name: Set up .NET 8
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "8.0.x"
- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
diff --git a/AGENTS.md b/AGENTS.md
index db711837a..398237fd0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -63,6 +63,42 @@ mvn -T16 test
mvn -T16 test -Dtest=org.apache.fory.TestClass#testMethod
```
+### C# Development
+
+- All dotnet commands must be executed within the `csharp` directory.
+- All changes to `csharp` must pass formatting and tests.
+- Fory C# requires .NET SDK `8.0+` and C# `12+`.
+- Use `dotnet format` to keep C# code style consistent.
+
+```bash
+# Restore
+dotnet restore Fory.sln
+
+# Build
+dotnet build Fory.sln -c Release --no-restore
+
+# Run tests
+dotnet test Fory.sln -c Release
+
+# Run specific test
+dotnet test tests/Fory.Tests/Fory.Tests.csproj -c Release --filter
"FullyQualifiedName~ForyRuntimeTests.DynamicObjectReadDepthExceededThrows"
+
+# Format code
+dotnet format Fory.sln
+
+# Format check
+dotnet format Fory.sln --verify-no-changes
+```
+
+Run C# xlang tests:
+
+```bash
+cd java
+mvn -T16 install -DskipTests
+cd fory-core
+FORY_CSHARP_JAVA_CI=1 ENABLE_FORY_DEBUG_OUTPUT=1 mvn -T16 test
-Dtest=org.apache.fory.xlang.CSharpXlangTest
+```
+
### C++ Development
- All commands must be executed within the `cpp` directory.
@@ -389,6 +425,7 @@ The `origin` points to forked repository instead of the
official repository.
- **Language Implementations**:
- `java/`: Java implementation (maven-based, multi-module)
+ - `csharp/`: C# implementation (.NET SDK + source generator)
- `python/`: Python implementation (pip/setuptools + bazel)
- `cpp/`: C++ implementation (bazel-based)
- `go/`: Go implementation (go modules)
@@ -609,7 +646,7 @@ Fory rust provides macro-based serialization and
deserialization. Fory rust cons
- **Unit Tests**: Focus on internal behavior verification
- **Integration Tests**: Use `integration_tests/` for cross-language
compatibility
-- **Language alignment and protocol compatibility**: Run
`org.apache.fory.xlang.CPPXlangTest`, `org.apache.fory.xlang.RustXlangTest`,
`org.apache.fory.xlang.GoXlangTest`, and
`org.apache.fory.xlang.PythonXlangTest` when changing xlang or type mapping
behavior
+- **Language alignment and protocol compatibility**: Run
`org.apache.fory.xlang.CPPXlangTest`, `org.apache.fory.xlang.CSharpXlangTest`,
`org.apache.fory.xlang.RustXlangTest`, `org.apache.fory.xlang.GoXlangTest`, and
`org.apache.fory.xlang.PythonXlangTest` when changing xlang or type mapping
behavior
- **Performance Tests**: Include benchmarks for performance-critical changes
### Documentation Requirements
diff --git a/ci/format.sh b/ci/format.sh
index 0545ea231..877b9845e 100755
--- a/ci/format.sh
+++ b/ci/format.sh
@@ -213,6 +213,21 @@ format_go() {
fi
}
+format_csharp() {
+ echo "$(date)" "dotnet format C# files...."
+ if command -v dotnet >/dev/null; then
+ pushd "$ROOT/csharp"
+ dotnet format Fory.sln \
+ --exclude src/Fory.Generator/AnalyzerReleases.Shipped.md \
+ --exclude src/Fory.Generator/AnalyzerReleases.Unshipped.md
+ popd
+ echo "$(date)" "C# formatting done!"
+ else
+ echo "ERROR: dotnet is not installed! Install .NET SDK from
https://dotnet.microsoft.com/download"
+ exit 1
+ fi
+}
+
format_swift() {
echo "$(date)" "SwiftLint check Swift files...."
if command -v swiftlint >/dev/null; then
@@ -251,6 +266,11 @@ format_all() {
git ls-files -- '*.go' "${GIT_LS_EXCLUDES[@]}" | xargs -P 5 gofmt -w
fi
+ echo "$(date)" "format csharp...."
+ if command -v dotnet >/dev/null; then
+ format_csharp
+ fi
+
echo "$(date)" "lint swift...."
format_swift
@@ -295,6 +315,14 @@ format_changed() {
fi
fi
+ if command -v dotnet >/dev/null; then
+ local csharp_changed
+ csharp_changed="$(git diff --name-only --diff-filter=ACRM "$MERGEBASE"
-- csharp || true)"
+ if [ -n "$csharp_changed" ]; then
+ format_csharp
+ fi
+ fi
+
if which node >/dev/null; then
pushd "$ROOT"
if ! git diff --diff-filter=ACRM --quiet --exit-code "$MERGEBASE" --
'*.ts' &>/dev/null; then
@@ -303,8 +331,17 @@ format_changed() {
fi
# Install prettier globally
npm install -g prettier
- # Fix markdown files
- prettier --write "**/*.md"
+ # Fix markdown files except analyzer release tracking files.
+ # Exclude symlinks (for example CLAUDE.md) because prettier fails on
explicitly passed symlink paths.
+ git ls-files -z -- '*.md' \
+ ':!:csharp/src/Fory.Generator/AnalyzerReleases.Shipped.md' \
+ ':!:csharp/src/Fory.Generator/AnalyzerReleases.Unshipped.md' \
+ | while IFS= read -r -d '' file; do
+ if [ ! -L "$file" ]; then
+ printf '%s\0' "$file"
+ fi
+ done \
+ | xargs -0 prettier --write
popd
fi
@@ -337,6 +374,10 @@ elif [ "${1-}" == '--go' ]; then
format_go
elif [ "${1-}" == '--swift' ]; then
format_swift
+elif [ "${1-}" == '--csharp' ]; then
+ format_csharp
+elif [ "${1-}" == '--swift' ]; then
+ format_swift
else
# Add the origin remote if it doesn't exist
if ! git remote -v | grep -q origin; then
diff --git a/csharp/.editorconfig b/csharp/.editorconfig
new file mode 100644
index 000000000..95f4897a3
--- /dev/null
+++ b/csharp/.editorconfig
@@ -0,0 +1,51 @@
+# 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.
+
+[*.{cs,csx}]
+indent_size = 4
+tab_width = 4
+
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = false
+
+csharp_new_line_before_open_brace = all
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = one_less_than_current
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = false
+
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_method_call_parentheses = false
+csharp_space_between_empty_method_declaration_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
diff --git a/csharp/README.md b/csharp/README.md
index 12bcdecef..d419341ce 100644
--- a/csharp/README.md
+++ b/csharp/README.md
@@ -154,8 +154,8 @@ Dictionary<object, object?> map = new()
[true] = null,
};
-byte[] payload = fory.SerializeObject(map);
-object? decoded = fory.DeserializeObject(payload);
+byte[] payload = fory.Serialize<object?>(map);
+object? decoded = fory.Deserialize<object?>(payload);
```
### 5. Thread-Safe Runtime
diff --git a/csharp/src/Fory.Generator/AnalyzerReleases.Shipped.md
b/csharp/src/Fory.Generator/AnalyzerReleases.Shipped.md
new file mode 100644
index 000000000..ed666bef5
--- /dev/null
+++ b/csharp/src/Fory.Generator/AnalyzerReleases.Shipped.md
@@ -0,0 +1,6 @@
+## Release 0.0.0
+
+### New Rules
+
+| Rule ID | Category | Severity | Notes |
+| ------- | -------- | -------- | ----- |
diff --git a/csharp/src/Fory.Generator/AnalyzerReleases.Unshipped.md
b/csharp/src/Fory.Generator/AnalyzerReleases.Unshipped.md
new file mode 100644
index 000000000..b1d4b0610
--- /dev/null
+++ b/csharp/src/Fory.Generator/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,7 @@
+### New Rules
+
+| Rule ID | Category | Severity | Notes |
+| ------- | -------- | -------- | ----- |
+| FORY001 | Fory | Error | Generic types are not supported by ForyObject
generator |
+| FORY002 | Fory | Error | Missing parameterless constructor |
+| FORY003 | Fory | Error | Unsupported Field encoding |
diff --git a/csharp/src/Fory.Generator/Fory.Generator.csproj
b/csharp/src/Fory.Generator/Fory.Generator.csproj
index 48864e20c..265d94481 100644
--- a/csharp/src/Fory.Generator/Fory.Generator.csproj
+++ b/csharp/src/Fory.Generator/Fory.Generator.csproj
@@ -9,6 +9,11 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
+ <ItemGroup>
+ <AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
+ <AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
+ </ItemGroup>
+
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers"
Version="3.11.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0"
PrivateAssets="all" />
diff --git a/csharp/src/Fory.Generator/ForyObjectGenerator.cs
b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
index 808e6ef6b..ea8e76244 100644
--- a/csharp/src/Fory.Generator/ForyObjectGenerator.cs
+++ b/csharp/src/Fory.Generator/ForyObjectGenerator.cs
@@ -33,7 +33,7 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
private static readonly DiagnosticDescriptor GenericTypeNotSupported = new(
id: "FORY001",
title: "Generic types are not supported by ForyObject generator",
- messageFormat: "Type '{0}' is generic and is not supported by
[ForyObject].",
+ messageFormat: "Type '{0}' is generic and is not supported by
[ForyObject]",
category: "Fory",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
@@ -41,7 +41,7 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
private static readonly DiagnosticDescriptor MissingCtor = new(
id: "FORY002",
title: "Missing parameterless constructor",
- messageFormat: "Class '{0}' must declare an accessible parameterless
constructor for [ForyObject].",
+ messageFormat: "Class '{0}' must declare an accessible parameterless
constructor for [ForyObject]",
category: "Fory",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
@@ -49,7 +49,7 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
private static readonly DiagnosticDescriptor UnsupportedEncoding = new(
id: "FORY003",
title: "Unsupported Field encoding",
- messageFormat: "Member '{0}' uses unsupported [Field] encoding for
type '{1}'.",
+ messageFormat: "Member '{0}' uses unsupported [Field] encoding for
type '{1}'",
category: "Fory",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true);
@@ -505,7 +505,7 @@ public sealed class ForyObjectGenerator :
IIncrementalGenerator
sb.AppendLine($" global::Apache.Fory.TypeInfo
{typeInfoVar};");
sb.AppendLine($" if (__Fory{cacheId}DictRuntimeType ==
{runtimeTypeVar} && __Fory{cacheId}DictTypeInfo is not null)");
sb.AppendLine(" {");
- sb.AppendLine($" {typeInfoVar} =
__Fory{cacheId}DictTypeInfo;");
+ sb.AppendLine($" {typeInfoVar} =
__Fory{cacheId}DictTypeInfo;");
sb.AppendLine(" }");
sb.AppendLine(" else");
sb.AppendLine(" {");
diff --git a/csharp/src/Fory/AnySerializer.cs b/csharp/src/Fory/AnySerializer.cs
index 77b6e80b2..d73a80da9 100644
--- a/csharp/src/Fory/AnySerializer.cs
+++ b/csharp/src/Fory/AnySerializer.cs
@@ -88,29 +88,25 @@ public sealed class DynamicAnyObjectSerializer :
Serializer<object?>
case RefFlag.Null:
return null;
case RefFlag.Ref:
- {
- uint refId = context.Reader.ReadVarUInt32();
- return context.RefReader.ReadRefValue(refId);
- }
- case RefFlag.RefValue:
- {
- uint reservedRefId = context.RefReader.ReserveRefId();
- context.RefReader.PushPendingReference(reservedRefId);
- if (readTypeInfo)
{
- ReadAnyTypeInfo(context);
+ uint refId = context.Reader.ReadVarUInt32();
+ return context.RefReader.ReadRefValue(refId);
}
-
- object? value = ReadData(context);
- if (readTypeInfo)
+ case RefFlag.RefValue:
{
- context.ClearDynamicTypeInfo(typeof(object));
+ uint reservedRefId = context.RefReader.ReserveRefId();
+ context.RefReader.PushPendingReference(reservedRefId);
+ try
+ {
+ object? value = ReadNonNullDynamicAny(context,
readTypeInfo);
+
context.RefReader.FinishPendingReferenceIfNeeded(value);
+ return value;
+ }
+ finally
+ {
+ context.RefReader.PopPendingReference();
+ }
}
-
- context.RefReader.FinishPendingReferenceIfNeeded(value);
- context.RefReader.PopPendingReference();
- return value;
- }
case RefFlag.NotNullValue:
break;
default:
@@ -118,18 +114,7 @@ public sealed class DynamicAnyObjectSerializer :
Serializer<object?>
}
}
- if (readTypeInfo)
- {
- ReadAnyTypeInfo(context);
- }
-
- object? result = ReadData(context);
- if (readTypeInfo)
- {
- context.ClearDynamicTypeInfo(typeof(object));
- }
-
- return result;
+ return ReadNonNullDynamicAny(context, readTypeInfo);
}
private static bool AnyValueIsReferenceTrackable(object value,
TypeResolver typeResolver)
@@ -143,6 +128,31 @@ public sealed class DynamicAnyObjectSerializer :
Serializer<object?>
DynamicTypeInfo typeInfo =
context.TypeResolver.ReadDynamicTypeInfo(context);
context.SetDynamicTypeInfo(typeof(object), typeInfo);
}
+
+ private object? ReadNonNullDynamicAny(ReadContext context, bool
readTypeInfo)
+ {
+ context.IncreaseDynamicReadDepth();
+ bool loadedDynamicTypeInfo = false;
+ try
+ {
+ if (readTypeInfo)
+ {
+ ReadAnyTypeInfo(context);
+ loadedDynamicTypeInfo = true;
+ }
+
+ return ReadData(context);
+ }
+ finally
+ {
+ if (loadedDynamicTypeInfo)
+ {
+ context.ClearDynamicTypeInfo(typeof(object));
+ }
+
+ context.DecreaseDynamicReadDepth();
+ }
+ }
}
public static class DynamicAnyCodec
diff --git a/csharp/src/Fory/Attributes.cs b/csharp/src/Fory/Attributes.cs
index 84fe5dc38..2d6749fd9 100644
--- a/csharp/src/Fory/Attributes.cs
+++ b/csharp/src/Fory/Attributes.cs
@@ -17,20 +17,41 @@
namespace Apache.Fory;
+/// <summary>
+/// Marks a class, struct, or enum as a generated Fory object type.
+/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct |
AttributeTargets.Enum)]
public sealed class ForyObjectAttribute : Attribute
{
}
+/// <summary>
+/// Specifies field-level integer/number encoding strategy for generated
serializers.
+/// </summary>
public enum FieldEncoding
{
+ /// <summary>
+ /// Variable-length integer encoding.
+ /// </summary>
Varint,
+ /// <summary>
+ /// Fixed-width integer encoding.
+ /// </summary>
Fixed,
+ /// <summary>
+ /// Tagged field encoding for schema-evolution scenarios.
+ /// </summary>
Tagged,
}
+/// <summary>
+/// Overrides generated serializer behavior for a field or property.
+/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class FieldAttribute : Attribute
{
+ /// <summary>
+ /// Gets or sets the field encoding strategy used by generated serializers.
+ /// </summary>
public FieldEncoding Encoding { get; set; } = FieldEncoding.Varint;
}
diff --git a/csharp/src/Fory/Config.cs b/csharp/src/Fory/Config.cs
index 5cde5d848..4745053e0 100644
--- a/csharp/src/Fory/Config.cs
+++ b/csharp/src/Fory/Config.cs
@@ -17,6 +17,14 @@
namespace Apache.Fory;
+/// <summary>
+/// Immutable runtime configuration used by <see cref="Fory"/> and <see
cref="ThreadSafeFory"/>.
+/// </summary>
+/// <param name="Xlang">Whether cross-language protocol mode is
enabled.</param>
+/// <param name="TrackRef">Whether shared and circular reference tracking is
enabled.</param>
+/// <param name="Compatible">Whether schema-compatible mode is enabled.</param>
+/// <param name="CheckStructVersion">Whether generated struct schema hash
checks are enforced.</param>
+/// <param name="MaxDepth">Maximum allowed nesting depth for dynamic object
payload reads.</param>
public sealed record Config(
bool Xlang = true,
bool TrackRef = false,
@@ -24,6 +32,9 @@ public sealed record Config(
bool CheckStructVersion = false,
int MaxDepth = 20);
+/// <summary>
+/// Fluent builder for creating <see cref="Fory"/> and <see
cref="ThreadSafeFory"/> runtimes.
+/// </summary>
public sealed class ForyBuilder
{
private bool _xlang = true;
@@ -32,30 +43,56 @@ public sealed class ForyBuilder
private bool _checkStructVersion;
private int _maxDepth = 20;
+ /// <summary>
+ /// Enables or disables cross-language protocol mode.
+ /// </summary>
+ /// <param name="enabled">Whether to enable cross-language mode. Defaults
to <c>true</c>.</param>
+ /// <returns>The same builder instance.</returns>
public ForyBuilder Xlang(bool enabled = true)
{
_xlang = enabled;
return this;
}
+ /// <summary>
+ /// Enables or disables reference tracking for shared and circular object
graphs.
+ /// </summary>
+ /// <param name="enabled">Whether to enable reference tracking. Defaults
to <c>false</c>.</param>
+ /// <returns>The same builder instance.</returns>
public ForyBuilder TrackRef(bool enabled = false)
{
_trackRef = enabled;
return this;
}
+ /// <summary>
+ /// Enables or disables schema-compatible mode for schema evolution
scenarios.
+ /// </summary>
+ /// <param name="enabled">Whether to enable compatible mode. Defaults to
<c>false</c>.</param>
+ /// <returns>The same builder instance.</returns>
public ForyBuilder Compatible(bool enabled = false)
{
_compatible = enabled;
return this;
}
+ /// <summary>
+ /// Enables or disables generated struct schema hash validation.
+ /// </summary>
+ /// <param name="enabled">Whether to enforce struct version checks.
Defaults to <c>false</c>.</param>
+ /// <returns>The same builder instance.</returns>
public ForyBuilder CheckStructVersion(bool enabled = false)
{
_checkStructVersion = enabled;
return this;
}
+ /// <summary>
+ /// Sets the maximum supported dynamic object nesting depth during
deserialization.
+ /// </summary>
+ /// <param name="value">Depth limit. Must be greater than <c>0</c>.</param>
+ /// <returns>The same builder instance.</returns>
+ /// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref
name="value"/> is less than or equal to <c>0</c>.</exception>
public ForyBuilder MaxDepth(int value)
{
if (value <= 0)
@@ -78,8 +115,9 @@ public sealed class ForyBuilder
}
/// <summary>
- /// Builds a single-thread <see cref="Fory"/> instance.
+ /// Builds a single-threaded <see cref="Fory"/> instance.
/// </summary>
+ /// <returns>A configured <see cref="Fory"/> runtime.</returns>
public Fory Build()
{
return new Fory(BuildConfig());
@@ -88,6 +126,7 @@ public sealed class ForyBuilder
/// <summary>
/// Builds a multi-thread-safe wrapper that keeps one <see cref="Fory"/>
per thread.
/// </summary>
+ /// <returns>A configured <see cref="ThreadSafeFory"/> runtime.</returns>
public ThreadSafeFory BuildThreadSafe()
{
return new ThreadSafeFory(BuildConfig());
diff --git a/csharp/src/Fory/Context.cs b/csharp/src/Fory/Context.cs
index f07466dee..990d89674 100644
--- a/csharp/src/Fory/Context.cs
+++ b/csharp/src/Fory/Context.cs
@@ -229,6 +229,8 @@ public sealed class ReadContext
private readonly Dictionary<Type, List<TypeMeta>>
_pendingCompatibleTypeMeta = [];
private readonly Dictionary<Type, DynamicTypeInfo> _pendingDynamicTypeInfo
= [];
private readonly Dictionary<CanonicalReferenceSignature,
List<CanonicalReferenceEntry>> _canonicalReferenceCache = [];
+ private readonly int _maxDynamicReadDepth;
+ private int _currentDynamicReadDepth;
public ReadContext(
ByteReader reader,
@@ -237,8 +239,14 @@ public sealed class ReadContext
bool compatible = false,
bool checkStructVersion = false,
CompatibleTypeDefReadState? compatibleTypeDefState = null,
- MetaStringReadState? metaStringReadState = null)
+ MetaStringReadState? metaStringReadState = null,
+ int maxDynamicReadDepth = 20)
{
+ if (maxDynamicReadDepth <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(maxDynamicReadDepth),
"MaxDepth must be greater than 0.");
+ }
+
Reader = reader;
TypeResolver = typeResolver;
TrackRef = trackRef;
@@ -247,6 +255,7 @@ public sealed class ReadContext
RefReader = new RefReader();
CompatibleTypeDefState = compatibleTypeDefState ?? new
CompatibleTypeDefReadState();
MetaStringReadState = metaStringReadState ?? new MetaStringReadState();
+ _maxDynamicReadDepth = maxDynamicReadDepth;
}
public ByteReader Reader { get; private set; }
@@ -322,6 +331,24 @@ public sealed class ReadContext
_pendingDynamicTypeInfo.Remove(type);
}
+ public void IncreaseDynamicReadDepth()
+ {
+ _currentDynamicReadDepth += 1;
+ if (_currentDynamicReadDepth > _maxDynamicReadDepth)
+ {
+ throw new InvalidDataException(
+ $"maximum dynamic object nesting depth
({_maxDynamicReadDepth}) exceeded. current depth: {_currentDynamicReadDepth}");
+ }
+ }
+
+ public void DecreaseDynamicReadDepth()
+ {
+ if (_currentDynamicReadDepth > 0)
+ {
+ _currentDynamicReadDepth -= 1;
+ }
+ }
+
public T CanonicalizeNonTrackingReference<T>(T value, int start, int end)
{
if (!TrackRef || end <= start || value is null || value is not object
obj)
@@ -361,6 +388,7 @@ public sealed class ReadContext
_pendingCompatibleTypeMeta.Clear();
_pendingDynamicTypeInfo.Clear();
_canonicalReferenceCache.Clear();
+ _currentDynamicReadDepth = 0;
}
public void Reset()
diff --git a/csharp/src/Fory/FieldSkipper.cs b/csharp/src/Fory/FieldSkipper.cs
index f5ccae21b..a2549f7e1 100644
--- a/csharp/src/Fory/FieldSkipper.cs
+++ b/csharp/src/Fory/FieldSkipper.cs
@@ -73,34 +73,34 @@ public static class FieldSkipper
case (uint)TypeId.String:
return
context.TypeResolver.GetSerializer<string>().Read(context, refMode, false);
case (uint)TypeId.List:
- {
- if (fieldType.Generics.Count != 1 ||
fieldType.Generics[0].TypeId != (uint)TypeId.String)
{
- throw new InvalidDataException("unsupported compatible
list element type");
- }
+ if (fieldType.Generics.Count != 1 ||
fieldType.Generics[0].TypeId != (uint)TypeId.String)
+ {
+ throw new InvalidDataException("unsupported compatible
list element type");
+ }
- return
context.TypeResolver.GetSerializer<List<string>>().Read(context, refMode,
false);
- }
+ return
context.TypeResolver.GetSerializer<List<string>>().Read(context, refMode,
false);
+ }
case (uint)TypeId.Set:
- {
- if (fieldType.Generics.Count != 1 ||
fieldType.Generics[0].TypeId != (uint)TypeId.String)
{
- throw new InvalidDataException("unsupported compatible set
element type");
- }
+ if (fieldType.Generics.Count != 1 ||
fieldType.Generics[0].TypeId != (uint)TypeId.String)
+ {
+ throw new InvalidDataException("unsupported compatible
set element type");
+ }
- return
context.TypeResolver.GetSerializer<HashSet<string>>().Read(context, refMode,
false);
- }
+ return
context.TypeResolver.GetSerializer<HashSet<string>>().Read(context, refMode,
false);
+ }
case (uint)TypeId.Map:
- {
- if (fieldType.Generics.Count != 2 ||
- fieldType.Generics[0].TypeId != (uint)TypeId.String ||
- fieldType.Generics[1].TypeId != (uint)TypeId.String)
{
- throw new InvalidDataException("unsupported compatible map
key/value type");
- }
+ if (fieldType.Generics.Count != 2 ||
+ fieldType.Generics[0].TypeId != (uint)TypeId.String ||
+ fieldType.Generics[1].TypeId != (uint)TypeId.String)
+ {
+ throw new InvalidDataException("unsupported compatible
map key/value type");
+ }
- return context.TypeResolver.GetSerializer<Dictionary<string,
string>>().Read(context, refMode, false);
- }
+ return
context.TypeResolver.GetSerializer<Dictionary<string, string>>().Read(context,
refMode, false);
+ }
case (uint)TypeId.Enum:
return ReadEnumOrdinal(context, refMode);
case (uint)TypeId.Union:
diff --git a/csharp/src/Fory/Fory.cs b/csharp/src/Fory/Fory.cs
index 412c17c61..d428ec2bc 100644
--- a/csharp/src/Fory/Fory.cs
+++ b/csharp/src/Fory/Fory.cs
@@ -49,34 +49,68 @@ public sealed class Fory
Config.Compatible,
Config.CheckStructVersion,
new CompatibleTypeDefReadState(),
- new MetaStringReadState());
+ new MetaStringReadState(),
+ Config.MaxDepth);
}
+ /// <summary>
+ /// Gets the immutable runtime configuration.
+ /// </summary>
public Config Config { get; }
+ /// <summary>
+ /// Creates a new <see cref="ForyBuilder"/> for configuring and building
runtimes.
+ /// </summary>
+ /// <returns>A new builder instance.</returns>
public static ForyBuilder Builder()
{
return new ForyBuilder();
}
+ /// <summary>
+ /// Registers a user type by numeric type identifier.
+ /// </summary>
+ /// <typeparam name="T">Type to register.</typeparam>
+ /// <param name="typeId">Numeric type identifier used on the wire.</param>
+ /// <returns>The same runtime instance.</returns>
public Fory Register<T>(uint typeId)
{
_typeResolver.Register(typeof(T), typeId);
return this;
}
+ /// <summary>
+ /// Registers a user type by name using an empty namespace.
+ /// </summary>
+ /// <typeparam name="T">Type to register.</typeparam>
+ /// <param name="typeName">Type name used on the wire.</param>
+ /// <returns>The same runtime instance.</returns>
public Fory Register<T>(string typeName)
{
_typeResolver.Register(typeof(T), string.Empty, typeName);
return this;
}
+ /// <summary>
+ /// Registers a user type by namespace and name.
+ /// </summary>
+ /// <typeparam name="T">Type to register.</typeparam>
+ /// <param name="typeNamespace">Namespace used on the wire.</param>
+ /// <param name="typeName">Type name used on the wire.</param>
+ /// <returns>The same runtime instance.</returns>
public Fory Register<T>(string typeNamespace, string typeName)
{
_typeResolver.Register(typeof(T), typeNamespace, typeName);
return this;
}
+ /// <summary>
+ /// Registers a user type by numeric type identifier with a custom
serializer.
+ /// </summary>
+ /// <typeparam name="T">Type to register.</typeparam>
+ /// <typeparam name="TSerializer">Serializer implementation used for
<typeparamref name="T"/>.</typeparam>
+ /// <param name="typeId">Numeric type identifier used on the wire.</param>
+ /// <returns>The same runtime instance.</returns>
public Fory Register<T, TSerializer>(uint typeId)
where TSerializer : Serializer<T>, new()
{
@@ -85,6 +119,14 @@ public sealed class Fory
return this;
}
+ /// <summary>
+ /// Registers a user type by namespace and name with a custom serializer.
+ /// </summary>
+ /// <typeparam name="T">Type to register.</typeparam>
+ /// <typeparam name="TSerializer">Serializer implementation used for
<typeparamref name="T"/>.</typeparam>
+ /// <param name="typeNamespace">Namespace used on the wire.</param>
+ /// <param name="typeName">Type name used on the wire.</param>
+ /// <returns>The same runtime instance.</returns>
public Fory Register<T, TSerializer>(string typeNamespace, string typeName)
where TSerializer : Serializer<T>, new()
{
@@ -93,6 +135,12 @@ public sealed class Fory
return this;
}
+ /// <summary>
+ /// Serializes a value into a new byte array containing one Fory frame.
+ /// </summary>
+ /// <typeparam name="T">Value type.</typeparam>
+ /// <param name="value">Value to serialize.</param>
+ /// <returns>Serialized bytes.</returns>
public byte[] Serialize<T>(in T value)
{
ByteWriter writer = _writeContext.Writer;
@@ -112,12 +160,25 @@ public sealed class Fory
return writer.ToArray();
}
+ /// <summary>
+ /// Serializes a value and writes one Fory frame into the provided buffer
writer.
+ /// </summary>
+ /// <typeparam name="T">Value type.</typeparam>
+ /// <param name="output">Destination writer.</param>
+ /// <param name="value">Value to serialize.</param>
public void Serialize<T>(IBufferWriter<byte> output, in T value)
{
byte[] payload = Serialize(value);
output.Write(payload);
}
+ /// <summary>
+ /// Deserializes a value from one Fory frame in the provided span.
+ /// </summary>
+ /// <typeparam name="T">Target type.</typeparam>
+ /// <param name="payload">Serialized bytes containing exactly one
frame.</param>
+ /// <returns>Deserialized value.</returns>
+ /// <exception cref="InvalidDataException">Thrown when trailing bytes
remain after decoding.</exception>
public T Deserialize<T>(ReadOnlySpan<byte> payload)
{
ByteReader reader = _readContext.Reader;
@@ -131,6 +192,13 @@ public sealed class Fory
return value;
}
+ /// <summary>
+ /// Deserializes a value from one Fory frame in the provided byte array.
+ /// </summary>
+ /// <typeparam name="T">Target type.</typeparam>
+ /// <param name="payload">Serialized bytes containing exactly one
frame.</param>
+ /// <returns>Deserialized value.</returns>
+ /// <exception cref="InvalidDataException">Thrown when trailing bytes
remain after decoding.</exception>
public T Deserialize<T>(byte[] payload)
{
ByteReader reader = _readContext.Reader;
@@ -144,6 +212,12 @@ public sealed class Fory
return value;
}
+ /// <summary>
+ /// Deserializes a value from the head of a framed sequence and advances
the sequence.
+ /// </summary>
+ /// <typeparam name="T">Target type.</typeparam>
+ /// <param name="payload">Input sequence. On success, sliced past the
consumed frame.</param>
+ /// <returns>Deserialized value.</returns>
public T Deserialize<T>(ref ReadOnlySequence<byte> payload)
{
byte[] bytes = payload.ToArray();
@@ -154,65 +228,12 @@ public sealed class Fory
return value;
}
- public byte[] SerializeObject(object? value)
- {
- ByteWriter writer = _writeContext.Writer;
- writer.Reset();
- bool isNone = value is null;
- WriteHead(writer, isNone);
- if (!isNone)
- {
- _writeContext.ResetFor(writer);
- RefMode refMode = Config.TrackRef ? RefMode.Tracking :
RefMode.NullOnly;
- DynamicAnyCodec.WriteAny(_writeContext, value, refMode, true,
false);
- _writeContext.ResetObjectState();
- }
-
- return writer.ToArray();
- }
-
- public void SerializeObject(IBufferWriter<byte> output, object? value)
- {
- byte[] payload = SerializeObject(value);
- output.Write(payload);
- }
-
- public object? DeserializeObject(ReadOnlySpan<byte> payload)
- {
- ByteReader reader = _readContext.Reader;
- reader.Reset(payload);
- object? value = DeserializeObjectFromReader(reader);
- if (reader.Remaining != 0)
- {
- throw new InvalidDataException("unexpected trailing bytes after
deserializing dynamic object");
- }
-
- return value;
- }
-
- public object? DeserializeObject(byte[] payload)
- {
- ByteReader reader = _readContext.Reader;
- reader.Reset(payload);
- object? value = DeserializeObjectFromReader(reader);
- if (reader.Remaining != 0)
- {
- throw new InvalidDataException("unexpected trailing bytes after
deserializing dynamic object");
- }
-
- return value;
- }
-
- public object? DeserializeObject(ref ReadOnlySequence<byte> payload)
- {
- byte[] bytes = payload.ToArray();
- ByteReader reader = _readContext.Reader;
- reader.Reset(bytes);
- object? value = DeserializeObjectFromReader(reader);
- payload = payload.Slice(reader.Cursor);
- return value;
- }
+ /// <summary>
+ /// Writes the frame header for a payload.
+ /// </summary>
+ /// <param name="writer">Destination writer.</param>
+ /// <param name="isNone">Whether the payload value is null.</param>
public void WriteHead(ByteWriter writer, bool isNone)
{
byte bitmap = 0;
@@ -229,6 +250,12 @@ public sealed class Fory
writer.WriteUInt8(bitmap);
}
+ /// <summary>
+ /// Reads and validates the frame header.
+ /// </summary>
+ /// <param name="reader">Source reader.</param>
+ /// <returns><c>true</c> if the payload value is null; otherwise
<c>false</c>.</returns>
+ /// <exception cref="InvalidDataException">Thrown when the peer xlang
bitmap does not match this runtime mode.</exception>
public bool ReadHead(ByteReader reader)
{
byte bitmap = reader.ReadUInt8();
@@ -257,18 +284,4 @@ public sealed class Fory
return value;
}
- private object? DeserializeObjectFromReader(ByteReader reader)
- {
- bool isNone = ReadHead(reader);
- if (isNone)
- {
- return null;
- }
-
- _readContext.ResetFor(reader);
- RefMode refMode = Config.TrackRef ? RefMode.Tracking :
RefMode.NullOnly;
- object? value = DynamicAnyCodec.ReadAny(_readContext, refMode, true);
- _readContext.ResetObjectState();
- return value;
- }
}
diff --git a/csharp/src/Fory/ForyException.cs b/csharp/src/Fory/ForyException.cs
index d3c4689ff..22883a5a0 100644
--- a/csharp/src/Fory/ForyException.cs
+++ b/csharp/src/Fory/ForyException.cs
@@ -17,54 +17,105 @@
namespace Apache.Fory;
+/// <summary>
+/// Base exception type for Apache Fory C# runtime errors.
+/// </summary>
public class ForyException : Exception
{
+ /// <summary>
+ /// Creates a new Fory exception with the provided message.
+ /// </summary>
+ /// <param name="message">Error description.</param>
public ForyException(string message) : base(message)
{
}
}
+/// <summary>
+/// Thrown when input data is malformed or inconsistent with expected wire
format.
+/// </summary>
public sealed class InvalidDataException : ForyException
{
+ /// <summary>
+ /// Creates a new invalid data exception.
+ /// </summary>
+ /// <param name="message">Invalid data details.</param>
public InvalidDataException(string message) : base($"Invalid data:
{message}")
{
}
}
+/// <summary>
+/// Thrown when received type metadata does not match expected type metadata.
+/// </summary>
public sealed class TypeMismatchException : ForyException
{
+ /// <summary>
+ /// Creates a new type mismatch exception.
+ /// </summary>
+ /// <param name="expected">Expected wire type id.</param>
+ /// <param name="actual">Actual wire type id.</param>
public TypeMismatchException(uint expected, uint actual)
: base($"Type mismatch: expected {expected}, got {actual}")
{
}
}
+/// <summary>
+/// Thrown when an unregistered type is serialized or deserialized in a
registered-type path.
+/// </summary>
public sealed class TypeNotRegisteredException : ForyException
{
+ /// <summary>
+ /// Creates a new type-not-registered exception.
+ /// </summary>
+ /// <param name="message">Unregistered type details.</param>
public TypeNotRegisteredException(string message) : base($"Type not
registered: {message}")
{
}
}
+/// <summary>
+/// Thrown when reference metadata is invalid or inconsistent.
+/// </summary>
public sealed class RefException : ForyException
{
+ /// <summary>
+ /// Creates a new reference exception.
+ /// </summary>
+ /// <param name="message">Reference error details.</param>
public RefException(string message) : base($"Reference error: {message}")
{
}
}
+/// <summary>
+/// Thrown when encoding or decoding logic encounters unsupported or invalid
state.
+/// </summary>
public sealed class EncodingException : ForyException
{
+ /// <summary>
+ /// Creates a new encoding exception.
+ /// </summary>
+ /// <param name="message">Encoding error details.</param>
public EncodingException(string message) : base($"Encoding error:
{message}")
{
}
}
+/// <summary>
+/// Thrown when buffer cursor movement would exceed available bounds.
+/// </summary>
public sealed class OutOfBoundsException : ForyException
{
+ /// <summary>
+ /// Creates a new out-of-bounds exception.
+ /// </summary>
+ /// <param name="cursor">Current cursor offset.</param>
+ /// <param name="need">Requested byte count.</param>
+ /// <param name="length">Buffer length.</param>
public OutOfBoundsException(int cursor, int need, int length)
: base($"Buffer out of bounds: cursor={cursor}, need={need},
length={length}")
{
}
}
-
diff --git a/csharp/src/Fory/OptionalSerializer.cs
b/csharp/src/Fory/OptionalSerializer.cs
index 9cb42c7eb..ef3271b93 100644
--- a/csharp/src/Fory/OptionalSerializer.cs
+++ b/csharp/src/Fory/OptionalSerializer.cs
@@ -85,26 +85,26 @@ public sealed class NullableSerializer<T> : Serializer<T?>
where T : struct
case RefMode.None:
return wrappedSerializer.Read(context, RefMode.None,
readTypeInfo);
case RefMode.NullOnly:
- {
- sbyte refFlag = context.Reader.ReadInt8();
- if (refFlag == (sbyte)RefFlag.Null)
{
- return null;
- }
+ sbyte refFlag = context.Reader.ReadInt8();
+ if (refFlag == (sbyte)RefFlag.Null)
+ {
+ return null;
+ }
- return wrappedSerializer.Read(context, RefMode.None,
readTypeInfo);
- }
+ return wrappedSerializer.Read(context, RefMode.None,
readTypeInfo);
+ }
case RefMode.Tracking:
- {
- sbyte refFlag = context.Reader.ReadInt8();
- if (refFlag == (sbyte)RefFlag.Null)
{
- return null;
- }
+ sbyte refFlag = context.Reader.ReadInt8();
+ if (refFlag == (sbyte)RefFlag.Null)
+ {
+ return null;
+ }
- context.Reader.MoveBack(1);
- return wrappedSerializer.Read(context, RefMode.Tracking,
readTypeInfo);
- }
+ context.Reader.MoveBack(1);
+ return wrappedSerializer.Read(context, RefMode.Tracking,
readTypeInfo);
+ }
default:
throw new InvalidDataException($"unsupported ref mode
{refMode}");
}
diff --git a/csharp/src/Fory/PrimitiveDictionarySerializers.cs
b/csharp/src/Fory/PrimitiveDictionarySerializers.cs
index 3e024b92e..d407cdcde 100644
--- a/csharp/src/Fory/PrimitiveDictionarySerializers.cs
+++ b/csharp/src/Fory/PrimitiveDictionarySerializers.cs
@@ -737,7 +737,7 @@ internal class PrimitiveDictionarySerializer<TKey, TValue,
TKeyCodec, TValueCode
public override Dictionary<TKey, TValue> DefaultValue => null!;
-public override void WriteData(WriteContext context, in Dictionary<TKey,
TValue> value, bool hasGenerics)
+ public override void WriteData(WriteContext context, in Dictionary<TKey,
TValue> value, bool hasGenerics)
{
Dictionary<TKey, TValue> map = value ?? [];
PrimitiveDictionaryCodecWriter.WriteMap<
diff --git a/csharp/src/Fory/Serializer.cs b/csharp/src/Fory/Serializer.cs
index a90242a3e..1aa219ea2 100644
--- a/csharp/src/Fory/Serializer.cs
+++ b/csharp/src/Fory/Serializer.cs
@@ -17,16 +17,42 @@
namespace Apache.Fory;
+/// <summary>
+/// Base class for custom serializers.
+/// </summary>
+/// <typeparam name="T">Runtime value type handled by this
serializer.</typeparam>
public abstract class Serializer<T>
{
+ /// <summary>
+ /// Gets the default value returned when a null marker is read for this
serializer.
+ /// </summary>
public virtual T DefaultValue => default!;
internal object? DefaultObject => DefaultValue;
+ /// <summary>
+ /// Writes the serializer-specific payload body.
+ /// </summary>
+ /// <param name="context">Write context.</param>
+ /// <param name="value">Value to encode.</param>
+ /// <param name="hasGenerics">Whether generic type metadata is present for
the current field path.</param>
public abstract void WriteData(WriteContext context, in T value, bool
hasGenerics);
+ /// <summary>
+ /// Reads the serializer-specific payload body.
+ /// </summary>
+ /// <param name="context">Read context.</param>
+ /// <returns>Decoded value.</returns>
public abstract T ReadData(ReadContext context);
+ /// <summary>
+ /// Writes reference metadata and optional type metadata, then delegates
to <see cref="WriteData"/>.
+ /// </summary>
+ /// <param name="context">Write context.</param>
+ /// <param name="value">Value to write.</param>
+ /// <param name="refMode">Reference handling mode.</param>
+ /// <param name="writeTypeInfo">Whether type metadata should be
written.</param>
+ /// <param name="hasGenerics">Whether generic type metadata is present for
the current field path.</param>
public virtual void Write(WriteContext context, in T value, RefMode
refMode, bool writeTypeInfo, bool hasGenerics)
{
if (refMode != RefMode.None)
@@ -63,6 +89,13 @@ public abstract class Serializer<T>
WriteData(context, value, hasGenerics);
}
+ /// <summary>
+ /// Reads reference metadata and optional type metadata, then delegates to
<see cref="ReadData"/>.
+ /// </summary>
+ /// <param name="context">Read context.</param>
+ /// <param name="refMode">Reference handling mode.</param>
+ /// <param name="readTypeInfo">Whether type metadata should be
read.</param>
+ /// <returns>Decoded value.</returns>
public virtual T Read(ReadContext context, RefMode refMode, bool
readTypeInfo)
{
if (refMode != RefMode.None)
@@ -74,24 +107,24 @@ public abstract class Serializer<T>
case RefFlag.Null:
return DefaultValue;
case RefFlag.Ref:
- {
- uint refId = context.Reader.ReadVarUInt32();
- return context.RefReader.ReadRef<T>(refId);
- }
- case RefFlag.RefValue:
- {
- uint reservedRefId = context.RefReader.ReserveRefId();
- context.RefReader.PushPendingReference(reservedRefId);
- if (readTypeInfo)
{
- context.TypeResolver.ReadTypeInfo(this, context);
+ uint refId = context.Reader.ReadVarUInt32();
+ return context.RefReader.ReadRef<T>(refId);
}
+ case RefFlag.RefValue:
+ {
+ uint reservedRefId = context.RefReader.ReserveRefId();
+ context.RefReader.PushPendingReference(reservedRefId);
+ if (readTypeInfo)
+ {
+ context.TypeResolver.ReadTypeInfo(this, context);
+ }
- T value = ReadData(context);
- context.RefReader.FinishPendingReferenceIfNeeded(value);
- context.RefReader.PopPendingReference();
- return value;
- }
+ T value = ReadData(context);
+
context.RefReader.FinishPendingReferenceIfNeeded(value);
+ context.RefReader.PopPendingReference();
+ return value;
+ }
case RefFlag.NotNullValue:
break;
default:
diff --git a/csharp/src/Fory/StringSerializer.cs
b/csharp/src/Fory/StringSerializer.cs
index 1e56fcfd6..a27674422 100644
--- a/csharp/src/Fory/StringSerializer.cs
+++ b/csharp/src/Fory/StringSerializer.cs
@@ -63,9 +63,9 @@ public sealed class StringSerializer : Serializer<string>
int byteLength = checked((int)(header >> 2));
ReadOnlySpan<byte> bytes = context.Reader.ReadSpan(byteLength);
return encoding switch
- {
- (ulong)ForyStringEncoding.Utf8 =>
Encoding.UTF8.GetString(bytes),
- (ulong)ForyStringEncoding.Latin1 => DecodeLatin1(bytes),
+ {
+ (ulong)ForyStringEncoding.Utf8 => Encoding.UTF8.GetString(bytes),
+ (ulong)ForyStringEncoding.Latin1 => DecodeLatin1(bytes),
(ulong)ForyStringEncoding.Utf16 => DecodeUtf16(bytes),
_ => throw new EncodingException($"unsupported string encoding
{encoding}"),
};
diff --git a/csharp/src/Fory/ThreadSafeFory.cs
b/csharp/src/Fory/ThreadSafeFory.cs
index 280f34bee..6270d3c8d 100644
--- a/csharp/src/Fory/ThreadSafeFory.cs
+++ b/csharp/src/Fory/ThreadSafeFory.cs
@@ -36,26 +36,55 @@ public sealed class ThreadSafeFory : IDisposable
_threadLocalFory = new ThreadLocal<Fory>(CreatePerThreadFory,
trackAllValues: true);
}
+ /// <summary>
+ /// Gets the immutable runtime configuration shared by all thread-local
runtimes.
+ /// </summary>
public Config Config => _config;
+ /// <summary>
+ /// Registers a user type by numeric type identifier for all current and
future thread-local runtimes.
+ /// </summary>
+ /// <typeparam name="T">Type to register.</typeparam>
+ /// <param name="typeId">Numeric type identifier used on the wire.</param>
+ /// <returns>The same runtime instance.</returns>
public ThreadSafeFory Register<T>(uint typeId)
{
ApplyRegistration(fory => fory.Register<T>(typeId));
return this;
}
+ /// <summary>
+ /// Registers a user type by name for all current and future thread-local
runtimes.
+ /// </summary>
+ /// <typeparam name="T">Type to register.</typeparam>
+ /// <param name="typeName">Type name used on the wire.</param>
+ /// <returns>The same runtime instance.</returns>
public ThreadSafeFory Register<T>(string typeName)
{
ApplyRegistration(fory => fory.Register<T>(typeName));
return this;
}
+ /// <summary>
+ /// Registers a user type by namespace and name for all current and future
thread-local runtimes.
+ /// </summary>
+ /// <typeparam name="T">Type to register.</typeparam>
+ /// <param name="typeNamespace">Namespace used on the wire.</param>
+ /// <param name="typeName">Type name used on the wire.</param>
+ /// <returns>The same runtime instance.</returns>
public ThreadSafeFory Register<T>(string typeNamespace, string typeName)
{
ApplyRegistration(fory => fory.Register<T>(typeNamespace, typeName));
return this;
}
+ /// <summary>
+ /// Registers a user type by numeric type identifier with a custom
serializer for all thread-local runtimes.
+ /// </summary>
+ /// <typeparam name="T">Type to register.</typeparam>
+ /// <typeparam name="TSerializer">Serializer implementation used for
<typeparamref name="T"/>.</typeparam>
+ /// <param name="typeId">Numeric type identifier used on the wire.</param>
+ /// <returns>The same runtime instance.</returns>
public ThreadSafeFory Register<T, TSerializer>(uint typeId)
where TSerializer : Serializer<T>, new()
{
@@ -63,6 +92,14 @@ public sealed class ThreadSafeFory : IDisposable
return this;
}
+ /// <summary>
+ /// Registers a user type by namespace and name with a custom serializer
for all thread-local runtimes.
+ /// </summary>
+ /// <typeparam name="T">Type to register.</typeparam>
+ /// <typeparam name="TSerializer">Serializer implementation used for
<typeparamref name="T"/>.</typeparam>
+ /// <param name="typeNamespace">Namespace used on the wire.</param>
+ /// <param name="typeName">Type name used on the wire.</param>
+ /// <returns>The same runtime instance.</returns>
public ThreadSafeFory Register<T, TSerializer>(string typeNamespace,
string typeName)
where TSerializer : Serializer<T>, new()
{
@@ -70,46 +107,53 @@ public sealed class ThreadSafeFory : IDisposable
return this;
}
+ /// <summary>
+ /// Serializes a value into a new byte array containing one Fory frame.
+ /// </summary>
+ /// <typeparam name="T">Value type.</typeparam>
+ /// <param name="value">Value to serialize.</param>
+ /// <returns>Serialized bytes.</returns>
public byte[] Serialize<T>(in T value)
{
return Current.Serialize(in value);
}
+ /// <summary>
+ /// Serializes a value and writes one Fory frame into the provided buffer
writer.
+ /// </summary>
+ /// <typeparam name="T">Value type.</typeparam>
+ /// <param name="output">Destination writer.</param>
+ /// <param name="value">Value to serialize.</param>
public void Serialize<T>(IBufferWriter<byte> output, in T value)
{
Current.Serialize(output, in value);
}
+ /// <summary>
+ /// Deserializes a value from one Fory frame in the provided span.
+ /// </summary>
+ /// <typeparam name="T">Target type.</typeparam>
+ /// <param name="payload">Serialized bytes containing exactly one
frame.</param>
+ /// <returns>Deserialized value.</returns>
public T Deserialize<T>(ReadOnlySpan<byte> payload)
{
return Current.Deserialize<T>(payload);
}
+ /// <summary>
+ /// Deserializes a value from the head of a framed sequence and advances
the sequence.
+ /// </summary>
+ /// <typeparam name="T">Target type.</typeparam>
+ /// <param name="payload">Input sequence. On success, sliced past the
consumed frame.</param>
+ /// <returns>Deserialized value.</returns>
public T Deserialize<T>(ref ReadOnlySequence<byte> payload)
{
return Current.Deserialize<T>(ref payload);
}
- public byte[] SerializeObject(object? value)
- {
- return Current.SerializeObject(value);
- }
-
- public void SerializeObject(IBufferWriter<byte> output, object? value)
- {
- Current.SerializeObject(output, value);
- }
-
- public object? DeserializeObject(ReadOnlySpan<byte> payload)
- {
- return Current.DeserializeObject(payload);
- }
-
- public object? DeserializeObject(ref ReadOnlySequence<byte> payload)
- {
- return Current.DeserializeObject(ref payload);
- }
-
+ /// <summary>
+ /// Disposes thread-local runtimes and prevents further API use.
+ /// </summary>
public void Dispose()
{
lock (_registrationLock)
diff --git a/csharp/src/Fory/TypeResolver.cs b/csharp/src/Fory/TypeResolver.cs
index 2f8b8ee72..c938c3052 100644
--- a/csharp/src/Fory/TypeResolver.cs
+++ b/csharp/src/Fory/TypeResolver.cs
@@ -293,42 +293,42 @@ public sealed class TypeResolver
{
case TypeId.CompatibleStruct:
case TypeId.NamedCompatibleStruct:
- {
- TypeMeta typeMeta = BuildCompatibleTypeMeta(info, wireTypeId,
context.TrackRef);
- context.WriteCompatibleTypeMeta(type, typeMeta);
- return;
- }
- case TypeId.NamedEnum:
- case TypeId.NamedStruct:
- case TypeId.NamedExt:
- case TypeId.NamedUnion:
- {
- if (context.Compatible)
{
TypeMeta typeMeta = BuildCompatibleTypeMeta(info,
wireTypeId, context.TrackRef);
context.WriteCompatibleTypeMeta(type, typeMeta);
+ return;
}
- else
+ case TypeId.NamedEnum:
+ case TypeId.NamedStruct:
+ case TypeId.NamedExt:
+ case TypeId.NamedUnion:
{
- if (!info.NamespaceName.HasValue ||
!info.TypeName.HasValue)
+ if (context.Compatible)
{
- throw new InvalidDataException("missing type name
metadata for name-registered type");
+ TypeMeta typeMeta = BuildCompatibleTypeMeta(info,
wireTypeId, context.TrackRef);
+ context.WriteCompatibleTypeMeta(type, typeMeta);
+ }
+ else
+ {
+ if (!info.NamespaceName.HasValue ||
!info.TypeName.HasValue)
+ {
+ throw new InvalidDataException("missing type name
metadata for name-registered type");
+ }
+
+ WriteMetaString(
+ context,
+ info.NamespaceName.Value,
+ TypeMetaEncodings.NamespaceMetaStringEncodings,
+ MetaStringEncoder.Namespace);
+ WriteMetaString(
+ context,
+ info.TypeName.Value,
+ TypeMetaEncodings.TypeNameMetaStringEncodings,
+ MetaStringEncoder.TypeName);
}
- WriteMetaString(
- context,
- info.NamespaceName.Value,
- TypeMetaEncodings.NamespaceMetaStringEncodings,
- MetaStringEncoder.Namespace);
- WriteMetaString(
- context,
- info.TypeName.Value,
- TypeMetaEncodings.TypeNameMetaStringEncodings,
- MetaStringEncoder.TypeName);
+ return;
}
-
- return;
- }
default:
if (!info.RegisterByName &&
WireTypeNeedsUserTypeId(wireTypeId))
{
@@ -412,50 +412,50 @@ public sealed class TypeResolver
{
case TypeId.CompatibleStruct:
case TypeId.NamedCompatibleStruct:
- {
- TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta();
- ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed,
typeId);
- context.PushCompatibleTypeMeta(type, remoteTypeMeta);
- return;
- }
+ {
+ TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta();
+ ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed,
typeId);
+ context.PushCompatibleTypeMeta(type, remoteTypeMeta);
+ return;
+ }
case TypeId.NamedEnum:
case TypeId.NamedStruct:
case TypeId.NamedExt:
case TypeId.NamedUnion:
- {
- if (context.Compatible)
{
- TypeMeta remoteTypeMeta = context.ReadCompatibleTypeMeta();
- ValidateCompatibleTypeMeta(remoteTypeMeta, info, allowed,
typeId);
- if (typeId == TypeId.NamedStruct)
+ if (context.Compatible)
{
- context.PushCompatibleTypeMeta(type, remoteTypeMeta);
+ TypeMeta remoteTypeMeta =
context.ReadCompatibleTypeMeta();
+ ValidateCompatibleTypeMeta(remoteTypeMeta, info,
allowed, typeId);
+ if (typeId == TypeId.NamedStruct)
+ {
+ context.PushCompatibleTypeMeta(type,
remoteTypeMeta);
+ }
}
- }
- else
- {
- MetaString namespaceName = ReadMetaString(
- context,
- MetaStringDecoder.Namespace,
- TypeMetaEncodings.NamespaceMetaStringEncodings);
- MetaString typeName = ReadMetaString(
- context,
- MetaStringDecoder.TypeName,
- TypeMetaEncodings.TypeNameMetaStringEncodings);
- if (!info.RegisterByName || !info.NamespaceName.HasValue
|| !info.TypeName.HasValue)
+ else
{
- throw new InvalidDataException("received
name-registered type info for id-registered local type");
+ MetaString namespaceName = ReadMetaString(
+ context,
+ MetaStringDecoder.Namespace,
+ TypeMetaEncodings.NamespaceMetaStringEncodings);
+ MetaString typeName = ReadMetaString(
+ context,
+ MetaStringDecoder.TypeName,
+ TypeMetaEncodings.TypeNameMetaStringEncodings);
+ if (!info.RegisterByName ||
!info.NamespaceName.HasValue || !info.TypeName.HasValue)
+ {
+ throw new InvalidDataException("received
name-registered type info for id-registered local type");
+ }
+
+ if (namespaceName.Value !=
info.NamespaceName.Value.Value || typeName.Value != info.TypeName.Value.Value)
+ {
+ throw new InvalidDataException(
+ $"type name mismatch: expected
{info.NamespaceName.Value.Value}::{info.TypeName.Value.Value}, got
{namespaceName.Value}::{typeName.Value}");
+ }
}
- if (namespaceName.Value != info.NamespaceName.Value.Value
|| typeName.Value != info.TypeName.Value.Value)
- {
- throw new InvalidDataException(
- $"type name mismatch: expected
{info.NamespaceName.Value.Value}::{info.TypeName.Value.Value}, got
{namespaceName.Value}::{typeName.Value}");
- }
+ return;
}
-
- return;
- }
default:
if (!info.RegisterByName && WireTypeNeedsUserTypeId(typeId))
{
@@ -553,31 +553,31 @@ public sealed class TypeResolver
{
case TypeId.CompatibleStruct:
case TypeId.NamedCompatibleStruct:
- {
- TypeMeta typeMeta = context.ReadCompatibleTypeMeta();
- if (typeMeta.RegisterByName)
{
- return new DynamicTypeInfo(wireTypeId, null,
typeMeta.NamespaceName, typeMeta.TypeName, typeMeta);
- }
+ TypeMeta typeMeta = context.ReadCompatibleTypeMeta();
+ if (typeMeta.RegisterByName)
+ {
+ return new DynamicTypeInfo(wireTypeId, null,
typeMeta.NamespaceName, typeMeta.TypeName, typeMeta);
+ }
- return new DynamicTypeInfo(wireTypeId, typeMeta.UserTypeId,
null, null, typeMeta);
- }
+ return new DynamicTypeInfo(wireTypeId,
typeMeta.UserTypeId, null, null, typeMeta);
+ }
case TypeId.NamedStruct:
case TypeId.NamedEnum:
case TypeId.NamedExt:
case TypeId.NamedUnion:
- {
- MetaString namespaceName = ReadMetaString(context.Reader,
MetaStringDecoder.Namespace, TypeMetaEncodings.NamespaceMetaStringEncodings);
- MetaString typeName = ReadMetaString(context.Reader,
MetaStringDecoder.TypeName, TypeMetaEncodings.TypeNameMetaStringEncodings);
- return new DynamicTypeInfo(wireTypeId, null, namespaceName,
typeName, null);
- }
+ {
+ MetaString namespaceName = ReadMetaString(context.Reader,
MetaStringDecoder.Namespace, TypeMetaEncodings.NamespaceMetaStringEncodings);
+ MetaString typeName = ReadMetaString(context.Reader,
MetaStringDecoder.TypeName, TypeMetaEncodings.TypeNameMetaStringEncodings);
+ return new DynamicTypeInfo(wireTypeId, null,
namespaceName, typeName, null);
+ }
case TypeId.Struct:
case TypeId.Enum:
case TypeId.Ext:
case TypeId.TypedUnion:
- {
- return new DynamicTypeInfo(wireTypeId,
context.Reader.ReadVarUInt32(), null, null, null);
- }
+ {
+ return new DynamicTypeInfo(wireTypeId,
context.Reader.ReadVarUInt32(), null, null, null);
+ }
default:
return new DynamicTypeInfo(wireTypeId, null, null, null, null);
}
@@ -664,51 +664,51 @@ public sealed class TypeResolver
case TypeId.Enum:
case TypeId.Ext:
case TypeId.TypedUnion:
- {
- if (!typeInfo.UserTypeId.HasValue)
{
- throw new InvalidDataException($"missing dynamic user type
id for {typeInfo.WireTypeId}");
- }
+ if (!typeInfo.UserTypeId.HasValue)
+ {
+ throw new InvalidDataException($"missing dynamic user
type id for {typeInfo.WireTypeId}");
+ }
- return ReadByUserTypeId(typeInfo.UserTypeId.Value, context);
- }
+ return ReadByUserTypeId(typeInfo.UserTypeId.Value,
context);
+ }
case TypeId.NamedStruct:
case TypeId.NamedEnum:
case TypeId.NamedExt:
case TypeId.NamedUnion:
- {
- if (!typeInfo.NamespaceName.HasValue ||
!typeInfo.TypeName.HasValue)
{
- throw new InvalidDataException($"missing dynamic type name
for {typeInfo.WireTypeId}");
- }
+ if (!typeInfo.NamespaceName.HasValue ||
!typeInfo.TypeName.HasValue)
+ {
+ throw new InvalidDataException($"missing dynamic type
name for {typeInfo.WireTypeId}");
+ }
- return ReadByTypeName(typeInfo.NamespaceName.Value.Value,
typeInfo.TypeName.Value.Value, context);
- }
+ return ReadByTypeName(typeInfo.NamespaceName.Value.Value,
typeInfo.TypeName.Value.Value, context);
+ }
case TypeId.CompatibleStruct:
case TypeId.NamedCompatibleStruct:
- {
- if (typeInfo.CompatibleTypeMeta is null)
{
- throw new InvalidDataException($"missing compatible type
meta for {typeInfo.WireTypeId}");
- }
+ if (typeInfo.CompatibleTypeMeta is null)
+ {
+ throw new InvalidDataException($"missing compatible
type meta for {typeInfo.WireTypeId}");
+ }
- TypeMeta compatibleTypeMeta = typeInfo.CompatibleTypeMeta;
- if (compatibleTypeMeta.RegisterByName)
- {
- return ReadByTypeName(
- compatibleTypeMeta.NamespaceName.Value,
- compatibleTypeMeta.TypeName.Value,
- context,
- compatibleTypeMeta);
- }
+ TypeMeta compatibleTypeMeta = typeInfo.CompatibleTypeMeta;
+ if (compatibleTypeMeta.RegisterByName)
+ {
+ return ReadByTypeName(
+ compatibleTypeMeta.NamespaceName.Value,
+ compatibleTypeMeta.TypeName.Value,
+ context,
+ compatibleTypeMeta);
+ }
- if (!compatibleTypeMeta.UserTypeId.HasValue)
- {
- throw new InvalidDataException("missing user type id in
compatible dynamic type meta");
- }
+ if (!compatibleTypeMeta.UserTypeId.HasValue)
+ {
+ throw new InvalidDataException("missing user type id
in compatible dynamic type meta");
+ }
- return ReadByUserTypeId(compatibleTypeMeta.UserTypeId.Value,
context, compatibleTypeMeta);
- }
+ return
ReadByUserTypeId(compatibleTypeMeta.UserTypeId.Value, context,
compatibleTypeMeta);
+ }
case TypeId.None:
return null;
default:
diff --git a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
index 5a9e4e6c2..ccc3ccafb 100644
--- a/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
+++ b/csharp/tests/Fory.Tests/ForyRuntimeTests.cs
@@ -513,19 +513,19 @@ public sealed class ForyRuntimeTests
}
[Fact]
- public void StreamDeserializeObjectConsumesSingleFrame()
+ public void StreamDeserializeGenericObjectConsumesSingleFrame()
{
ForyRuntime fory = ForyRuntime.Builder().Build();
- byte[] p1 = fory.SerializeObject("first");
- byte[] p2 = fory.SerializeObject(99);
+ byte[] p1 = fory.Serialize<object?>("first");
+ byte[] p2 = fory.Serialize<object?>(99);
byte[] joined = new byte[p1.Length + p2.Length];
Buffer.BlockCopy(p1, 0, joined, 0, p1.Length);
Buffer.BlockCopy(p2, 0, joined, p1.Length, p2.Length);
ReadOnlySequence<byte> sequence = new(joined);
- object? first = fory.DeserializeObject(ref sequence);
- object? second = fory.DeserializeObject(ref sequence);
+ object? first = fory.Deserialize<object?>(ref sequence);
+ object? second = fory.Deserialize<object?>(ref sequence);
Assert.Equal("first", first);
Assert.Equal(99, second);
@@ -790,7 +790,7 @@ public sealed class ForyRuntimeTests
[true] = null,
};
Dictionary<object, object?> mapDecoded =
- Assert.IsType<Dictionary<object,
object?>>(fory.DeserializeObject(fory.SerializeObject(map)));
+ Assert.IsType<Dictionary<object,
object?>>(fory.Deserialize<object?>(fory.Serialize<object?>(map)));
Assert.Equal(3, mapDecoded.Count);
Assert.Equal(7, mapDecoded["k1"]);
Assert.Equal("v2", mapDecoded[2]);
@@ -799,7 +799,7 @@ public sealed class ForyRuntimeTests
HashSet<object> set = ["a", 7, false];
HashSet<object?> setDecoded =
-
Assert.IsType<HashSet<object?>>(fory.DeserializeObject(fory.SerializeObject(set)));
+
Assert.IsType<HashSet<object?>>(fory.Deserialize<object?>(fory.Serialize<object?>(set)));
Assert.Equal(3, setDecoded.Count);
Assert.Contains("a", setDecoded);
Assert.Contains(7, setDecoded);
@@ -815,27 +815,52 @@ public sealed class ForyRuntimeTests
queue.Enqueue("q1");
queue.Enqueue(7);
queue.Enqueue(null);
- List<object?> queueDecoded =
Assert.IsType<List<object?>>(fory.DeserializeObject(fory.SerializeObject(queue)));
+ List<object?> queueDecoded =
Assert.IsType<List<object?>>(fory.Deserialize<object?>(fory.Serialize<object?>(queue)));
Assert.Equal(new object?[] { "q1", 7, null }, queueDecoded.ToArray());
Stack<object?> stack = new();
stack.Push("s1");
stack.Push(9);
- List<object?> stackDecoded =
Assert.IsType<List<object?>>(fory.DeserializeObject(fory.SerializeObject(stack)));
+ List<object?> stackDecoded =
Assert.IsType<List<object?>>(fory.Deserialize<object?>(fory.Serialize<object?>(stack)));
Assert.Equal(new object?[] { 9, "s1" }, stackDecoded.ToArray());
LinkedList<object?> linkedList = new(new object?[] { "l1", 3, null });
- List<object?> linkedListDecoded =
Assert.IsType<List<object?>>(fory.DeserializeObject(fory.SerializeObject(linkedList)));
+ List<object?> linkedListDecoded =
Assert.IsType<List<object?>>(fory.Deserialize<object?>(fory.Serialize<object?>(linkedList)));
Assert.Equal(new object?[] { "l1", 3, null },
linkedListDecoded.ToArray());
ImmutableHashSet<object?> immutableSet =
ImmutableHashSet.Create<object?>("i1", 5);
HashSet<object?> immutableSetDecoded =
-
Assert.IsType<HashSet<object?>>(fory.DeserializeObject(fory.SerializeObject(immutableSet)));
+
Assert.IsType<HashSet<object?>>(fory.Deserialize<object?>(fory.Serialize<object?>(immutableSet)));
Assert.Equal(2, immutableSetDecoded.Count);
Assert.Contains("i1", immutableSetDecoded);
Assert.Contains(5, immutableSetDecoded);
}
+ [Fact]
+ public void DynamicObjectReadDepthExceededThrows()
+ {
+ ForyRuntime writer = ForyRuntime.Builder().Build();
+ object? value = new List<object?> { new List<object?> { 1 } };
+ byte[] payload = writer.Serialize<object?>(value);
+
+ ForyRuntime reader = ForyRuntime.Builder().MaxDepth(2).Build();
+ InvalidDataException ex = Assert.Throws<InvalidDataException>(() =>
reader.Deserialize<object?>(payload));
+ Assert.Contains("dynamic object nesting depth", ex.Message);
+ }
+
+ [Fact]
+ public void DynamicObjectReadDepthWithinLimitRoundTrip()
+ {
+ ForyRuntime fory = ForyRuntime.Builder().MaxDepth(3).Build();
+ object? value = new List<object?> { new List<object?> { 1 } };
+
+ List<object?> outer =
Assert.IsType<List<object?>>(fory.Deserialize<object?>(fory.Serialize<object?>(value)));
+ Assert.Single(outer);
+ List<object?> inner = Assert.IsType<List<object?>>(outer[0]);
+ Assert.Single(inner);
+ Assert.Equal(1, inner[0]);
+ }
+
[Fact]
public void GeneratedSerializerSupportsObjectKeyMap()
{
diff --git a/csharp/tests/Fory.XlangPeer/Program.cs
b/csharp/tests/Fory.XlangPeer/Program.cs
index 60fece54b..3c76bb7f1 100644
--- a/csharp/tests/Fory.XlangPeer/Program.cs
+++ b/csharp/tests/Fory.XlangPeer/Program.cs
@@ -339,7 +339,7 @@ internal static class Program
List<byte> output = [];
foreach (string sample in StringSamples)
{
- Append(output, fory.SerializeObject(sample));
+ Append(output, fory.Serialize<object?>(sample));
}
return output.ToArray();
@@ -389,33 +389,33 @@ internal static class Program
Ensure(color == Color.White, "color mismatch");
List<byte> output = [];
- Append(output, fory.SerializeObject(b1));
- Append(output, fory.SerializeObject(b2));
- Append(output, fory.SerializeObject(i32));
- Append(output, fory.SerializeObject(i8a));
- Append(output, fory.SerializeObject(i8b));
- Append(output, fory.SerializeObject(i16a));
- Append(output, fory.SerializeObject(i16b));
- Append(output, fory.SerializeObject(i32a));
- Append(output, fory.SerializeObject(i32b));
- Append(output, fory.SerializeObject(i64a));
- Append(output, fory.SerializeObject(i64b));
- Append(output, fory.SerializeObject(f32));
- Append(output, fory.SerializeObject(f64));
- Append(output, fory.SerializeObject(str));
- Append(output, fory.SerializeObject(day));
- Append(output, fory.SerializeObject(timestamp));
- Append(output, fory.SerializeObject(bools));
- Append(output, fory.SerializeObject(bytes));
- Append(output, fory.SerializeObject(int16s));
- Append(output, fory.SerializeObject(int32s));
- Append(output, fory.SerializeObject(int64s));
- Append(output, fory.SerializeObject(floats));
- Append(output, fory.SerializeObject(doubles));
- Append(output, fory.SerializeObject(list));
- Append(output, fory.SerializeObject(set));
- Append(output, fory.SerializeObject(map));
- Append(output, fory.SerializeObject(color));
+ Append(output, fory.Serialize<object?>(b1));
+ Append(output, fory.Serialize<object?>(b2));
+ Append(output, fory.Serialize<object?>(i32));
+ Append(output, fory.Serialize<object?>(i8a));
+ Append(output, fory.Serialize<object?>(i8b));
+ Append(output, fory.Serialize<object?>(i16a));
+ Append(output, fory.Serialize<object?>(i16b));
+ Append(output, fory.Serialize<object?>(i32a));
+ Append(output, fory.Serialize<object?>(i32b));
+ Append(output, fory.Serialize<object?>(i64a));
+ Append(output, fory.Serialize<object?>(i64b));
+ Append(output, fory.Serialize<object?>(f32));
+ Append(output, fory.Serialize<object?>(f64));
+ Append(output, fory.Serialize<object?>(str));
+ Append(output, fory.Serialize<object?>(day));
+ Append(output, fory.Serialize<object?>(timestamp));
+ Append(output, fory.Serialize<object?>(bools));
+ Append(output, fory.Serialize<object?>(bytes));
+ Append(output, fory.Serialize<object?>(int16s));
+ Append(output, fory.Serialize<object?>(int32s));
+ Append(output, fory.Serialize<object?>(int64s));
+ Append(output, fory.Serialize<object?>(floats));
+ Append(output, fory.Serialize<object?>(doubles));
+ Append(output, fory.Serialize<object?>(list));
+ Append(output, fory.Serialize<object?>(set));
+ Append(output, fory.Serialize<object?>(map));
+ Append(output, fory.Serialize<object?>(color));
return output.ToArray();
}
@@ -445,10 +445,10 @@ internal static class Program
EnsureConsumed(sequence, nameof(CaseList));
List<byte> output = [];
- Append(output, fory.SerializeObject(strList));
- Append(output, fory.SerializeObject(strList2));
- Append(output, fory.SerializeObject(itemList));
- Append(output, fory.SerializeObject(itemList2));
+ Append(output, fory.Serialize<object?>(strList));
+ Append(output, fory.Serialize<object?>(strList2));
+ Append(output, fory.Serialize<object?>(itemList));
+ Append(output, fory.Serialize<object?>(itemList2));
return output.ToArray();
}
@@ -462,8 +462,8 @@ internal static class Program
EnsureConsumed(sequence, nameof(CaseMap));
List<byte> output = [];
- Append(output, fory.SerializeObject(strMap));
- Append(output, fory.SerializeObject(itemMap));
+ Append(output, fory.Serialize<object?>(strMap));
+ Append(output, fory.Serialize<object?>(itemMap));
return output.ToArray();
}
@@ -485,13 +485,13 @@ internal static class Program
Ensure(obj.F3 == 3 && obj.F4 == 4 && obj.F5 == 0 && obj.F6 == 0,
"item1 boxed fields mismatch");
List<byte> output = [];
- Append(output, fory.SerializeObject(obj));
- Append(output, fory.SerializeObject(f1));
- Append(output, fory.SerializeObject(f2));
- Append(output, fory.SerializeObject(f3));
- Append(output, fory.SerializeObject(f4));
- Append(output, fory.SerializeObject(f5));
- Append(output, fory.SerializeObject(f6));
+ Append(output, fory.Serialize<object?>(obj));
+ Append(output, fory.Serialize<object?>(f1));
+ Append(output, fory.Serialize<object?>(f2));
+ Append(output, fory.Serialize<object?>(f3));
+ Append(output, fory.Serialize<object?>(f4));
+ Append(output, fory.Serialize<object?>(f5));
+ Append(output, fory.Serialize<object?>(f6));
return output.ToArray();
}
@@ -506,9 +506,9 @@ internal static class Program
EnsureConsumed(sequence, nameof(CaseItem));
List<byte> output = [];
- Append(output, fory.SerializeObject(i1));
- Append(output, fory.SerializeObject(i2));
- Append(output, fory.SerializeObject(i3));
+ Append(output, fory.Serialize<object?>(i1));
+ Append(output, fory.Serialize<object?>(i2));
+ Append(output, fory.Serialize<object?>(i3));
return output.ToArray();
}
@@ -524,10 +524,10 @@ internal static class Program
EnsureConsumed(sequence, nameof(CaseColor));
List<byte> output = [];
- Append(output, fory.SerializeObject(c1));
- Append(output, fory.SerializeObject(c2));
- Append(output, fory.SerializeObject(c3));
- Append(output, fory.SerializeObject(c4));
+ Append(output, fory.Serialize<object?>(c1));
+ Append(output, fory.Serialize<object?>(c2));
+ Append(output, fory.Serialize<object?>(c3));
+ Append(output, fory.Serialize<object?>(c4));
return output.ToArray();
}
@@ -541,8 +541,8 @@ internal static class Program
EnsureConsumed(sequence, nameof(CaseStructWithList));
List<byte> output = [];
- Append(output, fory.SerializeObject(s1));
- Append(output, fory.SerializeObject(s2));
+ Append(output, fory.Serialize<object?>(s1));
+ Append(output, fory.Serialize<object?>(s2));
return output.ToArray();
}
@@ -556,8 +556,8 @@ internal static class Program
EnsureConsumed(sequence, nameof(CaseStructWithMap));
List<byte> output = [];
- Append(output, fory.SerializeObject(s1));
- Append(output, fory.SerializeObject(s2));
+ Append(output, fory.Serialize<object?>(s1));
+ Append(output, fory.Serialize<object?>(s2));
return output.ToArray();
}
@@ -577,8 +577,8 @@ internal static class Program
Ensure(second.Union.Value is long secondValue && secondValue == 42L,
"union case value mismatch for second value");
List<byte> output = [];
- Append(output, fory.SerializeObject(first));
- Append(output, fory.SerializeObject(second));
+ Append(output, fory.Serialize<object?>(first));
+ Append(output, fory.Serialize<object?>(second));
return output.ToArray();
}
@@ -614,19 +614,19 @@ internal static class Program
for (int i = 0; i < 3; i++)
{
Color color = fory.Deserialize<Color>(ref sequence);
- Append(output, fory.SerializeObject(color));
+ Append(output, fory.Serialize<object?>(color));
}
for (int i = 0; i < 3; i++)
{
MyStruct myStruct = fory.Deserialize<MyStruct>(ref sequence);
- Append(output, fory.SerializeObject(myStruct));
+ Append(output, fory.Serialize<object?>(myStruct));
}
for (int i = 0; i < 3; i++)
{
MyExt myExt = fory.Deserialize<MyExt>(ref sequence);
- Append(output, fory.SerializeObject(myExt));
+ Append(output, fory.Serialize<object?>(myExt));
}
EnsureConsumed(sequence, nameof(CaseConsistentNamed));
@@ -653,8 +653,8 @@ internal static class Program
EnsureConsumed(sequence, nameof(CasePolymorphicList));
List<byte> output = [];
- Append(output, fory.SerializeObject(animals));
- Append(output, fory.SerializeObject(holder));
+ Append(output, fory.Serialize<object?>(animals));
+ Append(output, fory.Serialize<object?>(holder));
return output.ToArray();
}
@@ -671,8 +671,8 @@ internal static class Program
EnsureConsumed(sequence, nameof(CasePolymorphicMap));
List<byte> output = [];
- Append(output, fory.SerializeObject(map));
- Append(output, fory.SerializeObject(holder));
+ Append(output, fory.Serialize<object?>(map));
+ Append(output, fory.Serialize<object?>(holder));
return output.ToArray();
}
@@ -768,7 +768,7 @@ internal static class Program
EnsureConsumed(sequence,
nameof(CaseEnumSchemaEvolutionCompatibleReverse));
Ensure(value.F1 == TestEnum.ValueC, "enum schema evolution reverse F1
mismatch");
Ensure(value.F2 == TestEnum.ValueA, "enum schema evolution reverse F2
default mismatch");
- return fory.SerializeObject(value);
+ return fory.Serialize<object?>(value);
}
private static byte[] CaseNullableFieldSchemaConsistentNotNull(byte[]
input)
@@ -809,7 +809,7 @@ internal static class Program
RefOuterSchemaConsistent outer =
fory.Deserialize<RefOuterSchemaConsistent>(ref sequence);
EnsureConsumed(sequence, nameof(CaseRefSchemaConsistent));
Ensure(ReferenceEquals(outer.Inner1, outer.Inner2), "reference
tracking mismatch");
- return fory.SerializeObject(outer);
+ return fory.Serialize<object?>(outer);
}
private static byte[] CaseRefCompatible(byte[] input)
@@ -822,7 +822,7 @@ internal static class Program
RefOuterCompatible outer = fory.Deserialize<RefOuterCompatible>(ref
sequence);
EnsureConsumed(sequence, nameof(CaseRefCompatible));
Ensure(ReferenceEquals(outer.Inner1, outer.Inner2), "reference
tracking mismatch");
- return fory.SerializeObject(outer);
+ return fory.Serialize<object?>(outer);
}
private static byte[] CaseCollectionElementRefOverride(byte[] input)
@@ -857,7 +857,7 @@ internal static class Program
}
}
- return fory.SerializeObject(container);
+ return fory.Serialize<object?>(container);
}
private static byte[] CaseCircularRefSchemaConsistent(byte[] input)
@@ -869,7 +869,7 @@ internal static class Program
CircularRefStruct value = fory.Deserialize<CircularRefStruct>(ref
sequence);
EnsureConsumed(sequence, nameof(CaseCircularRefSchemaConsistent));
Ensure(ReferenceEquals(value, value.SelfRef), "circular ref mismatch");
- return fory.SerializeObject(value);
+ return fory.Serialize<object?>(value);
}
private static byte[] CaseCircularRefCompatible(byte[] input)
@@ -881,7 +881,7 @@ internal static class Program
CircularRefStruct value = fory.Deserialize<CircularRefStruct>(ref
sequence);
EnsureConsumed(sequence, nameof(CaseCircularRefCompatible));
Ensure(ReferenceEquals(value, value.SelfRef), "circular ref mismatch");
- return fory.SerializeObject(value);
+ return fory.Serialize<object?>(value);
}
private static byte[] CaseUnsignedSchemaConsistentSimple(byte[] input)
@@ -910,7 +910,7 @@ internal static class Program
ReadOnlySequence<byte> sequence = new(input);
T value = fory.Deserialize<T>(ref sequence);
EnsureConsumed(sequence, typeof(T).Name);
- return fory.SerializeObject(value);
+ return fory.Serialize<object?>(value);
}
private static void RegisterSimpleById(ForyRuntime fory)
diff --git a/docs/guide/csharp/basic-serialization.md
b/docs/guide/csharp/basic-serialization.md
index 2188c912d..9c4573a0a 100644
--- a/docs/guide/csharp/basic-serialization.md
+++ b/docs/guide/csharp/basic-serialization.md
@@ -19,7 +19,7 @@ license: |
limitations under the License.
---
-This page covers typed and dynamic serialization APIs in Apache Fory™ C#.
+This page covers typed serialization APIs in Apache Fory™ C#.
## Object Graph Serialization
@@ -88,9 +88,9 @@ MyType first = fory.Deserialize<MyType>(ref sequence);
MyType second = fory.Deserialize<MyType>(ref sequence);
```
-## Dynamic Object API
+## Dynamic Payloads via Generic Object API
-Use object APIs when the compile-time type is unknown or heterogeneous.
+When the compile-time type is unknown or heterogeneous, use the generic API
with `object?`.
```csharp
Dictionary<object, object?> value = new()
@@ -100,8 +100,8 @@ Dictionary<object, object?> value = new()
[true] = null,
};
-byte[] payload = fory.SerializeObject(value);
-object? decoded = fory.DeserializeObject(payload);
+byte[] payload = fory.Serialize<object?>(value);
+object? decoded = fory.Deserialize<object?>(payload);
```
## Buffer Writer API
@@ -115,7 +115,7 @@ ArrayBufferWriter<byte> writer = new();
fory.Serialize(writer, value);
ArrayBufferWriter<byte> dynamicWriter = new();
-fory.SerializeObject(dynamicWriter, value);
+fory.Serialize<object?>(dynamicWriter, value);
```
## Notes
diff --git a/docs/guide/csharp/index.md b/docs/guide/csharp/index.md
index c12c5a32e..b18e414e2 100644
--- a/docs/guide/csharp/index.md
+++ b/docs/guide/csharp/index.md
@@ -19,7 +19,7 @@ license: |
limitations under the License.
---
-Apache Fory™ C# is a high-performance, cross-language serialization runtime
for .NET. It provides object graph serialization, schema evolution, dynamic
object support, and a thread-safe wrapper for concurrent workloads.
+Apache Fory™ C# is a high-performance, cross-language serialization runtime
for .NET. It provides object graph serialization, schema evolution, generic
object payload support, and a thread-safe wrapper for concurrent workloads.
## Why Fory C#?
@@ -67,7 +67,7 @@ User decoded = fory.Deserialize<User>(payload);
## Core API Surface
- `Serialize<T>(in T value)` / `Deserialize<T>(...)`
-- `SerializeObject(object? value)` / `DeserializeObject(...)` for dynamic
payloads
+- `Serialize<object?>(...)` / `Deserialize<object?>(...)` for dynamic payloads
- `Register<T>(uint typeId)` and namespace/name registration APIs
- `Register<T, TSerializer>(...)` for custom serializers
diff --git a/docs/guide/csharp/supported-types.md
b/docs/guide/csharp/supported-types.md
index 8a89a3fab..0b6397788 100644
--- a/docs/guide/csharp/supported-types.md
+++ b/docs/guide/csharp/supported-types.md
@@ -79,7 +79,7 @@ This page summarizes built-in and generated type support in
Apache Fory™ C#.
## Dynamic Types
-Dynamic object APIs (`SerializeObject` / `DeserializeObject`) support:
+Dynamic object payloads via `Serialize<object?>` / `Deserialize<object?>`
support:
- Primitive/object values
- Dynamic lists/sets/maps
diff --git a/python/setup.py b/python/setup.py
index 6dc32ade8..2a8457d00 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -18,6 +18,9 @@
import os
import platform
import subprocess
+import sys
+import threading
+import time
from os.path import abspath, join as pjoin
from setuptools import setup
@@ -40,6 +43,82 @@ print(f"setup_dir: {setup_dir}")
print(f"project_dir: {project_dir}")
print(f"fory_cpp_src_dir: {fory_cpp_src_dir}")
+_RETRYABLE_NETWORK_ERROR_PATTERNS = (
+ "error downloading",
+ "download_and_extract",
+ "download from",
+ "http archive",
+ "get returned 500",
+ "get returned 502",
+ "get returned 503",
+ "get returned 504",
+ "network is unreachable",
+ "connection refused",
+ "connection reset",
+ "connection timed out",
+ "timed out waiting for",
+ "timed out",
+ "name resolution",
+ "temporary failure in name resolution",
+ "tls handshake timeout",
+ "temporary failure",
+)
+
+
+def _is_retryable_network_error(output: str) -> bool:
+ lowered = output.lower()
+ return any(pattern in lowered for pattern in
_RETRYABLE_NETWORK_ERROR_PATTERNS)
+
+
+def _stream_pipe(pipe, sink, chunks):
+ try:
+ for line in iter(pipe.readline, ""):
+ chunks.append(line)
+ sink.write(line)
+ sink.flush()
+ finally:
+ pipe.close()
+
+
+def _run_with_retry(args, cwd, max_attempts=3):
+ for attempt in range(1, max_attempts + 1):
+ process = subprocess.Popen(
+ args,
+ cwd=cwd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ errors="replace",
+ bufsize=1,
+ )
+
+ stdout_chunks = []
+ stderr_chunks = []
+ stdout_thread = threading.Thread(target=_stream_pipe,
args=(process.stdout, sys.stdout, stdout_chunks))
+ stderr_thread = threading.Thread(target=_stream_pipe,
args=(process.stderr, sys.stderr, stderr_chunks))
+ stdout_thread.start()
+ stderr_thread.start()
+ returncode = process.wait()
+ stdout_thread.join()
+ stderr_thread.join()
+
+ stdout_text = "".join(stdout_chunks)
+ stderr_text = "".join(stderr_chunks)
+ combined_output = f"{stdout_text}\n{stderr_text}"
+ if returncode == 0:
+ return
+
+ if attempt >= max_attempts or not
_is_retryable_network_error(combined_output):
+ raise subprocess.CalledProcessError(returncode, args,
output=stdout_text, stderr=stderr_text)
+
+ backoff_seconds = attempt * 5
+ print(
+ f"Detected transient network/download error while running {'
'.join(args)} "
+ f"(attempt {attempt}/{max_attempts}); retrying in
{backoff_seconds}s.",
+ file=sys.stderr,
+ )
+ time.sleep(backoff_seconds)
+
class BinaryDistribution(Distribution):
def __init__(self, attrs=None):
@@ -59,7 +138,7 @@ class BinaryDistribution(Distribution):
bazel_args += ["//:cp_fory_so"]
# Ensure Windows path compatibility
cwd_path = os.path.normpath(project_dir)
- subprocess.check_call(bazel_args, cwd=cwd_path)
+ _run_with_retry(bazel_args, cwd=cwd_path)
def has_ext_modules(self):
return True
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]