This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch add_csharp_grpc_support
in repository https://gitbox.apache.org/repos/asf/fory.git

commit 809d618139a20a7d1981e35f518e5995d4063b18
Author: 慕白 <[email protected]>
AuthorDate: Sat Jun 13 13:00:28 2026 +0800

    feat(csharp): add generated grpc support
---
 .agents/languages/csharp.md                        |   2 +
 compiler/fory_compiler/cli.py                      |  34 +-
 compiler/fory_compiler/generators/csharp.py        | 139 ++-
 .../fory_compiler/generators/services/csharp.py    | 590 +++++++++++++
 .../fory_compiler/tests/test_csharp_generator.py   | 199 ++++-
 .../fory_compiler/tests/test_service_codegen.py    | 945 ++++++++++++++++++++-
 docs/compiler/compiler-guide.md                    |  18 +-
 docs/compiler/flatbuffers-idl.md                   |  12 +-
 docs/compiler/generated-code.md                    |  61 ++
 docs/compiler/index.md                             |  15 +-
 docs/compiler/protobuf-idl.md                      |  39 +-
 docs/compiler/schema-idl.md                        |   6 +-
 docs/guide/csharp/grpc-support.md                  | 220 +++++
 docs/guide/csharp/index.md                         |   1 +
 docs/guide/csharp/troubleshooting.md               |  22 +-
 integration_tests/grpc_tests/generate_grpc.py      |   2 +
 16 files changed, 2195 insertions(+), 110 deletions(-)

diff --git a/.agents/languages/csharp.md b/.agents/languages/csharp.md
index 891339a8f..098f9a50f 100644
--- a/.agents/languages/csharp.md
+++ b/.agents/languages/csharp.md
@@ -9,6 +9,8 @@ Load this file when changing `csharp/` or C# xlang behavior.
 - C# code must build without compiler or analyzer warnings. Treat warnings as 
blockers in project, test, and generated code.
 - Fory C# requires .NET SDK `8.0+` and C# `12+`.
 - Use `dotnet format` to keep C# code style consistent.
+- Generated C# gRPC service companions are compiler-owned files that depend on 
application-provided gRPC packages, not `csharp/src/Fory`. Keep gRPC package 
references out of the Fory runtime package.
+- C# generated schema modules are source-file owners. Service companions must 
use that module's `ThreadSafeFory` and must not introduce namespace-owned 
aliases or duplicate serializer registration paths.
 - Compatible scalar, list-array, and binary/uint8-array adaptations are 
immediate-field-only. Recursive matched-field comparison for collection 
elements, array elements, map keys, and map values must require exact 
nullability, ref tracking, generic arity, and type shape except documented 
user-type family normalization.
 - When extending C# tests from Java references, prioritize xlang spec behavior 
and the public C# contract before adding complex Java-specific parity cases.
 
diff --git a/compiler/fory_compiler/cli.py b/compiler/fory_compiler/cli.py
index a654f6a4e..13dfbef24 100644
--- a/compiler/fory_compiler/cli.py
+++ b/compiler/fory_compiler/cli.py
@@ -31,6 +31,7 @@ from fory_compiler.ir.emitter import FDLEmitter
 from fory_compiler.ir.validator import SchemaValidator
 from fory_compiler.generators.base import GeneratorOptions
 from fory_compiler.generators import GENERATORS
+from fory_compiler.generators.csharp import validate_csharp_generation
 from fory_compiler.generators.kotlin import (
     kotlin_output_paths,
     kotlin_package_for_schema,
@@ -317,6 +318,26 @@ def validate_kotlin_generation(
     return validate_kotlin_output_paths(graph, grpc=grpc)
 
 
+def validate_csharp_files(
+    files: List[Path],
+    import_paths: List[Path],
+    grpc: bool = False,
+) -> bool:
+    """Preflight C# generated paths and module owners before writing output."""
+    cache: Dict[Path, Schema] = {}
+    graph: List[Tuple[Path, Schema]] = []
+    for file_path in files:
+        file_graph = collect_schema_graph(file_path, import_paths, cache, 
set())
+        if file_graph is None:
+            return False
+        graph.extend(file_graph)
+    try:
+        return validate_csharp_generation(graph, grpc=grpc)
+    except ValueError as e:
+        print(f"Error: {e}", file=sys.stderr)
+        return False
+
+
 def _find_go_module_root(base_go_out: Path) -> Optional[Path]:
     base_go_out = base_go_out.resolve()
     for candidate in (base_go_out, *base_go_out.parents):
@@ -700,15 +721,17 @@ def compile_file(
             print(f"Error: {e}", file=sys.stderr)
             return False
 
-        if lang == "rust":
-            # Special error handling for Rust
+        if lang in {"rust", "csharp"}:
+            # Special error handling for languages with run-wide generated path
+            # validation.
+            display_lang = "C#" if lang == "csharp" else "Rust"
             output_targets: List[Path] = []
             for f in files:
                 target = (lang_output / f.path).resolve()
                 # Reject overwriting existing non-generated files
                 if target.exists() and not is_generated_file(target):
                     print(
-                        f"Error: Rust output path collision: {target} already 
exists",
+                        f"Error: {display_lang} output path collision: 
{target} already exists",
                         file=sys.stderr,
                     )
                     return False
@@ -716,7 +739,7 @@ def compile_file(
                 previous_source = generated_outputs.get(target)
                 if previous_source is not None and previous_source != 
file_path:
                     print(
-                        "Error: Rust output path collision: "
+                        f"Error: {display_lang} output path collision: "
                         f"{previous_source} and {file_path} both generate 
{target}",
                         file=sys.stderr,
                     )
@@ -899,6 +922,9 @@ def cmd_compile(args: argparse.Namespace) -> int:
     if "kotlin" in lang_output_dirs:
         if not validate_kotlin_generation(args.files, import_paths, 
grpc=args.grpc):
             return 1
+    if "csharp" in lang_output_dirs:
+        if not validate_csharp_files(args.files, import_paths, grpc=args.grpc):
+            return 1
 
     # Create output directories
     for out_dir in lang_output_dirs.values():
diff --git a/compiler/fory_compiler/generators/csharp.py 
b/compiler/fory_compiler/generators/csharp.py
index ee232e2e6..67395fddf 100644
--- a/compiler/fory_compiler/generators/csharp.py
+++ b/compiler/fory_compiler/generators/csharp.py
@@ -18,10 +18,12 @@
 """C# code generator."""
 
 from pathlib import Path
+import re
 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.generators.services.csharp import CSharpServiceMixin
 from fory_compiler.ir.ast import (
     ArrayType,
     Enum,
@@ -38,7 +40,101 @@ from fory_compiler.ir.ast import (
 from fory_compiler.ir.types import PrimitiveKind
 
 
-class CSharpGenerator(BaseGenerator):
+def csharp_namespace_for_schema(schema: Schema) -> str:
+    value = schema.get_option("csharp_namespace")
+    if value:
+        return str(value)
+    if schema.package:
+        return schema.package
+    return "generated"
+
+
+def csharp_module_file_name(schema: Schema) -> str:
+    if schema.source_file and not schema.source_file.startswith("<"):
+        return f"{Path(schema.source_file).stem}.cs"
+    if schema.package:
+        return f"{schema.package.replace('.', '_')}.cs"
+    return "generated.cs"
+
+
+def csharp_module_owner_stem(schema: Schema) -> str:
+    if schema.source_file and not schema.source_file.startswith("<"):
+        return Path(schema.source_file).stem
+    if schema.package:
+        return schema.package.replace(".", "_")
+    return "generated"
+
+
+def csharp_module_class_name(schema: Schema) -> str:
+    stem = re.sub(r"[^0-9A-Za-z_]", "_", csharp_module_owner_stem(schema))
+    parts = [part for part in stem.lower().split("_") if part]
+    class_name = "".join(part.capitalize() for part in parts)
+    if not class_name or not (class_name[0].isalpha() or class_name[0] == "_"):
+        class_name = f"Schema{class_name}"
+    return f"{class_name}ForyModule"
+
+
+def csharp_output_paths(
+    schema: Schema, include_services: bool = False
+) -> List[Tuple[str, str]]:
+    namespace_name = csharp_namespace_for_schema(schema)
+    namespace_path = namespace_name.replace(".", "/") if namespace_name else ""
+    model_file = csharp_module_file_name(schema)
+    model_path = f"{namespace_path}/{model_file}" if namespace_path else 
model_file
+    outputs = [(model_path, f"schema module 
{csharp_module_class_name(schema)}")]
+    if include_services:
+        for service in schema.services:
+            file_name = f"{service.name}Grpc.cs"
+            path = f"{namespace_path}/{file_name}" if namespace_path else 
file_name
+            outputs.append((path, f"service {service.name}"))
+    return outputs
+
+
+def validate_csharp_generation(
+    graph: List[Tuple[Path, Schema]], grpc: bool = False
+) -> bool:
+    output_owners: Dict[str, List[str]] = {}
+    module_owners: Dict[Tuple[str, str], List[str]] = {}
+    for path, schema in graph:
+        for output_path, owner in csharp_output_paths(schema, 
include_services=grpc):
+            output_owners.setdefault(output_path, []).append(f"{path} {owner}")
+        namespace_name = csharp_namespace_for_schema(schema)
+        module_name = csharp_module_class_name(schema)
+        module_owners.setdefault((namespace_name, module_name), 
[]).append(str(path))
+
+    output_collisions = {
+        output_path: owners
+        for output_path, owners in output_owners.items()
+        if len(owners) > 1
+    }
+    if output_collisions:
+        details = ", ".join(
+            f"{output_path}: {', '.join(owners)}"
+            for output_path, owners in sorted(output_collisions.items())
+        )
+        raise ValueError(
+            "C# generated file path collision; rename schema files or 
services, "
+            f"or use distinct C# namespaces. Collisions: {details}"
+        )
+
+    module_collisions = {
+        owner: paths for owner, paths in module_owners.items() if len(paths) > 
1
+    }
+    if module_collisions:
+        details = ", ".join(
+            f"{namespace_name}.{module_name}: {', '.join(paths)}"
+            for (namespace_name, module_name), paths in sorted(
+                module_collisions.items()
+            )
+        )
+        raise ValueError(
+            "C# schema module owner collision; rename schema files or use "
+            f"distinct C# namespaces. Collisions: {details}"
+        )
+    return True
+
+
+class CSharpGenerator(CSharpServiceMixin, BaseGenerator):
     """Generates C# models and registration helpers for Apache Fory."""
 
     language_name = "csharp"
@@ -196,29 +292,16 @@ class CSharpGenerator(BaseGenerator):
             visit_message(message, [])
 
     def get_csharp_namespace(self) -> str:
-        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"
+        return csharp_namespace_for_schema(self.schema)
 
     def get_module_class_name(self) -> str:
-        return 
self._module_class_name_for_namespace(self.get_csharp_namespace())
+        return csharp_module_class_name(self.schema)
 
-    def _module_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)}ForyModule"
+    def _module_class_name_for_schema(self, schema: Schema) -> str:
+        return csharp_module_class_name(schema)
 
     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"
+        return csharp_module_file_name(self.schema)
 
     def _namespace_path(self, namespace_name: str) -> str:
         return namespace_name.replace(".", "/") if namespace_name else ""
@@ -313,12 +396,7 @@ class CSharpGenerator(BaseGenerator):
         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"
+        return csharp_namespace_for_schema(schema)
 
     def _csharp_namespace_for_type(self, type_def: object) -> str:
         location = getattr(type_def, "location", None)
@@ -344,7 +422,7 @@ class CSharpGenerator(BaseGenerator):
             if imported_schema is None:
                 continue
             namespace_name = self._csharp_namespace_for_schema(imported_schema)
-            module_name = self._module_class_name_for_namespace(namespace_name)
+            module_name = self._module_class_name_for_schema(imported_schema)
             file_info[normalized] = (namespace_name, module_name)
 
         ordered: List[Tuple[str, str]] = []
@@ -365,14 +443,7 @@ class CSharpGenerator(BaseGenerator):
                 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
+        return ordered
 
     def generate(self) -> List[GeneratedFile]:
         return [self.generate_file()]
diff --git a/compiler/fory_compiler/generators/services/csharp.py 
b/compiler/fory_compiler/generators/services/csharp.py
new file mode 100644
index 000000000..1fa5fe11d
--- /dev/null
+++ b/compiler/fory_compiler/generators/services/csharp.py
@@ -0,0 +1,590 @@
+# 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# gRPC service companion generator."""
+
+import re
+from typing import Dict, List, Set, Tuple
+
+from fory_compiler.generators.base import GeneratedFile
+from fory_compiler.generators.services.base import StreamingMode, 
streaming_mode
+from fory_compiler.ir.ast import Service
+
+
+class CSharpServiceMixin:
+    """Generates C# gRPC service companions."""
+
+    def generate_services(self) -> List[GeneratedFile]:
+        services = [s for s in self.schema.services if not 
self.is_imported_type(s)]
+        if not services:
+            return []
+        self._validate_csharp_services(services)
+        return [self._generate_csharp_service(service) for service in services]
+
+    def _generate_csharp_service(self, service: Service) -> GeneratedFile:
+        namespace_name = self.get_csharp_namespace()
+        service_name = self.safe_type_identifier(service.name)
+        module_name = self.safe_type_identifier(self.get_module_class_name())
+        marshallers = self._csharp_marshallers(service)
+
+        lines: List[str] = []
+        lines.append(self.get_license_header("//"))
+        lines.append("#pragma warning disable 8981")
+        lines.append("")
+        lines.append("using grpc = global::Grpc.Core;")
+        lines.append("")
+        lines.append(f"namespace {namespace_name};")
+        lines.append("")
+        lines.append(f"public static partial class {service_name}")
+        lines.append("{")
+        lines.append(
+            f'{self.indent_str}static readonly string __ServiceName = 
"{self.get_grpc_service_name(service)}";'
+        )
+        lines.append(
+            f"{self.indent_str}private static readonly 
global::Apache.Fory.ThreadSafeFory __Fory ="
+        )
+        lines.append(f"{self.indent_str * 2}{module_name}.GetFory();")
+        lines.append("")
+        lines.extend(self._generate_marshallers(marshallers))
+        lines.append("")
+        lines.extend(self._generate_methods(service, marshallers))
+        lines.append("")
+        lines.extend(self._generate_server_base(service))
+        lines.append("")
+        lines.extend(self._generate_client(service))
+        lines.append("")
+        lines.extend(self._generate_bind_service(service))
+        lines.append("")
+        lines.extend(self._generate_cold_helpers())
+        lines.append("}")
+
+        file_name = f"{service.name}Grpc.cs"
+        ns_path = self._namespace_path(namespace_name)
+        path = f"{ns_path}/{file_name}" if ns_path else file_name
+        return GeneratedFile(path=path, content="\n".join(lines))
+
+    def _csharp_marshallers(
+        self, service: Service
+    ) -> Dict[str, Tuple[str, str, str, str]]:
+        marshallers: Dict[str, Tuple[str, str, str, str]] = {}
+        used_names: Set[str] = set()
+        for method in service.methods:
+            for named_type in (method.request_type, method.response_type):
+                type_ref = self._named_type_reference(named_type)
+                if type_ref in marshallers:
+                    continue
+                suffix = self._private_identifier(type_ref)
+                if suffix in used_names:
+                    index = 2
+                    candidate = f"{suffix}_{index}"
+                    while candidate in used_names:
+                        index += 1
+                        candidate = f"{suffix}_{index}"
+                    suffix = candidate
+                used_names.add(suffix)
+                marshaller = f"__Marshaller_{suffix}"
+                serialize = f"__Serialize_{suffix}"
+                deserialize = f"__Deserialize_{suffix}"
+                marshallers[type_ref] = (marshaller, serialize, deserialize, 
type_ref)
+        return marshallers
+
+    def _generate_marshallers(
+        self, marshallers: Dict[str, Tuple[str, str, str, str]]
+    ) -> List[str]:
+        lines: List[str] = []
+        for _, (marshaller, serialize, deserialize, type_ref) in 
marshallers.items():
+            lines.append(
+                f"{self.indent_str}static readonly 
grpc::Marshaller<{type_ref}> {marshaller} ="
+            )
+            lines.append(
+                f"{self.indent_str * 2}grpc::Marshallers.Create({serialize}, 
{deserialize});"
+            )
+            lines.append("")
+            lines.append(
+                f"{self.indent_str}private static void {serialize}({type_ref} 
value, grpc::SerializationContext context)"
+            )
+            lines.append(f"{self.indent_str}{{")
+            lines.append(f"{self.indent_str * 2}try")
+            lines.append(f"{self.indent_str * 2}{{")
+            lines.append(
+                f"{self.indent_str * 
3}context.Complete(__Fory.Serialize<{type_ref}>(in value));"
+            )
+            lines.append(f"{self.indent_str * 2}}}")
+            lines.append(f"{self.indent_str * 2}catch 
(global::System.Exception error)")
+            lines.append(f"{self.indent_str * 2}{{")
+            lines.append(f"{self.indent_str * 3}__ThrowSerializeError(error);")
+            lines.append(f"{self.indent_str * 2}}}")
+            lines.append(f"{self.indent_str}}}")
+            lines.append("")
+            lines.append(
+                f"{self.indent_str}private static {type_ref} 
{deserialize}(grpc::DeserializationContext context)"
+            )
+            lines.append(f"{self.indent_str}{{")
+            lines.append(f"{self.indent_str * 2}try")
+            lines.append(f"{self.indent_str * 2}{{")
+            lines.append(
+                f"{self.indent_str * 
3}global::System.Buffers.ReadOnlySequence<byte> body = 
context.PayloadAsReadOnlySequence();"
+            )
+            lines.append(
+                f"{self.indent_str * 3}{type_ref} value = 
__Fory.Deserialize<{type_ref}>(ref body);"
+            )
+            lines.append(f"{self.indent_str * 3}if (!body.IsEmpty)")
+            lines.append(f"{self.indent_str * 3}{{")
+            lines.append(
+                f"{self.indent_str * 4}return 
__ThrowTrailingBody<{type_ref}>();"
+            )
+            lines.append(f"{self.indent_str * 3}}}")
+            lines.append(f"{self.indent_str * 3}return value;")
+            lines.append(f"{self.indent_str * 2}}}")
+            lines.append(
+                f"{self.indent_str * 2}catch (global::System.Exception error) 
when (error is not grpc::RpcException)"
+            )
+            lines.append(f"{self.indent_str * 2}{{")
+            lines.append(
+                f"{self.indent_str * 3}return 
__ThrowDeserializeError<{type_ref}>(error);"
+            )
+            lines.append(f"{self.indent_str * 2}}}")
+            lines.append(f"{self.indent_str}}}")
+            lines.append("")
+        if lines:
+            lines.pop()
+        return lines
+
+    def _generate_methods(
+        self,
+        service: Service,
+        marshallers: Dict[str, Tuple[str, str, str, str]],
+    ) -> List[str]:
+        lines: List[str] = []
+        for method in service.methods:
+            request_type = self._named_type_reference(method.request_type)
+            response_type = self._named_type_reference(method.response_type)
+            request_marshaller = marshallers[request_type][0]
+            response_marshaller = marshallers[response_type][0]
+            descriptor = self._method_field_name(method.name)
+            grpc_type = self._method_type(method)
+            lines.append(
+                f"{self.indent_str}static readonly 
grpc::Method<{request_type}, {response_type}> {descriptor} ="
+            )
+            lines.append(
+                f"{self.indent_str * 2}new grpc::Method<{request_type}, 
{response_type}>("
+            )
+            lines.append(f"{self.indent_str * 3}{grpc_type},")
+            lines.append(f"{self.indent_str * 3}__ServiceName,")
+            lines.append(f'{self.indent_str * 3}"{method.name}",')
+            lines.append(f"{self.indent_str * 3}{request_marshaller},")
+            lines.append(f"{self.indent_str * 3}{response_marshaller});")
+            lines.append("")
+        if lines:
+            lines.pop()
+        return lines
+
+    def _generate_server_base(self, service: Service) -> List[str]:
+        service_name = self.safe_type_identifier(service.name)
+        base_name = self._base_type_name(service)
+        lines = [
+            
f'{self.indent_str}[grpc::BindServiceMethod(typeof({service_name}), 
"BindService")]',
+            f"{self.indent_str}public abstract partial class {base_name}",
+            f"{self.indent_str}{{",
+        ]
+        for method in service.methods:
+            lines.append("")
+            lines.extend(self._server_base_method(method))
+        lines.append(f"{self.indent_str}}}")
+        return lines
+
+    def _server_base_method(self, method) -> List[str]:
+        request_type = self._named_type_reference(method.request_type)
+        response_type = self._named_type_reference(method.response_type)
+        method_name = self._public_method_name(method.name)
+        mode = streaming_mode(method)
+        lines: List[str] = []
+        if mode is StreamingMode.UNARY:
+            lines.append(
+                f"{self.indent_str * 2}public virtual 
global::System.Threading.Tasks.Task<{response_type}> {method_name}("
+            )
+            lines.append(f"{self.indent_str * 3}{request_type} request,")
+            lines.append(f"{self.indent_str * 3}grpc::ServerCallContext 
context)")
+            lines.append(f"{self.indent_str * 2}{{")
+            lines.append(
+                f"{self.indent_str * 3}return 
__ThrowUnimplemented<{response_type}>();"
+            )
+        elif mode is StreamingMode.SERVER_STREAMING:
+            lines.append(
+                f"{self.indent_str * 2}public virtual 
global::System.Threading.Tasks.Task {method_name}("
+            )
+            lines.append(f"{self.indent_str * 3}{request_type} request,")
+            lines.append(
+                f"{self.indent_str * 
3}grpc::IServerStreamWriter<{response_type}> responseStream,"
+            )
+            lines.append(f"{self.indent_str * 3}grpc::ServerCallContext 
context)")
+            lines.append(f"{self.indent_str * 2}{{")
+            lines.append(f"{self.indent_str * 3}return 
__ThrowUnimplemented();")
+        elif mode is StreamingMode.CLIENT_STREAMING:
+            lines.append(
+                f"{self.indent_str * 2}public virtual 
global::System.Threading.Tasks.Task<{response_type}> {method_name}("
+            )
+            lines.append(
+                f"{self.indent_str * 
3}grpc::IAsyncStreamReader<{request_type}> requestStream,"
+            )
+            lines.append(f"{self.indent_str * 3}grpc::ServerCallContext 
context)")
+            lines.append(f"{self.indent_str * 2}{{")
+            lines.append(
+                f"{self.indent_str * 3}return 
__ThrowUnimplemented<{response_type}>();"
+            )
+        else:
+            lines.append(
+                f"{self.indent_str * 2}public virtual 
global::System.Threading.Tasks.Task {method_name}("
+            )
+            lines.append(
+                f"{self.indent_str * 
3}grpc::IAsyncStreamReader<{request_type}> requestStream,"
+            )
+            lines.append(
+                f"{self.indent_str * 
3}grpc::IServerStreamWriter<{response_type}> responseStream,"
+            )
+            lines.append(f"{self.indent_str * 3}grpc::ServerCallContext 
context)")
+            lines.append(f"{self.indent_str * 2}{{")
+            lines.append(f"{self.indent_str * 3}return 
__ThrowUnimplemented();")
+        lines.append(f"{self.indent_str * 2}}}")
+        return lines
+
+    def _generate_client(self, service: Service) -> List[str]:
+        client_name = self._client_type_name(service)
+        lines = [
+            f"{self.indent_str}public partial class {client_name} : 
grpc::ClientBase<{client_name}>",
+            f"{self.indent_str}{{",
+            f"{self.indent_str * 2}public {client_name}(grpc::ChannelBase 
channel) : base(channel)",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 2}}}",
+            "",
+            f"{self.indent_str * 2}public {client_name}(grpc::CallInvoker 
callInvoker) : base(callInvoker)",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 2}}}",
+            "",
+            f"{self.indent_str * 2}protected {client_name}() : base()",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 2}}}",
+            "",
+            f"{self.indent_str * 2}protected 
{client_name}(ClientBaseConfiguration configuration) : base(configuration)",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 2}}}",
+        ]
+        for method in service.methods:
+            lines.append("")
+            lines.extend(self._client_methods(method))
+        lines.append("")
+        lines.append(
+            f"{self.indent_str * 2}protected override {client_name} 
NewInstance(ClientBaseConfiguration configuration)"
+        )
+        lines.append(f"{self.indent_str * 2}{{")
+        lines.append(f"{self.indent_str * 3}return new 
{client_name}(configuration);")
+        lines.append(f"{self.indent_str * 2}}}")
+        lines.append(f"{self.indent_str}}}")
+        return lines
+
+    def _client_methods(self, method) -> List[str]:
+        request_type = self._named_type_reference(method.request_type)
+        response_type = self._named_type_reference(method.response_type)
+        method_name = self._public_method_name(method.name)
+        descriptor = self._method_field_name(method.name)
+        mode = streaming_mode(method)
+        lines: List[str] = []
+        if mode is StreamingMode.UNARY:
+            lines.extend(
+                self._unary_client_methods(
+                    method_name, descriptor, request_type, response_type
+                )
+            )
+        elif mode is StreamingMode.SERVER_STREAMING:
+            lines.extend(
+                self._server_stream_client_methods(
+                    method_name, descriptor, request_type, response_type
+                )
+            )
+        elif mode is StreamingMode.CLIENT_STREAMING:
+            lines.extend(
+                self._client_stream_client_methods(
+                    method_name, descriptor, request_type, response_type
+                )
+            )
+        else:
+            lines.extend(
+                self._duplex_client_methods(
+                    method_name, descriptor, request_type, response_type
+                )
+            )
+        return lines
+
+    def _unary_client_methods(
+        self, method_name: str, descriptor: str, request_type: str, 
response_type: str
+    ) -> List[str]:
+        async_name = self._append_identifier(method_name, "Async")
+        return [
+            f"{self.indent_str * 2}public virtual {response_type} 
{method_name}({request_type} request, grpc::Metadata? headers = null, 
global::System.DateTime? deadline = null, 
global::System.Threading.CancellationToken cancellationToken = default)",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 3}return {method_name}(request, new 
grpc::CallOptions(headers, deadline, cancellationToken));",
+            f"{self.indent_str * 2}}}",
+            "",
+            f"{self.indent_str * 2}public virtual {response_type} 
{method_name}({request_type} request, grpc::CallOptions options)",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 3}return 
CallInvoker.BlockingUnaryCall({descriptor}, null, options, request);",
+            f"{self.indent_str * 2}}}",
+            "",
+            f"{self.indent_str * 2}public virtual 
grpc::AsyncUnaryCall<{response_type}> {async_name}({request_type} request, 
grpc::Metadata? headers = null, global::System.DateTime? deadline = null, 
global::System.Threading.CancellationToken cancellationToken = default)",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 3}return {async_name}(request, new 
grpc::CallOptions(headers, deadline, cancellationToken));",
+            f"{self.indent_str * 2}}}",
+            "",
+            f"{self.indent_str * 2}public virtual 
grpc::AsyncUnaryCall<{response_type}> {async_name}({request_type} request, 
grpc::CallOptions options)",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 3}return 
CallInvoker.AsyncUnaryCall({descriptor}, null, options, request);",
+            f"{self.indent_str * 2}}}",
+        ]
+
+    def _server_stream_client_methods(
+        self, method_name: str, descriptor: str, request_type: str, 
response_type: str
+    ) -> List[str]:
+        return [
+            f"{self.indent_str * 2}public virtual 
grpc::AsyncServerStreamingCall<{response_type}> {method_name}({request_type} 
request, grpc::Metadata? headers = null, global::System.DateTime? deadline = 
null, global::System.Threading.CancellationToken cancellationToken = default)",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 3}return {method_name}(request, new 
grpc::CallOptions(headers, deadline, cancellationToken));",
+            f"{self.indent_str * 2}}}",
+            "",
+            f"{self.indent_str * 2}public virtual 
grpc::AsyncServerStreamingCall<{response_type}> {method_name}({request_type} 
request, grpc::CallOptions options)",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 3}return 
CallInvoker.AsyncServerStreamingCall({descriptor}, null, options, request);",
+            f"{self.indent_str * 2}}}",
+        ]
+
+    def _client_stream_client_methods(
+        self, method_name: str, descriptor: str, request_type: str, 
response_type: str
+    ) -> List[str]:
+        return [
+            f"{self.indent_str * 2}public virtual 
grpc::AsyncClientStreamingCall<{request_type}, {response_type}> 
{method_name}(grpc::Metadata? headers = null, global::System.DateTime? deadline 
= null, global::System.Threading.CancellationToken cancellationToken = 
default)",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 3}return {method_name}(new 
grpc::CallOptions(headers, deadline, cancellationToken));",
+            f"{self.indent_str * 2}}}",
+            "",
+            f"{self.indent_str * 2}public virtual 
grpc::AsyncClientStreamingCall<{request_type}, {response_type}> 
{method_name}(grpc::CallOptions options)",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 3}return 
CallInvoker.AsyncClientStreamingCall({descriptor}, null, options);",
+            f"{self.indent_str * 2}}}",
+        ]
+
+    def _duplex_client_methods(
+        self, method_name: str, descriptor: str, request_type: str, 
response_type: str
+    ) -> List[str]:
+        return [
+            f"{self.indent_str * 2}public virtual 
grpc::AsyncDuplexStreamingCall<{request_type}, {response_type}> 
{method_name}(grpc::Metadata? headers = null, global::System.DateTime? deadline 
= null, global::System.Threading.CancellationToken cancellationToken = 
default)",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 3}return {method_name}(new 
grpc::CallOptions(headers, deadline, cancellationToken));",
+            f"{self.indent_str * 2}}}",
+            "",
+            f"{self.indent_str * 2}public virtual 
grpc::AsyncDuplexStreamingCall<{request_type}, {response_type}> 
{method_name}(grpc::CallOptions options)",
+            f"{self.indent_str * 2}{{",
+            f"{self.indent_str * 3}return 
CallInvoker.AsyncDuplexStreamingCall({descriptor}, null, options);",
+            f"{self.indent_str * 2}}}",
+        ]
+
+    def _generate_bind_service(self, service: Service) -> List[str]:
+        base_name = self._base_type_name(service)
+        builder_lines = [
+            f"{self.indent_str}public static grpc::ServerServiceDefinition 
BindService({base_name} serviceImpl)",
+            f"{self.indent_str}{{",
+            f"{self.indent_str * 
2}global::System.ArgumentNullException.ThrowIfNull(serviceImpl);",
+            f"{self.indent_str * 2}return 
grpc::ServerServiceDefinition.CreateBuilder()",
+        ]
+        for index, method in enumerate(service.methods):
+            suffix = ".Build();" if index == len(service.methods) - 1 else ""
+            builder_lines.append(
+                f"{self.indent_str * 
3}.AddMethod({self._method_field_name(method.name)}, 
serviceImpl.{self._public_method_name(method.name)}){suffix}"
+            )
+        if not service.methods:
+            builder_lines[-1] = (
+                f"{self.indent_str * 2}return 
grpc::ServerServiceDefinition.CreateBuilder().Build();"
+            )
+        builder_lines.append(f"{self.indent_str}}}")
+        builder_lines.append("")
+        builder_lines.append(
+            f"{self.indent_str}public static void 
BindService(grpc::ServiceBinderBase serviceBinder, {base_name}? serviceImpl)"
+        )
+        builder_lines.append(f"{self.indent_str}{{")
+        builder_lines.append(
+            f"{self.indent_str * 
2}global::System.ArgumentNullException.ThrowIfNull(serviceBinder);"
+        )
+        for method in service.methods:
+            builder_lines.extend(self._binder_method(method))
+        builder_lines.append(f"{self.indent_str}}}")
+        return builder_lines
+
+    def _binder_method(self, method) -> List[str]:
+        request_type = self._named_type_reference(method.request_type)
+        response_type = self._named_type_reference(method.response_type)
+        delegate_type = {
+            StreamingMode.UNARY: "UnaryServerMethod",
+            StreamingMode.SERVER_STREAMING: "ServerStreamingServerMethod",
+            StreamingMode.CLIENT_STREAMING: "ClientStreamingServerMethod",
+            StreamingMode.BIDIRECTIONAL: "DuplexStreamingServerMethod",
+        }[streaming_mode(method)]
+        return [
+            f"{self.indent_str * 2}serviceBinder.AddMethod(",
+            f"{self.indent_str * 3}{self._method_field_name(method.name)},",
+            f"{self.indent_str * 3}serviceImpl == null",
+            f"{self.indent_str * 4}? null",
+            f"{self.indent_str * 4}: new grpc::{delegate_type}<{request_type}, 
{response_type}>(serviceImpl.{self._public_method_name(method.name)}));",
+        ]
+
+    def _generate_cold_helpers(self) -> List[str]:
+        no_inline = (
+            "global::System.Runtime.CompilerServices.MethodImpl("
+            
"global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)"
+        )
+        return [
+            f"{self.indent_str}[{no_inline}]",
+            f"{self.indent_str}private static void 
__ThrowSerializeError(global::System.Exception error)",
+            f"{self.indent_str}{{",
+            f'{self.indent_str * 2}throw new grpc::RpcException(new 
grpc::Status(grpc::StatusCode.Internal, $"Fory gRPC serialization failed: 
{{error.Message}}"));',
+            f"{self.indent_str}}}",
+            "",
+            f"{self.indent_str}[{no_inline}]",
+            f"{self.indent_str}private static T 
__ThrowDeserializeError<T>(global::System.Exception error)",
+            f"{self.indent_str}{{",
+            f'{self.indent_str * 2}throw new grpc::RpcException(new 
grpc::Status(grpc::StatusCode.Internal, $"Fory gRPC deserialization failed: 
{{error.Message}}"));',
+            f"{self.indent_str}}}",
+            "",
+            f"{self.indent_str}[{no_inline}]",
+            f"{self.indent_str}private static T __ThrowTrailingBody<T>()",
+            f"{self.indent_str}{{",
+            f'{self.indent_str * 2}throw new grpc::RpcException(new 
grpc::Status(grpc::StatusCode.Internal, "Fory gRPC message contained trailing 
bytes"));',
+            f"{self.indent_str}}}",
+            "",
+            f"{self.indent_str}[{no_inline}]",
+            f"{self.indent_str}private static 
global::System.Threading.Tasks.Task<T> __ThrowUnimplemented<T>()",
+            f"{self.indent_str}{{",
+            f'{self.indent_str * 2}throw new grpc::RpcException(new 
grpc::Status(grpc::StatusCode.Unimplemented, ""));',
+            f"{self.indent_str}}}",
+            "",
+            f"{self.indent_str}[{no_inline}]",
+            f"{self.indent_str}private static 
global::System.Threading.Tasks.Task __ThrowUnimplemented()",
+            f"{self.indent_str}{{",
+            f'{self.indent_str * 2}throw new grpc::RpcException(new 
grpc::Status(grpc::StatusCode.Unimplemented, ""));',
+            f"{self.indent_str}}}",
+        ]
+
+    def _validate_csharp_services(self, services: List[Service]) -> None:
+        namespace_name = self.get_csharp_namespace()
+        top_level_types = self._top_level_type_names(namespace_name)
+        module_names = self._module_names(namespace_name)
+        service_names: Dict[str, str] = {}
+        for service in services:
+            service_name = self.safe_type_identifier(service.name)
+            prior = service_names.get(service_name)
+            if prior is not None:
+                raise ValueError(
+                    f"C# gRPC service class collision: {prior} and 
{service.name} both generate {service_name}"
+                )
+            service_names[service_name] = service.name
+            if service_name in top_level_types:
+                raise ValueError(
+                    f"C# gRPC service class {service_name} conflicts with 
generated type in namespace {namespace_name}"
+                )
+            if service_name in module_names:
+                raise ValueError(
+                    f"C# gRPC service class {service_name} conflicts with 
generated module in namespace {namespace_name}"
+                )
+            if service_name in {"BindService", "NewInstance"}:
+                raise ValueError(
+                    f"C# gRPC service class {service.name} conflicts with 
generated member {service_name}"
+                )
+            self._validate_method_names(service)
+
+    def _validate_method_names(self, service: Service) -> None:
+        client_name = self._client_type_name(service).lstrip("@")
+        base_name = self._base_type_name(service).lstrip("@")
+        fixed = {"BindService", "NewInstance", client_name, base_name}
+        client_members: Dict[str, str] = {}
+        base_members: Dict[str, str] = {}
+        for method in service.methods:
+            method_name = self._public_method_name(method.name)
+            raw_name = method_name.lstrip("@")
+            if raw_name in fixed:
+                raise ValueError(
+                    f"C# gRPC method {service.name}.{method.name} conflicts 
with generated member {raw_name}"
+                )
+            self._reserve_member(base_members, raw_name, method.name, 
service.name)
+            self._reserve_member(client_members, raw_name, method.name, 
service.name)
+            if streaming_mode(method) is StreamingMode.UNARY:
+                async_name = self._append_identifier(method_name, 
"Async").lstrip("@")
+                self._reserve_member(
+                    client_members, async_name, f"{method.name} async", 
service.name
+                )
+
+    def _reserve_member(
+        self, members: Dict[str, str], name: str, source: str, service_name: 
str
+    ) -> None:
+        prior = members.get(name)
+        if prior is not None:
+            raise ValueError(
+                f"C# gRPC method name collision in service {service_name}: 
{prior} and {source} both generate {name}"
+            )
+        members[name] = source
+
+    def _top_level_type_names(self, namespace_name: str) -> Set[str]:
+        names: Set[str] = set()
+        for type_def in self.schema.enums + self.schema.unions + 
self.schema.messages:
+            if self._type_namespace(type_def) == namespace_name:
+                names.add(self.safe_type_identifier(type_def.name))
+        return names
+
+    def _module_names(self, namespace_name: str) -> Set[str]:
+        names = {self.safe_type_identifier(self.get_module_class_name())}
+        for module_namespace, module_name in self._collect_imported_modules():
+            if module_namespace == namespace_name:
+                names.add(self.safe_type_identifier(module_name))
+        return names
+
+    def _method_type(self, method) -> str:
+        return {
+            StreamingMode.UNARY: "grpc::MethodType.Unary",
+            StreamingMode.SERVER_STREAMING: "grpc::MethodType.ServerStreaming",
+            StreamingMode.CLIENT_STREAMING: "grpc::MethodType.ClientStreaming",
+            StreamingMode.BIDIRECTIONAL: "grpc::MethodType.DuplexStreaming",
+        }[streaming_mode(method)]
+
+    def _public_method_name(self, name: str) -> str:
+        return self.safe_member_name(name)
+
+    def _method_field_name(self, name: str) -> str:
+        return f"__Method_{self._private_identifier(name)}"
+
+    def _client_type_name(self, service: Service) -> str:
+        return self.safe_type_identifier(f"{service.name}Client")
+
+    def _base_type_name(self, service: Service) -> str:
+        return self.safe_type_identifier(f"{service.name}Base")
+
+    def _private_identifier(self, value: str) -> str:
+        identifier = re.sub(r"[^0-9A-Za-z_]", "_", value.replace("global::", 
""))
+        identifier = re.sub(r"_+", "_", identifier).strip("_")
+        if not identifier or identifier[0].isdigit():
+            identifier = f"_{identifier}"
+        return identifier
+
+    def _append_identifier(self, identifier: str, suffix: str) -> str:
+        if identifier.startswith("@"):
+            return f"@{identifier[1:]}{suffix}"
+        return f"{identifier}{suffix}"
diff --git a/compiler/fory_compiler/tests/test_csharp_generator.py 
b/compiler/fory_compiler/tests/test_csharp_generator.py
index 190dc7cac..307eb88fa 100644
--- a/compiler/fory_compiler/tests/test_csharp_generator.py
+++ b/compiler/fory_compiler/tests/test_csharp_generator.py
@@ -20,11 +20,12 @@
 import warnings
 from pathlib import Path
 
-from fory_compiler.cli import resolve_imports
+from fory_compiler.cli import main as foryc_main, 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
+from fory_compiler.ir.validator import SchemaValidator
 
 
 def parse_schema(source: str):
@@ -54,7 +55,7 @@ def test_csharp_namespace_option_used():
     assert "public sealed partial class Payment" in file.content
 
 
-def test_csharp_namespace_fallback_to_package():
+def test_csharp_namespace_uses_package():
     file = generate(
         """
         package com.example.models;
@@ -123,7 +124,7 @@ def test_csharp_semantic_model_attributes():
     assert "[ForyObject]" not in file.content
 
 
-def test_csharp_registration_uses_fdl_package_for_name_registration():
+def test_csharp_registers_fdl_name():
     file = generate(
         """
         package myapp.models;
@@ -161,7 +162,7 @@ def test_csharp_field_encoding_attributes():
     assert "public int Plain { get; set; }" in file.content
 
 
-def test_csharp_nested_schema_type_attributes():
+def test_csharp_nested_type_attrs():
     file = generate(
         """
         package example;
@@ -207,7 +208,7 @@ def test_csharp_reduced_precision_carriers():
     assert "public List<BFloat16> Bf16Values { get; set; } = new();" in 
file.content
 
 
-def test_csharp_imported_registration_calls_generated():
+def test_csharp_imported_modules():
     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])
@@ -219,6 +220,194 @@ def test_csharp_imported_registration_calls_generated():
     assert "global::tree.TreeForyModule.Install(fory);" in file.content
 
 
+def test_csharp_module_uses_source_file(tmp_path: Path):
+    schema_file = tmp_path / "order-events.fdl"
+    schema_file.write_text(
+        """
+        package app.events;
+        option csharp_namespace = "App.Events";
+
+        message Event {
+            string name = 1;
+        }
+        """
+    )
+
+    schema = resolve_imports(schema_file)
+    validator = SchemaValidator(schema)
+    assert validator.validate(), validator.errors
+    file = CSharpGenerator(schema, 
GeneratorOptions(output_dir=tmp_path)).generate()[0]
+
+    assert file.path == "App/Events/order-events.cs"
+    assert "public static class OrderEventsForyModule" in file.content
+    assert "public static class EventsForyModule" not in file.content
+
+
+def test_csharp_import_modules_distinct(tmp_path: Path):
+    first = tmp_path / "first.fdl"
+    first.write_text(
+        """
+        package shared;
+        option csharp_namespace = "Demo.Shared";
+
+        message First {
+            string name = 1;
+        }
+        """
+    )
+    second = tmp_path / "second.fdl"
+    second.write_text(
+        """
+        package shared;
+        option csharp_namespace = "Demo.Shared";
+
+        message Second {
+            string name = 1;
+        }
+        """
+    )
+    main = tmp_path / "main.fdl"
+    main.write_text(
+        """
+        package app;
+        option csharp_namespace = "Demo.App";
+
+        import "first.fdl";
+        import "second.fdl";
+
+        message Holder {
+            First first = 1;
+            Second second = 2;
+        }
+        """
+    )
+
+    schema = resolve_imports(main, [tmp_path])
+    validator = SchemaValidator(schema)
+    assert validator.validate(), validator.errors
+    file = CSharpGenerator(schema, 
GeneratorOptions(output_dir=tmp_path)).generate()[0]
+
+    assert "global::Demo.Shared.FirstForyModule.Install(fory);" in file.content
+    assert "global::Demo.Shared.SecondForyModule.Install(fory);" in 
file.content
+
+
+def test_csharp_grpc_path_collision(tmp_path: Path, capsys):
+    schema_file = tmp_path / "GreeterGrpc.fdl"
+    schema_file.write_text(
+        """
+        package demo.collision;
+
+        message Req {}
+        message Res {}
+
+        service Greeter {
+            rpc Call (Req) returns (Res);
+        }
+        """
+    )
+    out = tmp_path / "out"
+
+    result = foryc_main(
+        [
+            "--lang",
+            "csharp",
+            "--csharp_out",
+            str(out),
+            "--grpc",
+            str(schema_file),
+        ]
+    )
+
+    captured = capsys.readouterr()
+    assert result == 1
+    assert "C# generated file path collision" in captured.err
+    assert not out.exists()
+
+
+def test_csharp_module_collision(tmp_path: Path, capsys):
+    first = tmp_path / "foo-bar.fdl"
+    first.write_text(
+        """
+        package demo.same;
+
+        message First {
+            string name = 1;
+        }
+        """
+    )
+    second = tmp_path / "foo_bar.fdl"
+    second.write_text(
+        """
+        package demo.same;
+
+        message Second {
+            string name = 1;
+        }
+        """
+    )
+    out = tmp_path / "out"
+
+    result = foryc_main(
+        [
+            "--lang",
+            "csharp",
+            "--csharp_out",
+            str(out),
+            str(first),
+            str(second),
+        ]
+    )
+
+    captured = capsys.readouterr()
+    assert result == 1
+    assert "C# schema module owner collision" in captured.err
+    assert not out.exists()
+
+
+def test_csharp_output_collision(tmp_path: Path, capsys):
+    first_dir = tmp_path / "first"
+    second_dir = tmp_path / "second"
+    first_dir.mkdir()
+    second_dir.mkdir()
+    first = first_dir / "common.fdl"
+    first.write_text(
+        """
+        package demo.same;
+
+        message First {
+            string name = 1;
+        }
+        """
+    )
+    second = second_dir / "common.fdl"
+    second.write_text(
+        """
+        package demo.same;
+
+        message Second {
+            string name = 1;
+        }
+        """
+    )
+    out = tmp_path / "out"
+
+    result = foryc_main(
+        [
+            "--lang",
+            "csharp",
+            "--csharp_out",
+            str(out),
+            str(first),
+            str(second),
+        ]
+    )
+
+    captured = capsys.readouterr()
+    assert result == 1
+    assert "C# generated file path collision" in captured.err
+    assert not out.exists()
+
+
 def test_csharp_namespace_option_is_known():
     source = """
     package myapp;
diff --git a/compiler/fory_compiler/tests/test_service_codegen.py 
b/compiler/fory_compiler/tests/test_service_codegen.py
index f2e794ba7..2376d1264 100644
--- a/compiler/fory_compiler/tests/test_service_codegen.py
+++ b/compiler/fory_compiler/tests/test_service_codegen.py
@@ -18,12 +18,14 @@
 """Codegen smoke tests for schemas that contain service definitions."""
 
 from pathlib import Path
+import shutil
+import subprocess
 from textwrap import dedent
 from typing import Dict, Tuple, Type
 
 import pytest
 
-from fory_compiler.cli import compile_file, resolve_imports
+from fory_compiler.cli import compile_file, main as foryc_main, resolve_imports
 from fory_compiler.frontend.fdl.lexer import Lexer
 from fory_compiler.frontend.fdl.parser import Parser
 from fory_compiler.frontend.fbs.lexer import Lexer as FbsLexer
@@ -119,7 +121,7 @@ def generate_service_files(
     return {item.path: item.content for item in generator.generate_services()}
 
 
-def test_service_definition_does_not_affect_message_codegen():
+def test_services_do_not_change_models():
     schema_with = parse_fdl(_GREETER_WITH_SERVICE)
     schema_without = parse_fdl(_GREETER_WITHOUT_SERVICE)
     for generator_cls in GENERATOR_CLASSES:
@@ -130,13 +132,14 @@ def 
test_service_definition_does_not_affect_message_codegen():
         )
 
 
-def test_generate_services_returns_empty_list_for_unsupported_generators():
+def test_unsupported_generators_no_services():
     schema = parse_fdl(_GREETER_WITH_SERVICE)
     for generator_cls in GENERATOR_CLASSES:
         if generator_cls in (
             JavaGenerator,
             PythonGenerator,
             GoGenerator,
+            CSharpGenerator,
             RustGenerator,
             KotlinGenerator,
         ):
@@ -148,7 +151,7 @@ def 
test_generate_services_returns_empty_list_for_unsupported_generators():
         )
 
 
-def test_java_grpc_service_codegen_contains_fory_marshaller():
+def test_java_grpc_fory_marshaller():
     schema = parse_fdl(_GREETER_WITH_SERVICE)
     files = generate_service_files(schema, JavaGenerator)
     assert set(files) == {"demo/greeter/GreeterGrpc.java"}
@@ -163,7 +166,7 @@ def 
test_java_grpc_service_codegen_contains_fory_marshaller():
     assert "ProtoUtils" not in content
 
 
-def test_python_grpc_service_codegen_uses_byte_callbacks():
+def test_python_grpc_byte_callbacks():
     schema = parse_fdl(_GREETER_WITH_SERVICE)
     files = generate_service_files(schema, PythonGenerator)
     assert set(files) == {"demo_greeter_grpc.py"}
@@ -183,7 +186,7 @@ def test_python_grpc_service_codegen_uses_byte_callbacks():
     assert "FromString" not in content
 
 
-def test_kotlin_grpc_service_codegen_contains_fory_marshaller():
+def test_kotlin_grpc_fory_marshaller():
     schema = parse_fdl(_GREETER_WITH_SERVICE)
     files = generate_service_files(schema, KotlinGenerator)
     assert set(files) == {"demo/greeter/GreeterGrpcKt.kt"}
@@ -219,6 +222,46 @@ def 
test_kotlin_grpc_service_codegen_contains_fory_marshaller():
     assert "MessageLite" not in content
 
 
+def test_csharp_grpc_fory_marshaller():
+    schema = parse_fdl(_GREETER_WITH_SERVICE)
+    files = generate_service_files(schema, CSharpGenerator)
+    assert set(files) == {"demo/greeter/GreeterGrpc.cs"}
+    content = files["demo/greeter/GreeterGrpc.cs"]
+    assert "public static partial class Greeter" in content
+    assert 'static readonly string __ServiceName = "demo.greeter.Greeter"' in 
content
+    assert (
+        "private static readonly global::Apache.Fory.ThreadSafeFory __Fory" in 
content
+    )
+    assert "demoGreeterForyModule" not in content
+    assert "grpc::Marshallers.Create(__Serialize_" in content
+    assert "context.Complete(__Fory.Serialize<" in content
+    assert "PayloadAsReadOnlySequence()" in content
+    assert "PayloadAsNewBuffer" not in content
+    assert "new grpc::Method<global::demo.greeter.HelloRequest" in content
+    assert "grpc::MethodType.Unary" in content
+    assert '[grpc::BindServiceMethod(typeof(Greeter), "BindService")]' in 
content
+    assert (
+        "public partial class GreeterClient : grpc::ClientBase<GreeterClient>"
+        in content
+    )
+    assert "protected override GreeterClient NewInstance" in content
+    assert "public static grpc::ServerServiceDefinition BindService" in content
+    assert (
+        "public static void BindService(grpc::ServiceBinderBase serviceBinder"
+        in content
+    )
+    assert "serviceImpl == null" in content
+    assert "new grpc::UnaryServerMethod" in content
+    assert "MethodImplOptions.NoInlining" in content
+    assert "ProtoUtils" not in content
+    hot_path = content.split("__ThrowSerializeError", 1)[0]
+    assert '$"' not in hot_path
+    assert "System.Reflection" not in content
+    assert "ValueTuple" not in content
+    assert "Tuple<" not in content
+    assert "Activator" not in content
+
+
 def test_grpc_streaming_method_shapes():
     schema = parse_fdl(
         dedent(
@@ -287,6 +330,32 @@ def test_grpc_streaming_method_shapes():
     assert "io.grpc.kotlin.ServerCalls.clientStreamingServerMethodDefinition" 
in kotlin
     assert "io.grpc.kotlin.ServerCalls.bidiStreamingServerMethodDefinition" in 
kotlin
 
+    csharp = next(iter(generate_service_files(schema, 
CSharpGenerator).values()))
+    assert "grpc::MethodType.Unary" in csharp
+    assert "grpc::MethodType.ServerStreaming" in csharp
+    assert "grpc::MethodType.ClientStreaming" in csharp
+    assert "grpc::MethodType.DuplexStreaming" in csharp
+    assert "public virtual global::demo.streams.Res Unary(" in csharp
+    assert (
+        "public virtual grpc::AsyncUnaryCall<global::demo.streams.Res> 
UnaryAsync"
+        in csharp
+    )
+    assert (
+        "public virtual 
grpc::AsyncServerStreamingCall<global::demo.streams.Res> Server"
+        in csharp
+    )
+    assert (
+        "public virtual 
grpc::AsyncClientStreamingCall<global::demo.streams.Req, 
global::demo.streams.Res> Client"
+        in csharp
+    )
+    assert (
+        "public virtual 
grpc::AsyncDuplexStreamingCall<global::demo.streams.Payload, 
global::demo.streams.Payload> Bidi"
+        in csharp
+    )
+    assert "new grpc::ServerStreamingServerMethod" in csharp
+    assert "new grpc::ClientStreamingServerMethod" in csharp
+    assert "new grpc::DuplexStreamingServerMethod" in csharp
+
 
 def test_go_grpc_service_codegen():
     schema = parse_fdl(_GREETER_WITH_SERVICE)
@@ -310,7 +379,7 @@ def test_go_grpc_service_codegen():
     assert "mustEmbedUnimplementedGreeterServer()" in content
 
 
-def test_go_grpc_service_desc_uses_idl_method_names():
+def test_go_grpc_uses_idl_names():
     schema = parse_fdl(
         dedent(
             """
@@ -346,7 +415,7 @@ def test_go_grpc_service_desc_uses_idl_method_names():
     assert 'StreamName:\t"StreamReplies"' not in content
 
 
-def test_java_outer_classname_service_references_nested_model_types():
+def test_java_outer_service_types():
     schema = parse_fdl(
         dedent(
             """
@@ -370,7 +439,7 @@ def 
test_java_outer_classname_service_references_nested_model_types():
     assert "marshaller(OuterModels.Res.class)" in content
 
 
-def test_grpc_services_use_imported_java_type_references(tmp_path: Path):
+def test_grpc_imported_java_types(tmp_path: Path):
     common = tmp_path / "common.fdl"
     common.write_text(
         dedent(
@@ -432,8 +501,20 @@ def 
test_grpc_services_use_imported_java_type_references(tmp_path: Path):
     assert "marshaller(common.Shared::class.java)" in kotlin
     assert "public open suspend fun get(request: common.Shared): Local" in 
kotlin
 
+    csharp_files = generate_service_files(schema, CSharpGenerator)
+    assert set(csharp_files) == {"api/ApiServiceGrpc.cs"}
+    csharp = csharp_files["api/ApiServiceGrpc.cs"]
+    assert (
+        "grpc::Method<global::common.Shared, global::api.Local> __Method_Get" 
in csharp
+    )
+    assert "grpc::Marshaller<global::common.Shared>" in csharp
+    assert (
+        "public virtual global::System.Threading.Tasks.Task<global::api.Local> 
Get("
+        in csharp
+    )
 
-def test_proto_grpc_services_use_imported_qualified_type_references(tmp_path: 
Path):
+
+def test_proto_grpc_imported_types(tmp_path: Path):
     common = tmp_path / "common.proto"
     common.write_text(
         dedent(
@@ -490,8 +571,15 @@ def 
test_proto_grpc_services_use_imported_qualified_type_references(tmp_path: Pa
     assert "io.grpc.MethodDescriptor<common.Shared, Local>" in kotlin
     assert "marshaller(common.Shared::class.java)" in kotlin
 
+    csharp_files = generate_service_files(schema, CSharpGenerator)
+    csharp = csharp_files["api/ApiServiceGrpc.cs"]
+    assert (
+        "grpc::Method<global::common.Shared, global::api.Local> __Method_Get" 
in csharp
+    )
+    assert "grpc::Marshaller<global::common.Shared>" in csharp
+
 
-def test_proto_grpc_absolute_rpc_type_uses_package_type_not_nested_shadow():
+def test_proto_grpc_absolute_type():
     schema = parse_proto(
         dedent(
             """
@@ -530,8 +618,13 @@ def 
test_proto_grpc_absolute_rpc_type_uses_package_type_not_nested_shadow():
     assert "io.grpc.MethodDescriptor<Request, Response>" in kotlin
     assert "io.grpc.MethodDescriptor<demo.Request, Response>" not in kotlin
 
+    csharp_files = generate_service_files(schema, CSharpGenerator)
+    csharp = csharp_files["demo/ApiServiceGrpc.cs"]
+    assert "grpc::Method<global::demo.Request, global::demo.Response>" in 
csharp
+    assert "global::demo.demo.Request" not in csharp
 
-def test_proto_grpc_absolute_rpc_type_prefers_longest_package_prefix(tmp_path: 
Path):
+
+def test_proto_grpc_longest_package(tmp_path: Path):
     common = tmp_path / "common.proto"
     common.write_text(
         dedent(
@@ -586,8 +679,13 @@ def 
test_proto_grpc_absolute_rpc_type_prefers_longest_package_prefix(tmp_path: P
     assert "io.grpc.MethodDescriptor<alpha.beta.C, alpha.beta.C>" in kotlin
     assert "io.grpc.MethodDescriptor<beta.C, beta.C>" not in kotlin
 
+    csharp_files = generate_service_files(schema, CSharpGenerator)
+    csharp = csharp_files["alpha/ApiServiceGrpc.cs"]
+    assert "grpc::Method<global::alpha.beta.C, global::alpha.beta.C>" in csharp
+    assert "global::alpha.beta.beta.C" not in csharp
+
 
-def test_java_grpc_service_class_collision_fails():
+def test_java_grpc_class_collision():
     schema = parse_fdl(
         dedent(
             """
@@ -614,7 +712,7 @@ def test_java_grpc_service_class_collision_fails():
         raise AssertionError("Expected Java gRPC service class collision")
 
 
-def test_kotlin_grpc_service_class_collision_fails():
+def test_kotlin_grpc_class_collision():
     schema = parse_fdl(
         dedent(
             """
@@ -634,7 +732,30 @@ def test_kotlin_grpc_service_class_collision_fails():
         KotlinGenerator(schema, GeneratorOptions(output_dir=Path("/tmp"), 
grpc=True))
 
 
-def test_java_grpc_service_class_collision_with_imported_type_fails(tmp_path: 
Path):
+def test_csharp_grpc_class_collision():
+    schema = parse_fdl(
+        dedent(
+            """
+            package demo.collision;
+
+            message Greeter {}
+            message Req {}
+            message Res {}
+
+            service Greeter {
+                rpc Call (Req) returns (Res);
+            }
+            """
+        )
+    )
+    generator = CSharpGenerator(
+        schema, GeneratorOptions(output_dir=Path("/tmp"), grpc=True)
+    )
+    with pytest.raises(ValueError, match="C# gRPC service class Greeter 
conflicts"):
+        generator.generate_services()
+
+
+def test_java_grpc_import_type_collision(tmp_path: Path):
     common = tmp_path / "common.fdl"
     common.write_text(
         dedent(
@@ -675,7 +796,7 @@ def 
test_java_grpc_service_class_collision_with_imported_type_fails(tmp_path: Pa
         raise AssertionError("Expected imported Java gRPC service class 
collision")
 
 
-def test_java_grpc_service_class_collision_with_imported_outer_fails(tmp_path: 
Path):
+def test_java_grpc_import_outer_collision(tmp_path: Path):
     common = tmp_path / "common.fdl"
     common.write_text(
         dedent(
@@ -786,8 +907,19 @@ def test_grpc_method_name_collisions_fail():
     else:
         raise AssertionError("Expected Kotlin gRPC method name collision")
 
+    csharp_generator = CSharpGenerator(
+        schema, GeneratorOptions(output_dir=Path("/tmp"), grpc=True)
+    )
+    try:
+        csharp_generator.generate_services()
+    except ValueError as e:
+        assert "C# gRPC method name collision" in str(e)
+        assert "Foo and foo" in str(e)
+    else:
+        raise AssertionError("Expected C# gRPC method name collision")
 
-def test_java_python_grpc_method_keywords_are_safe_names():
+
+def test_grpc_method_keywords_safe():
     schema = parse_fdl(
         dedent(
             """
@@ -820,7 +952,7 @@ def test_java_python_grpc_method_keywords_are_safe_names():
     assert "implementation = ::`class`" in kotlin
 
 
-def test_python_grpc_service_registration_collisions_fail():
+def test_python_grpc_registration_collision():
     schema = parse_fdl(
         dedent(
             """
@@ -843,7 +975,7 @@ def test_python_grpc_service_registration_collisions_fail():
         raise AssertionError("Expected Python gRPC service registration 
collision")
 
 
-def test_rust_grpc_service_module_collisions_fail():
+def test_rust_grpc_module_collision():
     schema = parse_fdl(
         dedent(
             """
@@ -866,7 +998,7 @@ def test_rust_grpc_service_module_collisions_fail():
         raise AssertionError("Expected Rust gRPC service module collision")
 
 
-def test_default_package_java_grpc_output_path_and_service_name():
+def test_java_grpc_default_package():
     schema = parse_fdl(
         dedent(
             """
@@ -934,7 +1066,7 @@ def test_proto_and_fbs_grpc_service_codegen():
     assert '"/demo.fbs.FbsSvc/Call"' in fbs_python["demo_fbs_grpc.py"]
 
 
-def test_service_schema_produces_one_file_per_message_per_language():
+def test_service_schema_model_files():
     schema = parse_fdl(_GREETER_WITH_SERVICE)
     for generator_cls in GENERATOR_CLASSES:
         files = generate_files(schema, generator_cls)
@@ -943,7 +1075,7 @@ def 
test_service_schema_produces_one_file_per_message_per_language():
         )
 
 
-def test_compile_service_schema_with_grpc_flag(tmp_path: Path, capsys):
+def test_compile_service_with_grpc(tmp_path: Path, capsys):
     example_path = Path(__file__).resolve().parents[2] / "examples" / 
"service.fdl"
     lang_dirs = {}
     for lang in ("java", "python", "rust", "go", "cpp", "csharp", "swift", 
"kotlin"):
@@ -960,10 +1092,132 @@ def 
test_compile_service_schema_with_grpc_flag(tmp_path: Path, capsys):
     assert output.count("demo_greeter_grpc.go") == 1
     assert (lang_dirs["rust"] / "demo_greeter_service.rs").exists()
     assert (lang_dirs["rust"] / "demo_greeter_service_grpc.rs").exists()
+    assert (lang_dirs["csharp"] / "demo" / "greeter" / "service.cs").exists()
+    assert (lang_dirs["csharp"] / "demo" / "greeter" / 
"GreeterGrpc.cs").exists()
     assert (lang_dirs["kotlin"] / "demo" / "greeter" / 
"GreeterGrpcKt.kt").exists()
 
 
-def test_generated_message_contains_key_signatures():
[email protected](shutil.which("dotnet") is None, reason="dotnet not 
installed")
+def test_csharp_grpc_dotnet_fixture(tmp_path: Path):
+    repo_root = Path(__file__).resolve().parents[3]
+    common = tmp_path / "common.fdl"
+    common.write_text(
+        dedent(
+            """
+            package demo.shared;
+            option csharp_namespace = "Demo.Shared";
+
+            message SharedRequest {
+                string name = 1;
+            }
+
+            message SharedReply {
+                string text = 1;
+            }
+
+            union SharedChoice {
+                SharedRequest request = 1;
+                SharedReply reply = 2;
+            }
+            """
+        )
+    )
+    main = tmp_path / "main.fdl"
+    main.write_text(
+        dedent(
+            """
+            package demo.greeter;
+            option csharp_namespace = "Demo.Greeter";
+
+            import "common.fdl";
+
+            message LocalRequest {
+                string name = 1;
+            }
+
+            message LocalReply {
+                string text = 1;
+            }
+
+            union LocalChoice {
+                LocalRequest request = 1;
+                LocalReply reply = 2;
+            }
+
+            service Greeter {
+                rpc Unary (LocalRequest) returns (LocalReply);
+                rpc Server (LocalRequest) returns (stream LocalReply);
+                rpc Client (stream SharedRequest) returns (SharedReply);
+                rpc Bidi (stream LocalChoice) returns (stream SharedChoice);
+            }
+            """
+        )
+    )
+    out = tmp_path / "out"
+    assert (
+        foryc_main(
+            [
+                "--lang",
+                "csharp",
+                "--csharp_out",
+                str(out),
+                "--grpc",
+                str(common),
+                str(main),
+            ]
+        )
+        == 0
+    )
+
+    service = out / "Demo" / "Greeter" / "GreeterGrpc.cs"
+    main_model = out / "Demo" / "Greeter" / "main.cs"
+    common_model = out / "Demo" / "Shared" / "common.cs"
+    assert service.is_file()
+    assert main_model.is_file()
+    assert common_model.is_file()
+    service_code = service.read_text()
+    assert "MainForyModule.GetFory()" in service_code
+    assert "global::Demo.Shared.SharedChoice" in service_code
+    assert "PayloadAsNewBuffer" not in service_code
+
+    project = out / "GrpcValidation.csproj"
+    project.write_text(
+        dedent(
+            f"""
+            <Project Sdk="Microsoft.NET.Sdk">
+              <PropertyGroup>
+                <OutputType>Exe</OutputType>
+                <TargetFramework>net8.0</TargetFramework>
+                <LangVersion>12.0</LangVersion>
+                <ImplicitUsings>enable</ImplicitUsings>
+                <Nullable>enable</Nullable>
+                <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+              </PropertyGroup>
+              <ItemGroup>
+                <ProjectReference Include="{repo_root / 
"csharp/src/Fory/Fory.csproj"}" />
+                <ProjectReference Include="{repo_root / 
"csharp/src/Fory.Generator/Fory.Generator.csproj"}"
+                                  OutputItemType="Analyzer"
+                                  ReferenceOutputAssembly="false" />
+                <PackageReference Include="Grpc.Core.Api" Version="2.71.0" />
+              </ItemGroup>
+            </Project>
+            """
+        ).strip()
+    )
+    (out / "Program.cs").write_text(CSHARP_GRPC_VALIDATION_PROGRAM)
+
+    result = subprocess.run(
+        ["dotnet", "run", "--project", str(project), "-v:quiet"],
+        cwd=repo_root,
+        text=True,
+        capture_output=True,
+        timeout=180,
+        check=False,
+    )
+    assert result.returncode == 0, result.stdout + result.stderr
+
+
+def test_generated_message_signatures():
     schema = parse_fdl(_GREETER_WITH_SERVICE)
     java_files = generate_files(schema, JavaGenerator)
     all_java = "\n".join(java_files.values())
@@ -978,7 +1232,650 @@ def test_generated_message_contains_key_signatures():
     assert "HelloReply" in all_python
 
 
-def test_rust_grpc_rejects_non_thread_safe_refs():
+CSHARP_GRPC_VALIDATION_PROGRAM = dedent(
+    r"""
+    using System;
+    using System.Buffers;
+    using System.Collections.Generic;
+    using System.Reflection;
+    using System.Threading;
+    using System.Threading.Tasks;
+    using Apache.Fory;
+    using Demo.Greeter;
+    using Demo.Shared;
+    using Grpc.Core;
+
+    static class Program
+    {
+        static async Task Main()
+        {
+            TestMarshallers();
+            TestClientDispatch();
+            await TestServerBinding();
+            TestGeneratedSerializers();
+            TestAllocationBaseline();
+        }
+
+        static void TestMarshallers()
+        {
+            Method<LocalRequest, LocalReply> unary =
+                GetMethod<LocalRequest, LocalReply>("__Method_Unary");
+            Method<SharedRequest, SharedReply> client =
+                GetMethod<SharedRequest, SharedReply>("__Method_Client");
+            Method<LocalChoice, SharedChoice> bidi =
+                GetMethod<LocalChoice, SharedChoice>("__Method_Bidi");
+
+            LocalRequest localRequest = new() { Name = "local" };
+            LocalRequest decodedLocal = RoundTrip(unary.RequestMarshaller, 
localRequest);
+            Require(decodedLocal.Name == "local", "local request marshaller");
+
+            LocalReply localReply = new() { Text = "reply" };
+            LocalReply decodedReply = RoundTrip(unary.ResponseMarshaller, 
localReply);
+            Require(decodedReply.Text == "reply", "local response marshaller");
+
+            SharedRequest sharedRequest = new() { Name = "shared" };
+            SharedRequest decodedShared =
+                RoundTrip(client.RequestMarshaller, sharedRequest);
+            Require(decodedShared.Name == "shared", "imported request 
marshaller");
+
+            SharedReply sharedReply = new() { Text = "shared-reply" };
+            SharedReply decodedSharedReply =
+                RoundTrip(client.ResponseMarshaller, sharedReply);
+            Require(
+                decodedSharedReply.Text == "shared-reply",
+                "imported response marshaller");
+
+            LocalChoice localUnion =
+                new LocalChoice.Request(new LocalRequest { Name = 
"union-local" });
+            LocalChoice decodedLocalUnion =
+                RoundTrip(bidi.RequestMarshaller, localUnion);
+            Require(
+                decodedLocalUnion is LocalChoice.Request { Value.Name: 
"union-local" },
+                "local union marshaller");
+
+            SharedChoice sharedUnion =
+                new SharedChoice.Reply(new SharedReply { Text = "union-shared" 
});
+            SharedChoice decodedSharedUnion =
+                RoundTrip(bidi.ResponseMarshaller, sharedUnion);
+            Require(
+                decodedSharedUnion is SharedChoice.Reply { Value.Text: 
"union-shared" },
+                "imported union marshaller");
+
+            byte[] bytes = Serialize(unary.RequestMarshaller, localRequest);
+            byte[] trailing = new byte[bytes.Length + 1];
+            Array.Copy(bytes, trailing, bytes.Length);
+            RpcException error = RequireThrows<RpcException>(() =>
+                unary.RequestMarshaller.ContextualDeserializer(
+                    new BytesDeserializationContext(trailing)));
+            Require(error.StatusCode == StatusCode.Internal, "trailing bytes 
rejected");
+        }
+
+        static void TestClientDispatch()
+        {
+            CapturingInvoker invoker = new();
+            Greeter.GreeterClient client = new(invoker);
+            LocalRequest request = new() { Name = "client" };
+
+            client.Unary(request);
+            client.UnaryAsync(request);
+            client.Server(request);
+            client.Client();
+            client.Bidi();
+
+            Require(invoker.Calls.Count == 5, "client dispatch count");
+            Require(invoker.Calls[0] == "Unary:Unary", "blocking unary 
dispatch");
+            Require(invoker.Calls[1] == "AsyncUnary:Unary", "async unary 
dispatch");
+            Require(
+                invoker.Calls[2] == "ServerStreaming:Server",
+                "server streaming dispatch");
+            Require(
+                invoker.Calls[3] == "ClientStreaming:Client",
+                "client streaming dispatch");
+            Require(
+                invoker.Calls[4] == "DuplexStreaming:Bidi",
+                "duplex streaming dispatch");
+        }
+
+        static async Task TestServerBinding()
+        {
+            CapturingBinder metadataBinder = new();
+            Greeter.BindService(metadataBinder, null);
+            Require(metadataBinder.Handlers.Count == 4, "metadata binding 
count");
+            Require(
+                metadataBinder.Handlers.TrueForAll(static handler => handler 
is null),
+                "metadata binding uses null handlers");
+
+            CapturingBinder binder = new();
+            Greeter.BindService(binder, new ServiceImpl());
+            Require(binder.Handlers.Count == 4, "implementation binding 
count");
+            Require(binder.Methods[0].Name == "Unary", "unary method name");
+            Require(
+                binder.Methods[1].Type == MethodType.ServerStreaming,
+                "server stream method type");
+            Require(
+                binder.Methods[2].Type == MethodType.ClientStreaming,
+                "client stream method type");
+            Require(
+                binder.Methods[3].Type == MethodType.DuplexStreaming,
+                "duplex method type");
+
+            var unary =
+                (UnaryServerMethod<LocalRequest, 
LocalReply>)binder.Handlers[0]!;
+            LocalReply reply = await unary(
+                new LocalRequest { Name = "bound" },
+                TestServerCallContext.Instance);
+            Require(reply.Text == "bound", "unary delegate dispatch");
+
+            var server =
+                (ServerStreamingServerMethod<LocalRequest, LocalReply>)
+                    binder.Handlers[1]!;
+            CapturingServerWriter<LocalReply> serverWriter = new();
+            await server(
+                new LocalRequest { Name = "stream" },
+                serverWriter,
+                TestServerCallContext.Instance);
+            Require(
+                serverWriter.Messages.Count == 1
+                    && serverWriter.Messages[0].Text == "stream",
+                "server stream dispatch");
+
+            var client =
+                (ClientStreamingServerMethod<SharedRequest, SharedReply>)
+                    binder.Handlers[2]!;
+            SharedReply clientReply = await client(
+                new ArrayStreamReader<SharedRequest>(
+                    new SharedRequest { Name = "a" },
+                    new SharedRequest { Name = "b" }),
+                TestServerCallContext.Instance);
+            Require(clientReply.Text == "a,b", "client stream dispatch");
+
+            var duplex =
+                (DuplexStreamingServerMethod<LocalChoice, SharedChoice>)
+                    binder.Handlers[3]!;
+            CapturingServerWriter<SharedChoice> duplexWriter = new();
+            await duplex(
+                new ArrayStreamReader<LocalChoice>(
+                    new LocalChoice.Request(new LocalRequest { Name = "duplex" 
})),
+                duplexWriter,
+                TestServerCallContext.Instance);
+            Require(duplexWriter.Messages.Count == 1, "duplex stream 
dispatch");
+            Require(
+                duplexWriter.Messages[0]
+                    is SharedChoice.Reply { Value.Text: "duplex" },
+                "duplex union response");
+        }
+
+        static void TestGeneratedSerializers()
+        {
+            Fory fory = Fory.Builder().TrackRef(true).Build();
+            MainForyModule.Install(fory);
+            TypeResolver resolver = Resolver(fory);
+            Require(IsGeneratedSerializer<LocalRequest>(resolver), "local 
message serializer");
+            Require(IsGeneratedSerializer<LocalChoice>(resolver), "local union 
serializer");
+            Require(IsGeneratedSerializer<SharedRequest>(resolver), "imported 
message serializer");
+            Require(IsGeneratedSerializer<SharedChoice>(resolver), "imported 
union serializer");
+        }
+
+        static void TestAllocationBaseline()
+        {
+            Method<LocalRequest, LocalReply> method =
+                GetMethod<LocalRequest, LocalReply>("__Method_Unary");
+            LocalRequest request = new() { Name = "allocation" };
+            BytesSerializationContext serialization = new();
+            BytesDeserializationContext deserialization = 
new(Array.Empty<byte>());
+            Apache.Fory.ThreadSafeFory fory = MainForyModule.GetFory();
+
+            for (int i = 0; i < 256; i++)
+            {
+                DirectRoundTrip(fory, request);
+                GeneratedRoundTrip(method.RequestMarshaller, request, 
serialization, deserialization);
+            }
+
+            GC.Collect();
+            GC.WaitForPendingFinalizers();
+            GC.Collect();
+            long direct = Measure(2048, () => DirectRoundTrip(fory, request));
+            long generated = Measure(
+                2048,
+                () => GeneratedRoundTrip(
+                    method.RequestMarshaller,
+                    request,
+                    serialization,
+                    deserialization));
+            Require(
+                generated <= direct + 32768,
+                $"generated marshaller allocated {generated} bytes vs direct 
{direct}");
+        }
+
+        static bool IsGeneratedSerializer<T>(TypeResolver resolver)
+        {
+            string name = resolver.GetSerializer<T>().GetType().Name;
+            return name.Contains("__ForySerializer_", 
StringComparison.Ordinal);
+        }
+
+        static TypeResolver Resolver(Fory fory)
+        {
+            FieldInfo field = typeof(Fory).GetField(
+                "_typeResolver",
+                BindingFlags.NonPublic | BindingFlags.Instance)
+                ?? throw new InvalidOperationException("Fory resolver field 
not found");
+            return (TypeResolver)field.GetValue(fory)!;
+        }
+
+        static void DirectRoundTrip(
+            Apache.Fory.ThreadSafeFory fory,
+            LocalRequest request)
+        {
+            byte[] bytes = fory.Serialize<LocalRequest>(in request);
+            ReadOnlySequence<byte> sequence = new(bytes);
+            LocalRequest decoded = fory.Deserialize<LocalRequest>(ref 
sequence);
+            Require(decoded.Name == request.Name, "direct roundtrip");
+        }
+
+        static void GeneratedRoundTrip(
+            Marshaller<LocalRequest> marshaller,
+            LocalRequest request,
+            BytesSerializationContext serialization,
+            BytesDeserializationContext deserialization)
+        {
+            serialization.Reset();
+            marshaller.ContextualSerializer(request, serialization);
+            deserialization.Reset(serialization.Payload);
+            LocalRequest decoded = 
marshaller.ContextualDeserializer(deserialization);
+            Require(decoded.Name == request.Name, "generated roundtrip");
+            Require(
+                deserialization.ReadOnlySequenceCalls == 1
+                    && deserialization.NewBufferCalls == 0,
+                "generated deserializer used read-only sequence");
+        }
+
+        static long Measure(int iterations, Action action)
+        {
+            long before = GC.GetAllocatedBytesForCurrentThread();
+            for (int i = 0; i < iterations; i++)
+            {
+                action();
+            }
+            return GC.GetAllocatedBytesForCurrentThread() - before;
+        }
+
+        static Method<TRequest, TResponse> GetMethod<TRequest, 
TResponse>(string name)
+            where TRequest : class
+            where TResponse : class
+        {
+            FieldInfo field = typeof(Greeter).GetField(
+                name,
+                BindingFlags.NonPublic | BindingFlags.Static)
+                ?? throw new InvalidOperationException(
+                    $"Missing generated method field {name}");
+            return (Method<TRequest, TResponse>)field.GetValue(null)!;
+        }
+
+        static T RoundTrip<T>(Marshaller<T> marshaller, T value)
+        {
+            BytesDeserializationContext deserialization =
+                new(Serialize(marshaller, value));
+            T decoded = marshaller.ContextualDeserializer(deserialization);
+            Require(
+                deserialization.ReadOnlySequenceCalls == 1
+                    && deserialization.NewBufferCalls == 0,
+                "deserializer used read-only sequence");
+            return decoded;
+        }
+
+        static byte[] Serialize<T>(Marshaller<T> marshaller, T value)
+        {
+            BytesSerializationContext context = new();
+            marshaller.ContextualSerializer(value, context);
+            return context.Payload;
+        }
+
+        static TException RequireThrows<TException>(Action action)
+            where TException : Exception
+        {
+            try
+            {
+                action();
+            }
+            catch (TException error)
+            {
+                return error;
+            }
+            throw new InvalidOperationException($"Expected 
{typeof(TException).Name}");
+        }
+
+        static void Require(bool condition, string message)
+        {
+            if (!condition)
+            {
+                throw new InvalidOperationException(message);
+            }
+        }
+    }
+
+    sealed class BytesSerializationContext : SerializationContext
+    {
+        readonly ArrayBufferWriter<byte> _writer = new();
+
+        public byte[] Payload { get; private set; } = Array.Empty<byte>();
+
+        public void Reset()
+        {
+            Payload = Array.Empty<byte>();
+        }
+
+        public override void Complete(byte[] payload)
+        {
+            Payload = payload;
+        }
+
+        public override IBufferWriter<byte> GetBufferWriter()
+        {
+            return _writer;
+        }
+
+        public override void SetPayloadLength(int payloadLength)
+        {
+        }
+
+        public override void Complete()
+        {
+            Payload = _writer.WrittenMemory.ToArray();
+        }
+    }
+
+    sealed class BytesDeserializationContext(byte[] payload) : 
DeserializationContext
+    {
+        byte[] _payload = payload;
+
+        public int NewBufferCalls { get; private set; }
+        public int ReadOnlySequenceCalls { get; private set; }
+        public override int PayloadLength => _payload.Length;
+
+        public void Reset(byte[] payload)
+        {
+            _payload = payload;
+            NewBufferCalls = 0;
+            ReadOnlySequenceCalls = 0;
+        }
+
+        public override byte[] PayloadAsNewBuffer()
+        {
+            NewBufferCalls++;
+            return (byte[])_payload.Clone();
+        }
+
+        public override ReadOnlySequence<byte> PayloadAsReadOnlySequence()
+        {
+            ReadOnlySequenceCalls++;
+            return new ReadOnlySequence<byte>(_payload);
+        }
+    }
+
+    sealed class CapturingInvoker : CallInvoker
+    {
+        public List<string> Calls { get; } = [];
+
+        public override TResponse BlockingUnaryCall<TRequest, TResponse>(
+            Method<TRequest, TResponse> method,
+            string? host,
+            CallOptions options,
+            TRequest request)
+        {
+            Calls.Add($"Unary:{method.Name}");
+            return Create<TResponse>();
+        }
+
+        public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, 
TResponse>(
+            Method<TRequest, TResponse> method,
+            string? host,
+            CallOptions options,
+            TRequest request)
+        {
+            Calls.Add($"AsyncUnary:{method.Name}");
+            return new AsyncUnaryCall<TResponse>(
+                Task.FromResult(Create<TResponse>()),
+                Task.FromResult(new Metadata()),
+                static () => Status.DefaultSuccess,
+                static () => new Metadata(),
+                static () => { });
+        }
+
+        public override AsyncServerStreamingCall<TResponse>
+            AsyncServerStreamingCall<TRequest, TResponse>(
+                Method<TRequest, TResponse> method,
+                string? host,
+                CallOptions options,
+                TRequest request)
+        {
+            Calls.Add($"ServerStreaming:{method.Name}");
+            return new AsyncServerStreamingCall<TResponse>(
+                new ArrayStreamReader<TResponse>(),
+                Task.FromResult(new Metadata()),
+                static () => Status.DefaultSuccess,
+                static () => new Metadata(),
+                static () => { });
+        }
+
+        public override AsyncClientStreamingCall<TRequest, TResponse>
+            AsyncClientStreamingCall<TRequest, TResponse>(
+                Method<TRequest, TResponse> method,
+                string? host,
+                CallOptions options)
+        {
+            Calls.Add($"ClientStreaming:{method.Name}");
+            return new AsyncClientStreamingCall<TRequest, TResponse>(
+                new ClientStreamWriter<TRequest>(),
+                Task.FromResult(Create<TResponse>()),
+                Task.FromResult(new Metadata()),
+                static () => Status.DefaultSuccess,
+                static () => new Metadata(),
+                static () => { });
+        }
+
+        public override AsyncDuplexStreamingCall<TRequest, TResponse>
+            AsyncDuplexStreamingCall<TRequest, TResponse>(
+                Method<TRequest, TResponse> method,
+                string? host,
+                CallOptions options)
+        {
+            Calls.Add($"DuplexStreaming:{method.Name}");
+            return new AsyncDuplexStreamingCall<TRequest, TResponse>(
+                new ClientStreamWriter<TRequest>(),
+                new ArrayStreamReader<TResponse>(),
+                Task.FromResult(new Metadata()),
+                static () => Status.DefaultSuccess,
+                static () => new Metadata(),
+                static () => { });
+        }
+
+        static T Create<T>()
+        {
+            return Activator.CreateInstance<T>();
+        }
+    }
+
+    sealed class CapturingBinder : ServiceBinderBase
+    {
+        public List<IMethod> Methods { get; } = [];
+        public List<Delegate?> Handlers { get; } = [];
+
+        public override void AddMethod<TRequest, TResponse>(
+            Method<TRequest, TResponse> method,
+            UnaryServerMethod<TRequest, TResponse>? handler)
+        {
+            Methods.Add(method);
+            Handlers.Add(handler);
+        }
+
+        public override void AddMethod<TRequest, TResponse>(
+            Method<TRequest, TResponse> method,
+            ClientStreamingServerMethod<TRequest, TResponse>? handler)
+        {
+            Methods.Add(method);
+            Handlers.Add(handler);
+        }
+
+        public override void AddMethod<TRequest, TResponse>(
+            Method<TRequest, TResponse> method,
+            ServerStreamingServerMethod<TRequest, TResponse>? handler)
+        {
+            Methods.Add(method);
+            Handlers.Add(handler);
+        }
+
+        public override void AddMethod<TRequest, TResponse>(
+            Method<TRequest, TResponse> method,
+            DuplexStreamingServerMethod<TRequest, TResponse>? handler)
+        {
+            Methods.Add(method);
+            Handlers.Add(handler);
+        }
+    }
+
+    sealed class ServiceImpl : Greeter.GreeterBase
+    {
+        public override Task<LocalReply> Unary(
+            LocalRequest request,
+            ServerCallContext context)
+        {
+            return Task.FromResult(new LocalReply { Text = request.Name });
+        }
+
+        public override async Task Server(
+            LocalRequest request,
+            IServerStreamWriter<LocalReply> responseStream,
+            ServerCallContext context)
+        {
+            await responseStream.WriteAsync(new LocalReply { Text = 
request.Name });
+        }
+
+        public override async Task<SharedReply> Client(
+            IAsyncStreamReader<SharedRequest> requestStream,
+            ServerCallContext context)
+        {
+            List<string> names = [];
+            while (await requestStream.MoveNext(CancellationToken.None))
+            {
+                names.Add(requestStream.Current.Name);
+            }
+            return new SharedReply { Text = string.Join(",", names) };
+        }
+
+        public override async Task Bidi(
+            IAsyncStreamReader<LocalChoice> requestStream,
+            IServerStreamWriter<SharedChoice> responseStream,
+            ServerCallContext context)
+        {
+            while (await requestStream.MoveNext(CancellationToken.None))
+            {
+                string name = requestStream.Current switch
+                {
+                    LocalChoice.Request request => request.Value.Name,
+                    LocalChoice.Reply reply => reply.Value.Text,
+                    _ => "unknown",
+                };
+                await responseStream.WriteAsync(
+                    new SharedChoice.Reply(new SharedReply { Text = name }));
+            }
+        }
+    }
+
+    sealed class ArrayStreamReader<T>(params T[] items) : IAsyncStreamReader<T>
+    {
+        int _index = -1;
+
+        public T Current { get; private set; } = default!;
+
+        public Task<bool> MoveNext(CancellationToken cancellationToken)
+        {
+            _index++;
+            if (_index >= items.Length)
+            {
+                return Task.FromResult(false);
+            }
+            Current = items[_index];
+            return Task.FromResult(true);
+        }
+    }
+
+    sealed class CapturingServerWriter<T> : IServerStreamWriter<T>
+    {
+        public List<T> Messages { get; } = [];
+        public WriteOptions? WriteOptions { get; set; }
+
+        public Task WriteAsync(T message)
+        {
+            Messages.Add(message);
+            return Task.CompletedTask;
+        }
+
+        public Task WriteAsync(T message, CancellationToken cancellationToken)
+        {
+            Messages.Add(message);
+            return Task.CompletedTask;
+        }
+    }
+
+    sealed class ClientStreamWriter<T> : IClientStreamWriter<T>
+    {
+        public WriteOptions? WriteOptions { get; set; }
+
+        public Task CompleteAsync()
+        {
+            return Task.CompletedTask;
+        }
+
+        public Task WriteAsync(T message)
+        {
+            return Task.CompletedTask;
+        }
+
+        public Task WriteAsync(T message, CancellationToken cancellationToken)
+        {
+            return Task.CompletedTask;
+        }
+    }
+
+    sealed class TestServerCallContext : ServerCallContext
+    {
+        public static readonly TestServerCallContext Instance = new();
+        readonly Dictionary<object, object> _userState = [];
+        Metadata _trailers = [];
+        Status _status = Status.DefaultSuccess;
+
+        protected override string MethodCore => "test";
+        protected override string HostCore => "localhost";
+        protected override string PeerCore => "peer";
+        protected override DateTime DeadlineCore => 
DateTime.UtcNow.AddMinutes(1);
+        protected override Metadata RequestHeadersCore => [];
+        protected override CancellationToken CancellationTokenCore =>
+            CancellationToken.None;
+        protected override Metadata ResponseTrailersCore => _trailers;
+        protected override Status StatusCore { get => _status; set => _status 
= value; }
+        protected override WriteOptions? WriteOptionsCore { get; set; }
+        protected override AuthContext AuthContextCore =>
+            new("none", new Dictionary<string, List<AuthProperty>>());
+
+        protected override ContextPropagationToken CreatePropagationTokenCore(
+            ContextPropagationOptions? options)
+        {
+            throw new NotSupportedException();
+        }
+
+        protected override Task WriteResponseHeadersAsyncCore(
+            Metadata responseHeaders)
+        {
+            return Task.CompletedTask;
+        }
+
+        protected override IDictionary<object, object> UserStateCore => 
_userState;
+    }
+    """
+).lstrip()
+
+
+def test_rust_grpc_rejects_unsafe_refs():
     cases = [
         (
             "Rust gRPC payload type Request.node uses non-thread-safe ref",
diff --git a/docs/compiler/compiler-guide.md b/docs/compiler/compiler-guide.md
index 683bd3417..d513ac577 100644
--- a/docs/compiler/compiler-guide.md
+++ b/docs/compiler/compiler-guide.md
@@ -141,24 +141,26 @@ foryc schema.fdl --output ./src/generated
 foryc user.fdl order.fdl product.fdl --output ./generated
 ```
 
-**Compile a simple schema containing service definitions (Java + Python + Rust 
+ Kotlin models):**
+**Compile a simple schema containing service definitions (Java + Python + Rust 
+ C# + Kotlin models):**
 
 ```bash
-foryc compiler/examples/service.fdl --java_out=./generated/java 
--python_out=./generated/python --rust_out=./generated/rust 
--kotlin_out=./generated/kotlin
+foryc compiler/examples/service.fdl --java_out=./generated/java 
--python_out=./generated/python --rust_out=./generated/rust 
--csharp_out=./generated/csharp --kotlin_out=./generated/kotlin
 ```
 
-**Generate Java, Python, Rust, and Kotlin gRPC service companions:**
+**Generate Java, Python, Rust, C#, and Kotlin gRPC service companions:**
 
 ```bash
-foryc compiler/examples/service.fdl --java_out=./generated/java 
--python_out=./generated/python --rust_out=./generated/rust 
--kotlin_out=./generated/kotlin --grpc
+foryc compiler/examples/service.fdl --java_out=./generated/java 
--python_out=./generated/python --rust_out=./generated/rust 
--csharp_out=./generated/csharp --kotlin_out=./generated/kotlin --grpc
 ```
 
 The generated gRPC service code uses Fory to serialize request and response
 payloads. Java output imports grpc-java APIs, Python output imports `grpc`, and
 Rust output imports `tonic` and `bytes`. Kotlin output imports grpc-java and
-grpc-kotlin APIs and uses coroutine stubs. Applications that compile or run
-those generated service files must provide their own gRPC dependencies. Fory
-packages do not add a hard gRPC dependency for this feature.
+grpc-kotlin APIs and uses coroutine stubs. C# output imports `Grpc.Core.Api`
+types and can be hosted with normal .NET gRPC packages such as
+`Grpc.AspNetCore` or called through `Grpc.Net.Client`. Applications that
+compile or run those generated service files must provide their own gRPC
+dependencies. Fory packages do not add a hard gRPC dependency for this feature.
 
 **Use import search paths:**
 
@@ -372,6 +374,8 @@ generated/
   `ToBytes`/`FromBytes` methods
 - Imported schemas are installed transitively (for example `root.idl` importing
   `addressbook.fdl` and `tree.fdl`)
+- With `--grpc`, one `<ServiceName>Grpc.cs` companion per service next to the
+  schema file output
 
 ### Swift
 
diff --git a/docs/compiler/flatbuffers-idl.md b/docs/compiler/flatbuffers-idl.md
index fa29946ac..9764dc930 100644
--- a/docs/compiler/flatbuffers-idl.md
+++ b/docs/compiler/flatbuffers-idl.md
@@ -126,8 +126,8 @@ message Container {
 
 FlatBuffers `rpc_service` definitions are translated to Fory services. With
 `--grpc`, the compiler emits gRPC service companions for supported outputs such
-as Java, Python, Go, Rust, and Kotlin. These companions use Fory serialization 
for
-request and response payloads.
+as Java, Python, Go, Rust, C#, and Kotlin. These companions use Fory
+serialization for request and response payloads.
 
 ```fbs
 rpc_service SearchService {
@@ -137,13 +137,13 @@ rpc_service SearchService {
 ```
 
 ```bash
-foryc api.fbs --java_out=./generated/java --python_out=./generated/python 
--rust_out=./generated/rust --kotlin_out=./generated/kotlin --grpc
+foryc api.fbs --java_out=./generated/java --python_out=./generated/python 
--rust_out=./generated/rust --csharp_out=./generated/csharp 
--kotlin_out=./generated/kotlin --grpc
 ```
 
 Generated service code imports grpc APIs, so applications must provide 
grpc-java,
-grpc-kotlin, `grpcio`, grpc-go, or Rust `tonic` and `bytes` dependencies when
-they compile or run those files. Fory packages do not add gRPC as a hard
-dependency.
+grpc-kotlin, `grpcio`, grpc-go, Rust `tonic` and `bytes`, or C#
+`Grpc.Core.Api` plus server/client dependencies when they compile or run those
+files. Fory packages do not add gRPC as a hard dependency.
 
 ### Defaults and Metadata
 
diff --git a/docs/compiler/generated-code.md b/docs/compiler/generated-code.md
index d197fb86f..782c941d4 100644
--- a/docs/compiler/generated-code.md
+++ b/docs/compiler/generated-code.md
@@ -937,6 +937,67 @@ the same C# namespace.
 When explicit type IDs are not provided, generated installation uses computed
 numeric IDs (same behavior as other targets).
 
+For existing generated C# call sites, this is a breaking generated-code name
+rule when the namespace leaf and source file stem differ. For example,
+`service.fdl` with `option csharp_namespace = "Demo.Greeter";` now generates
+`ServiceForyModule`, not `GreeterForyModule`. Regenerate the files and update
+manual calls such as `GreeterForyModule.Install(fory)` to
+`ServiceForyModule.Install(fory)`. No legacy alias is emitted.
+
+### gRPC Service Companions
+
+When a schema contains services and the compiler is run with `--grpc`, C#
+generation emits one `<ServiceName>Grpc.cs` file per service next to the schema
+model file.
+
+```csharp
+public static partial class AddressBookService
+{
+    public abstract partial class AddressBookServiceBase
+    {
+        public virtual Task<AddressBook> Lookup(
+            Person request,
+            grpc::ServerCallContext context) { ... }
+    }
+
+    public partial class AddressBookServiceClient
+        : grpc::ClientBase<AddressBookServiceClient>
+    {
+        public virtual AddressBook Lookup(Person request, grpc::CallOptions 
options) { ... }
+        public virtual grpc::AsyncUnaryCall<AddressBook> LookupAsync(
+            Person request,
+            grpc::CallOptions options) { ... }
+    }
+
+    public static grpc::ServerServiceDefinition BindService(
+        AddressBookServiceBase serviceImpl) { ... }
+
+    public static void BindService(
+        grpc::ServiceBinderBase serviceBinder,
+        AddressBookServiceBase? serviceImpl) { ... }
+}
+```
+
+Each generated method descriptor uses a static Fory-backed
+`Grpc.Core.Marshaller<T>` that reuses the schema module's `ThreadSafeFory`.
+Deserialization reads the gRPC body through `PayloadAsReadOnlySequence()` and
+rejects trailing bytes after the single Fory frame. Generated service 
companions
+do not use protobuf parsers and do not create Fory instances per RPC call.
+
+Streaming RPCs map to standard gRPC C# APIs:
+
+| IDL shape                                 | Server method                    
                                             | Client method                    
           |
+| ----------------------------------------- | 
----------------------------------------------------------------------------- | 
------------------------------------------- |
+| `rpc A (Req) returns (Res)`               | `Task<Res> A(Req request, 
ServerCallContext context)`                         | `A(...)` and 
`AAsync(...)`                  |
+| `rpc A (Req) returns (stream Res)`        | `Task A(Req request, 
IServerStreamWriter<Res> responseStream, ...)`           | 
`AsyncServerStreamingCall<Res> A(...)`      |
+| `rpc A (stream Req) returns (Res)`        | `Task<Res> 
A(IAsyncStreamReader<Req> requestStream, ...)`                     | 
`AsyncClientStreamingCall<Req, Res> A(...)` |
+| `rpc A (stream Req) returns (stream Res)` | `Task A(IAsyncStreamReader<Req> 
requestStream, IServerStreamWriter<Res> ...)` | `AsyncDuplexStreamingCall<Req, 
Res> A(...)` |
+
+Applications compiling generated C# service files must provide `Grpc.Core.Api`
+and their chosen .NET gRPC hosting or client package, such as `Grpc.AspNetCore`
+or `Grpc.Net.Client`. The `Apache.Fory` package does not add gRPC dependencies
+as hard dependencies.
+
 ## JavaScript/TypeScript
 
 ### Output Layout
diff --git a/docs/compiler/index.md b/docs/compiler/index.md
index f9fea15b4..4fda9db63 100644
--- a/docs/compiler/index.md
+++ b/docs/compiler/index.md
@@ -23,9 +23,9 @@ 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, C++, Go, Rust,
 JavaScript/TypeScript, C#, Swift, Dart, Scala, and Kotlin. Fory IDL can also
-describe RPC services; for Java, Python, Go, Rust, and Kotlin, the compiler can
-generate gRPC service companions that use Fory serialization for request and
-response payloads.
+describe RPC services; for Java, Python, Go, Rust, C#, and Kotlin, the compiler
+can generate gRPC service companions that use Fory serialization for request
+and response payloads.
 
 ## Example Schema
 
@@ -88,16 +88,17 @@ service AnimalService {
 }
 ```
 
-Generate Java, Python, Rust, and Kotlin models plus gRPC service companions 
with:
+Generate Java, Python, Rust, C#, and Kotlin models plus gRPC service 
companions with:
 
 ```bash
-foryc animals.fdl --java_out=./generated/java --python_out=./generated/python 
--rust_out=./generated/rust --kotlin_out=./generated/kotlin --grpc
+foryc animals.fdl --java_out=./generated/java --python_out=./generated/python 
--rust_out=./generated/rust --csharp_out=./generated/csharp 
--kotlin_out=./generated/kotlin --grpc
 ```
 
 The generated service code uses normal gRPC APIs, but request and response
 objects are serialized with Fory. Applications provide their own grpc-java,
-grpc-kotlin, `grpcio`, grpc-go, or Rust `tonic` and `bytes` dependencies; Fory
-packages do not add gRPC as a hard dependency.
+grpc-kotlin, `grpcio`, grpc-go, Rust `tonic` and `bytes`, or C#
+`Grpc.Core.Api` and hosting/client dependencies; Fory packages do not add gRPC
+as a hard dependency.
 
 ## Why Fory IDL?
 
diff --git a/docs/compiler/protobuf-idl.md b/docs/compiler/protobuf-idl.md
index 4ce825e37..92549ff75 100644
--- a/docs/compiler/protobuf-idl.md
+++ b/docs/compiler/protobuf-idl.md
@@ -41,19 +41,19 @@ how protobuf concepts map to Fory, and how to use 
protobuf-only Fory extension o
 
 ## Protobuf vs Fory at a Glance
 
-| Aspect             | Protocol Buffers              | Fory                    
                   |
-| ------------------ | ----------------------------- | 
------------------------------------------ |
-| Primary purpose    | RPC/message contracts         | High-performance object 
serialization      |
-| Encoding model     | Tag-length-value              | Fory binary protocol    
                   |
-| Reference tracking | Not built-in                  | First-class (`ref`)     
                   |
-| Circular refs      | Not supported                 | Supported               
                   |
-| Unknown fields     | Preserved                     | Not preserved           
                   |
-| Generated types    | Protobuf-specific model types | Native language 
constructs                 |
-| gRPC ecosystem     | Native                        | 
Java/Python/Go/Rust/Kotlin service codegen |
-
-Fory can generate Java, Python, Go, Rust, and Kotlin gRPC service companions 
with
-`--grpc`. Those services use normal gRPC transports but serialize request and
-response payloads with Fory rather than protobuf. For broad gRPC ecosystem
+| Aspect             | Protocol Buffers              | Fory                    
                      |
+| ------------------ | ----------------------------- | 
--------------------------------------------- |
+| Primary purpose    | RPC/message contracts         | High-performance object 
serialization         |
+| Encoding model     | Tag-length-value              | Fory binary protocol    
                      |
+| Reference tracking | Not built-in                  | First-class (`ref`)     
                      |
+| Circular refs      | Not supported                 | Supported               
                      |
+| Unknown fields     | Preserved                     | Not preserved           
                      |
+| Generated types    | Protobuf-specific model types | Native language 
constructs                    |
+| gRPC ecosystem     | Native                        | 
Java/Python/Go/Rust/C#/Kotlin service codegen |
+
+Fory can generate Java, Python, Go, Rust, C#, and Kotlin gRPC service 
companions
+with `--grpc`. Those services use normal gRPC transports but serialize request
+and response payloads with Fory rather than protobuf. For broad gRPC ecosystem
 tooling, schema reflection, and protobuf-native interceptors, protobuf remains
 the mature/default choice.
 
@@ -314,16 +314,17 @@ languages.
 For supported service outputs, add `--grpc` to emit gRPC companion code:
 
 ```bash
-foryc api.proto --java_out=./generated/java --python_out=./generated/python 
--rust_out=./generated/rust --kotlin_out=./generated/kotlin --grpc
+foryc api.proto --java_out=./generated/java --python_out=./generated/python 
--rust_out=./generated/rust --csharp_out=./generated/csharp 
--kotlin_out=./generated/kotlin --grpc
 ```
 
 Generated Java service files compile against grpc-java, generated Python 
service
 modules import `grpc`, generated Rust service files import `tonic` and `bytes`,
-and generated Kotlin service files compile against grpc-java and grpc-kotlin.
-Add those dependencies in your application build; Fory packages do not add gRPC
-as a hard dependency. Protobuf `oneof` fields are translated to Fory union
-fields inside request and response messages. Direct union RPC request or
-response types are not part of normal protobuf RPC syntax.
+generated C# service files import `Grpc.Core.Api` types, and generated Kotlin
+service files compile against grpc-java and grpc-kotlin. Add those dependencies
+in your application build; Fory packages do not add gRPC as a hard dependency.
+Protobuf `oneof` fields are translated to Fory union fields inside request and
+response messages. Direct union RPC request or response types are not part of
+normal protobuf RPC syntax.
 
 ### Step 5: Run Compatibility Checks
 
diff --git a/docs/compiler/schema-idl.md b/docs/compiler/schema-idl.md
index 40f082108..56399b8bd 100644
--- a/docs/compiler/schema-idl.md
+++ b/docs/compiler/schema-idl.md
@@ -908,7 +908,7 @@ union_field := ['repeated'] field_type IDENTIFIER '=' 
INTEGER [field_options] ';
 Services define RPC method contracts in Fory IDL. They are optional: schemas
 with services still generate the normal data model types, and gRPC service code
 is generated only when the compiler is run with `--grpc` for supported language
-outputs such as Java, Python, Go, Rust, and Kotlin.
+outputs such as Java, Python, Go, Rust, C#, and Kotlin.
 
 ```protobuf
 message GetPetRequest [id=200] {
@@ -950,8 +950,8 @@ service PetDirectory {
   of a service contract.
 - The generated gRPC companions use Fory serialization for each RPC payload.
   Applications that compile or run those companions provide their own gRPC
-  dependency, such as grpc-java, grpc-kotlin, `grpcio`, grpc-go, or Rust
-  `tonic` and `bytes`.
+  dependency, such as grpc-java, grpc-kotlin, `grpcio`, grpc-go, Rust `tonic`
+  and `bytes`, or C# `Grpc.Core.Api` plus a server or client package.
 
 **Grammar:**
 
diff --git a/docs/guide/csharp/grpc-support.md 
b/docs/guide/csharp/grpc-support.md
new file mode 100644
index 000000000..cb246b7d7
--- /dev/null
+++ b/docs/guide/csharp/grpc-support.md
@@ -0,0 +1,220 @@
+---
+title: gRPC Support
+sidebar_position: 12
+id: grpc_support
+license: |
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+---
+
+Fory can generate C# gRPC service companions for schemas that define services.
+The generated code uses normal gRPC clients, service bases, method descriptors,
+metadata, deadlines, cancellations, and status codes, while request and 
response
+objects are serialized with Fory instead of protobuf.
+
+Use this mode when both RPC peers are generated from the same Fory IDL,
+protobuf IDL, or FlatBuffers IDL and both sides expect Fory-encoded message
+bodies. Use normal protobuf gRPC generation for APIs that must be consumed by
+generic protobuf clients, reflection tools, or components that expect protobuf
+message bytes.
+
+## Add Dependencies
+
+The `Apache.Fory` package does not add gRPC dependencies. Add the gRPC packages
+in the application that compiles or runs generated service companions.
+
+Server project:
+
+```xml
+<ItemGroup>
+  <PackageReference Include="Apache.Fory" Version="1.2.0-dev" />
+  <PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
+</ItemGroup>
+```
+
+Client project:
+
+```xml
+<ItemGroup>
+  <PackageReference Include="Apache.Fory" Version="1.2.0-dev" />
+  <PackageReference Include="Grpc.Core.Api" Version="2.71.0" />
+  <PackageReference Include="Grpc.Net.Client" Version="2.71.0" />
+</ItemGroup>
+```
+
+`Grpc.Core.Api` is the API surface used by generated companions. Server and
+client applications can choose their normal gRPC hosting or transport packages.
+
+## Define a Service
+
+Service definitions can come from Fory IDL, protobuf IDL, or FlatBuffers
+`rpc_service` definitions. A Fory IDL service looks like this:
+
+```protobuf
+package demo.greeter;
+option csharp_namespace = "Demo.Greeter";
+
+message HelloRequest {
+  string name = 1;
+}
+
+message HelloReply {
+  string reply = 1;
+}
+
+service Greeter {
+  rpc SayHello (HelloRequest) returns (HelloReply);
+}
+```
+
+Generate C# model and gRPC companion code with `--grpc`:
+
+```bash
+foryc service.fdl --csharp_out=./Generated --grpc
+```
+
+For this schema, the C# generator emits:
+
+| File                                        | Purpose                        
              |
+| ------------------------------------------- | 
-------------------------------------------- |
+| `Demo/Greeter/service.cs`                   | Fory model types and schema 
module           |
+| `Demo/Greeter/GreeterGrpc.cs`               | gRPC service base, client, and 
descriptors   |
+| `ServiceForyModule` in `service.cs`         | Fory registration module for 
generated types |
+| `Greeter.GreeterBase` in `GreeterGrpc.cs`   | Base class for server 
implementations        |
+| `Greeter.GreeterClient` in `GreeterGrpc.cs` | Client stub for gRPC calls     
              |
+
+## Implement a Server
+
+Extend the generated `Greeter.GreeterBase` class and map it through normal
+ASP.NET Core gRPC hosting:
+
+```csharp
+using Demo.Greeter;
+using Grpc.Core;
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddGrpc();
+
+var app = builder.Build();
+app.MapGrpcService<GreeterService>();
+app.Run();
+
+public sealed class GreeterService : Greeter.GreeterBase
+{
+    public override Task<HelloReply> SayHello(
+        HelloRequest request,
+        ServerCallContext context)
+    {
+        return Task.FromResult(new HelloReply
+        {
+            Reply = "Hello, " + request.Name,
+        });
+    }
+}
+```
+
+Generated request and response types are registered by the generated schema
+module used by the service companion, so service implementations do not perform
+manual serializer registration.
+
+## Create a Client
+
+Use the generated client with a `Grpc.Net.Client` call invoker:
+
+```csharp
+using Demo.Greeter;
+using Grpc.Net.Client;
+
+using GrpcChannel channel = GrpcChannel.ForAddress("https://localhost:5001";);
+var client = new Greeter.GreeterClient(channel.CreateCallInvoker());
+
+HelloReply reply = await client.SayHelloAsync(
+    new HelloRequest { Name = "Fory" });
+Console.WriteLine(reply.Reply);
+```
+
+The generated client also exposes synchronous unary methods and the normal
+gRPC streaming call shapes.
+
+## Streaming RPCs
+
+Fory service definitions can use the same gRPC streaming shapes:
+
+```protobuf
+service Greeter {
+  rpc SayHello (HelloRequest) returns (HelloReply);
+  rpc LotsOfReplies (HelloRequest) returns (stream HelloReply);
+  rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply);
+  rpc Chat (stream HelloRequest) returns (stream HelloReply);
+}
+```
+
+Generated C# service methods follow gRPC C# conventions:
+
+| IDL shape                                 | Server method                    
                                             | Client method                    
                  |
+| ----------------------------------------- | 
----------------------------------------------------------------------------- | 
-------------------------------------------------- |
+| `rpc A (Req) returns (Res)`               | `Task<Res> A(Req request, 
ServerCallContext context)`                         | `Res A(...)` and 
`AsyncUnaryCall<Res> AAsync(...)` |
+| `rpc A (Req) returns (stream Res)`        | `Task A(Req request, 
IServerStreamWriter<Res> responseStream, ...)`           | 
`AsyncServerStreamingCall<Res> A(...)`             |
+| `rpc A (stream Req) returns (Res)`        | `Task<Res> 
A(IAsyncStreamReader<Req> requestStream, ...)`                     | 
`AsyncClientStreamingCall<Req, Res> A(...)`        |
+| `rpc A (stream Req) returns (stream Res)` | `Task A(IAsyncStreamReader<Req> 
requestStream, IServerStreamWriter<Res> ...)` | `AsyncDuplexStreamingCall<Req, 
Res> A(...)`        |
+
+The generated descriptors preserve the exact IDL service and method names for
+the gRPC path.
+
+## Generated Module Names
+
+C# schema modules are named from the source file stem, not from the namespace.
+This lets several schema files target the same C# namespace without colliding.
+For example, `service.fdl` with `option csharp_namespace = "Demo.Greeter";`
+generates `ServiceForyModule`.
+
+If you previously called a namespace-derived module name, update that call 
after
+regenerating:
+
+```csharp
+// Before
+GreeterForyModule.Install(fory);
+
+// After
+ServiceForyModule.Install(fory);
+```
+
+No legacy alias is generated.
+
+## Operations
+
+The generated service code only replaces request and response serialization.
+All normal gRPC operational features still belong to your gRPC stack:
+
+- Deadlines and cancellations
+- TLS and authentication
+- Name resolution and load balancing
+- Client and server interceptors
+- Status codes and metadata
+- Channel pooling and lifecycle management
+
+## Troubleshooting
+
+### Missing `Grpc.Core` Types
+
+Add `Grpc.Core.Api` or a server/client package that brings it transitively.
+Generated Fory service files import gRPC APIs, but `Apache.Fory` intentionally
+does not depend on gRPC.
+
+### Protobuf Clients Cannot Decode the Service
+
+Fory gRPC companions do not use protobuf wire encoding for messages. Use a
+Fory-generated client for Fory-generated services, or expose a separate 
protobuf
+service endpoint for generic protobuf clients.
diff --git a/docs/guide/csharp/index.md b/docs/guide/csharp/index.md
index 24b33e281..fee0e7fa9 100644
--- a/docs/guide/csharp/index.md
+++ b/docs/guide/csharp/index.md
@@ -96,6 +96,7 @@ User decoded = fory.Deserialize<User>(payload);
 | [Schema Evolution](schema-evolution.md)       | Compatible mode behavior     
                 |
 | [Supported Types](supported-types.md)         | Built-in and generated type 
support           |
 | [Thread Safety](thread-safety.md)             | `Fory` vs `ThreadSafeFory` 
usage              |
+| [gRPC Support](grpc-support.md)               | Generated Fory-backed gRPC 
service companions |
 | [Troubleshooting](troubleshooting.md)         | Common errors and debugging 
steps             |
 
 ## Related Resources
diff --git a/docs/guide/csharp/troubleshooting.md 
b/docs/guide/csharp/troubleshooting.md
index 8ac2a414e..e5d6bb6bd 100644
--- a/docs/guide/csharp/troubleshooting.md
+++ b/docs/guide/csharp/troubleshooting.md
@@ -1,6 +1,6 @@
 ---
 title: Troubleshooting
-sidebar_position: 12
+sidebar_position: 13
 id: troubleshooting
 license: |
   Licensed to the Apache Software Foundation (ASF) under one or more
@@ -84,6 +84,25 @@ Fory fory = Fory.Builder().TrackRef(true).Build();
 
 **Fix**: Use `BuildThreadSafe()`.
 
+## Generated gRPC Compile Errors
+
+**Symptom**: Generated `*Grpc.cs` files cannot find `Grpc.Core` types.
+
+**Cause**: gRPC packages are application dependencies. The `Apache.Fory`
+package does not add gRPC as a hard dependency.
+
+**Fix**: Add `Grpc.Core.Api` and your chosen gRPC server or client package, 
such
+as `Grpc.AspNetCore` for server hosting or `Grpc.Net.Client` for clients. See
+[gRPC Support](grpc-support.md).
+
+## Protobuf Client Cannot Decode a Fory gRPC Service
+
+**Cause**: Fory gRPC companions use gRPC transports with Fory-encoded message
+bodies. They do not send protobuf message bytes.
+
+**Fix**: Use a Fory-generated client and server for the Fory endpoint, or 
expose
+a separate protobuf endpoint for generic protobuf clients.
+
 ## Validation Commands
 
 Run C# tests from repo root:
@@ -96,5 +115,6 @@ dotnet test Fory.sln -c Release
 ## Related Topics
 
 - [Configuration](configuration.md)
+- [gRPC Support](grpc-support.md)
 - [Schema Evolution](schema-evolution.md)
 - [Thread Safety](thread-safety.md)
diff --git a/integration_tests/grpc_tests/generate_grpc.py 
b/integration_tests/grpc_tests/generate_grpc.py
index 9f9c64332..9c45e5925 100644
--- a/integration_tests/grpc_tests/generate_grpc.py
+++ b/integration_tests/grpc_tests/generate_grpc.py
@@ -35,6 +35,7 @@ OUTPUTS = {
     "python": TEST_DIR / "python/grpc_tests/generated",
     "go": TEST_DIR / "go/generated",
     "rust": TEST_DIR / "rust/generated/src",
+    "csharp": TEST_DIR / "csharp/generated",
     "kotlin": TEST_DIR / "kotlin/src/main/kotlin/generated",
 }
 
@@ -77,6 +78,7 @@ def main() -> int:
                 f"--python_out={OUTPUTS['python']}",
                 f"--go_out={go_pkg_out}",
                 f"--rust_out={OUTPUTS['rust']}",
+                f"--csharp_out={OUTPUTS['csharp']}",
                 f"--kotlin_out={OUTPUTS['kotlin']}",
                 "--grpc",
             ],


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to