Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions docs/site/src/content/docs/implementation/serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Orleans serialization serves two related pipelines:
- value serialization, deep copying, and activation for message and storage payloads;
- RPC code generation for grain references, request objects, dispatch, and responses.

Most application code uses generated components. Reflection-based discovery is deliberately not the default architecture: generated manifests make the participating types explicit and keep runtime dispatch compatible with [trimming](https://learn.microsoft.com/dotnet/core/deploying/trimming/prepare-libraries-for-trimming) and ahead-of-time compilation.
Most application code uses generated components. Generated manifests make the participating types explicit and keep runtime dispatch compatible with [trimming](https://learn.microsoft.com/dotnet/core/deploying/trimming/prepare-libraries-for-trimming) and ahead-of-time compilation.

## Incremental generator pipeline

Expand Down Expand Up @@ -72,7 +72,17 @@ Statically closed value-type constructor delegates update the caller's `ref` val

## RPC generation

Grain-reference construction resolves the generated proxy from the registered type manifest and invokes its `(GrainReferenceShared, IdSpan)` constructor. The activator caches the constructor delegate and shares the runtime, interface version, invocation options, and serialization services across references for the same grain type and interface. Each reference retains its own grain key. JIT runtimes use an emitted constructor delegate; NativeAOT uses a cached reflection constructor invoker. Generated `AddInterfaceProxy` registrations preserve the public proxy constructor during trimming.
Generated manifests register each concrete grain interface with a typed factory which directly constructs its proxy using `(GrainReferenceShared, IdSpan)`. The runtime resolves the factory using the declared interface identity. The activator caches that factory and shares the runtime, interface version, invocation options, and serialization services across references for the same grain type and interface. Each reference retains its own grain key. Direct construction preserves proxy constructors through normal static call reachability and propagates constructor exceptions directly.

Proxy bases declare their construction delegate using <xref:Orleans.GenerateProxyFactoryAttribute>. <xref:Orleans.Runtime.GrainReference> declares <xref:Orleans.Runtime.GrainReferenceFactory>, which constructs a grain reference from shared runtime state and a grain key. Other RPC proxy bases can select their own delegate signatures. The generator emits a static `Create` method matching the declared delegate and forwards its parameters to the proxy constructor. C# compilation checks that constructor call for both non-generic and open generic proxies.

Derived proxy bases inherit the factory declaration. A declaration on a nearer base overrides the inherited signature. Factory declarations use closed delegate types with a by-value proxy result; attribute arguments follow C#'s rules for concrete type references. The generator validates the delegate and its result conversion at build time, and preserves parameter passing modes such as `ref`. Proxy hierarchies supply accessible constructors matching their declared contract.

The serialization manifest stores factories in <xref:Orleans.Serialization.Configuration.InterfaceProxyFactoryOptions`1>, parameterized by the construction delegate type. This keeps factory registration independent of grain runtime types and supports proxy hierarchies with different construction signatures. Custom proxies can register a direct typed factory in the manifest's <xref:Orleans.Serialization.Configuration.TypeManifestOptions.GetOrCreate*> configuration.

The generator automatically registers factories for non-generic grain interfaces and open proxy mappings for generic interfaces. A statically closed factory registration selects direct construction for that concrete interface before the runtime resolves open generic families. NativeAOT deployments supply a concrete factory for each generic interface they construct; generated proxy names remain assembly-local implementation details.

JIT applications also support generic arguments selected at runtime through explicitly registered open generic proxy mappings. Those mappings use reflective generic closure and a cached constructor invoker. Older generated manifests and custom proxies registered by type retain the same constructor-invoker compatibility path.

For each grain interface method, generated code captures arguments in an invokable object. The generated proxy submits that object through its proxy base. On the target, generated dispatch metadata invokes the concrete implementation and encodes the response.

Expand Down
2 changes: 2 additions & 0 deletions src/Orleans.CodeGenerator/LibraryTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ private LibraryTypes(Compilation compilation)
MethodInfo = Type("System.Reflection.MethodInfo");
Func_2 = Type("System.Func`2");
GenerateMethodSerializersAttribute = Type("Orleans.GenerateMethodSerializersAttribute");
GenerateProxyFactoryAttribute = TypeOrDefault("Orleans.GenerateProxyFactoryAttribute");
GenerateSerializerAttribute = Type("Orleans.GenerateSerializerAttribute");
SerializationCallbacksAttribute = Type("Orleans.SerializationCallbacksAttribute");
IActivator_1 = Type("Orleans.Serialization.Activators.IActivator`1");
Expand Down Expand Up @@ -217,6 +218,7 @@ INamedTypeSymbol Type(string metadataName)
public INamedTypeSymbol Func_2 { get; private set; }
public INamedTypeSymbol CompoundTypeAliasAttribute { get; private set; }
public INamedTypeSymbol GenerateMethodSerializersAttribute { get; private set; }
public INamedTypeSymbol? GenerateProxyFactoryAttribute { get; }
public INamedTypeSymbol GenerateSerializerAttribute { get; private set; }
public INamedTypeSymbol IActivator_1 { get; private set; }
public INamedTypeSymbol IBufferWriter { get; private set; }
Expand Down
24 changes: 23 additions & 1 deletion src/Orleans.CodeGenerator/MetadataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@ private ClassDeclarationSyntax GenerateIncrementalMetadata()
AddRegistration(body, addProxyMethod, GetGeneratedProxyTypeSyntax(type));
}

var factoryGroups = orderedProxyInterfaces
.Where(static proxy => !proxy.ProxyBase.FactoryType.IsEmpty)
.GroupBy(static proxy => proxy.ProxyBase.FactoryType)
.ToArray();
for (var index = 0; index < factoryGroups.Length; index++)
{
var group = factoryGroups[index];
var name = index == 0 ? "proxyFactories" : $"proxyFactories{index}";
body.Add(ParseStatement(
$"var {name} = config.GetOrCreate<global::Orleans.Serialization.Configuration.InterfaceProxyFactoryOptions<{group.Key.SyntaxString}>>();"));
foreach (var proxy in group)
{
var interfaceType = GetOpenTypeSyntax(proxy.InterfaceType);
var proxyType = GetGeneratedProxyTypeSyntax(proxy);
var factory = GetProxyGenericArity(proxy) == 0 ? $", {proxyType}.Create" : string.Empty;
body.Add(ParseStatement($"{name}.Add(typeof({interfaceType}), typeof({proxyType}){factory});"));
}
}

var addInterfaceMethod = configParam.Member("AddInterface");
foreach (var type in orderedProxyInterfaces.Select(static proxy => proxy.InterfaceType).Distinct())
{
Expand Down Expand Up @@ -585,10 +604,13 @@ private sealed class OpenGenericTypeSyntaxRewriter : CSharpSyntaxRewriter

private static TypeSyntax GetGeneratedProxyTypeSyntax(ProxyInterfaceModel proxy)
{
var genericArity = Math.Max(proxy.TypeParameters.Length, CountGenericArguments(proxy.InterfaceType));
var genericArity = GetProxyGenericArity(proxy);
return CreateGeneratedTypeSyntax(proxy.GeneratedNamespace, ProxyGenerator.GetSimpleClassName(proxy.Name), genericArity);
}

private static int GetProxyGenericArity(ProxyInterfaceModel proxy)
=> Math.Max(proxy.TypeParameters.Length, CountGenericArguments(proxy.InterfaceType));

private static int CountGenericArguments(TypeRef typeRef)
{
var typeSyntax = typeRef.ToTypeSyntax();
Expand Down
42 changes: 42 additions & 0 deletions src/Orleans.CodeGenerator/Model/ProxyInterfaceDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public ProxyInterfaceDescription(
INamedTypeSymbol interfaceType)
{
ValidateBaseClass(generationContext.LibraryTypes, proxyBaseType);
FactorySignature = GetFactorySignature(generationContext.LibraryTypes, proxyBaseType, interfaceType);

var prop = interfaceType.GetAllMembers<IPropertySymbol>().FirstOrDefault();
if (prop is { })
Expand Down Expand Up @@ -112,6 +113,47 @@ static IEnumerable<INamedTypeSymbol> GetAllInterfaces(INamedTypeSymbol s)
public string GeneratedNamespace { get; }
public List<(string Name, ITypeParameterSymbol Parameter)> TypeParameters { get; }
public INamedTypeSymbol ProxyBaseType { get; }
public IMethodSymbol? FactorySignature { get; }

private static IMethodSymbol? GetFactorySignature(LibraryTypes libraryTypes, INamedTypeSymbol proxyBaseType, INamedTypeSymbol interfaceType)
{
var attribute = libraryTypes.GenerateProxyFactoryAttribute is { } attributeType
? proxyBaseType.GetAttribute(attributeType, inherited: true)
: null;
if (attribute is null)
{
return null;
}

var location = attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ?? proxyBaseType.Locations.First();
if (attribute.ConstructorArguments.Length != 1
|| attribute.ConstructorArguments[0].Value is not INamedTypeSymbol { IsUnboundGenericType: false } factoryType
|| factoryType.DelegateInvokeMethod is not { ReturnsVoid: false, ReturnsByRef: false, ReturnsByRefReadonly: false } signature)
{
throw new OrleansGeneratorDiagnosticAnalysisException(
IncorrectProxyBaseClassSpecificationDiagnostic.CreateDiagnostic(
proxyBaseType, location, "GenerateProxyFactory requires a closed delegate with a non-void, by-value return type."));
}

var baseConversion = libraryTypes.Compilation.ClassifyCommonConversion(proxyBaseType, signature.ReturnType);
var interfaceConversion = libraryTypes.Compilation.ClassifyCommonConversion(interfaceType, signature.ReturnType);
if (!(baseConversion.IsIdentity || baseConversion.IsImplicit && baseConversion.IsReference)
&& !(interfaceConversion.IsIdentity || interfaceConversion.IsImplicit && interfaceConversion.IsReference))
{
throw new OrleansGeneratorDiagnosticAnalysisException(
IncorrectProxyBaseClassSpecificationDiagnostic.CreateDiagnostic(
proxyBaseType, location, "GenerateProxyFactory's return type must accept the generated proxy base or interface through an identity or implicit reference conversion."));
}

if (signature.Parameters.Any(static parameter => parameter.RefKind is not (RefKind.None or RefKind.Ref or RefKind.Out or RefKind.In)))
{
throw new OrleansGeneratorDiagnosticAnalysisException(
IncorrectProxyBaseClassSpecificationDiagnostic.CreateDiagnostic(
proxyBaseType, location, "GenerateProxyFactory supports by-value, ref, in, and out parameters."));
}

return signature;
}

private static void ValidateBaseClass(LibraryTypes l, INamedTypeSymbol baseClass)
{
Expand Down
3 changes: 2 additions & 1 deletion src/Orleans.CodeGenerator/Model/ProxyInterfaceModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ internal sealed record class ProxyBaseModel(
TypeRef ProxyBaseType,
bool IsExtension,
string GeneratedClassNameComponent,
TypeMetadataIdentity MetadataIdentity = default);
TypeMetadataIdentity MetadataIdentity = default,
TypeRef FactoryType = default);

/// <summary>
/// Describes a <c>[GenerateMethodSerializers]</c>-annotated interface for incremental proxy/invokable generation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,17 @@ internal static class ProxyInterfaceModelExtractor

var proxyBaseType = proxyBaseTypeSymbol.OriginalDefinition;
var generatedClassNameComponent = isExtension ? $"{proxyBaseType.Name}_Ext" : proxyBaseType.Name;
var factoryType = libraryTypes.GenerateProxyFactoryAttribute is { } factoryAttributeType
&& proxyBaseType.GetAttribute(factoryAttributeType, inherited: true) is { ConstructorArguments.Length: 1 } factoryAttribute
&& factoryAttribute.ConstructorArguments[0].Value is INamedTypeSymbol factoryDelegate
? new TypeRef(factoryDelegate.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))
: TypeRef.Empty;
var proxyBase = new ProxyBaseModel(
new TypeRef(proxyBaseType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
isExtension,
generatedClassNameComponent,
TypeMetadataIdentity.Create(proxyBaseType));
TypeMetadataIdentity.Create(proxyBaseType),
factoryType);

var name = GetProxyInterfaceName(typeSymbol, libraryTypes);
var typeParameters = ExtractInterfaceTypeParameters(typeSymbol);
Expand Down
36 changes: 36 additions & 0 deletions src/Orleans.CodeGenerator/ProxyGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal class ProxyGenerator(IGeneratorServices generatorServices, CopierGenera
var proxyMethods = CreateProxyMethods(fieldDescriptions, interfaceDescription);

var ctors = GenerateConstructors(generatedClassName, fieldDescriptions, interfaceDescription.ProxyBaseType);
var factories = GenerateFactory(generatedClassName, interfaceDescription);

var classDeclaration = ClassDeclaration(generatedClassName)
.AddBaseListTypes(
Expand All @@ -41,6 +42,7 @@ internal class ProxyGenerator(IGeneratorServices generatorServices, CopierGenera
.AddAttributeLists(GeneratedCodeUtilities.GetGeneratedCodeAttributes())
.AddMembers(fieldDeclarations)
.AddMembers(ctors)
.AddMembers(factories)
.AddMembers(proxyMethods);

var typeParameters = interfaceDescription.TypeParameters;
Expand All @@ -58,6 +60,40 @@ public static string GetSimpleClassName(ProxyInterfaceDescription interfaceDescr
public static string GetSimpleClassName(string name)
=> $"Proxy_{SyntaxGeneration.Identifier.SanitizeIdentifierName(name)}";

private MemberDeclarationSyntax[] GenerateFactory(string className, ProxyInterfaceDescription interfaceDescription)
{
if (interfaceDescription.FactorySignature is not { } signature)
{
return [];
}

var proxyType = interfaceDescription.TypeParameters.Count == 0
? (TypeSyntax)IdentifierName(className)
: GenericName(Identifier(className), TypeArgumentList(SeparatedList<TypeSyntax>(
interfaceDescription.TypeParameters.Select(static parameter => parameter.Name.ToIdentifierName()))));
return
[
MethodDeclaration(signature.ReturnType.ToTypeSyntax(), "Create")
.AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))
.AddParameterListParameters(
[.. signature.Parameters.Select((parameter, index) => GetParameterSyntax(index, parameter, typeParameterSubstitutions: null))])
.WithExpressionBody(ArrowExpressionClause(
ObjectCreationExpression(proxyType).WithArgumentList(ArgumentList(SeparatedList(
signature.Parameters.Select((parameter, index) =>
{
var argument = Argument(IdentifierName($"arg{index}"));
return parameter.RefKind switch
{
RefKind.Ref => argument.WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)),
RefKind.Out => argument.WithRefOrOutKeyword(Token(SyntaxKind.OutKeyword)),
RefKind.In => argument.WithRefOrOutKeyword(Token(SyntaxKind.InKeyword)),
_ => argument,
};
}))))))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)),
];
}

private List<GeneratedFieldDescription> GetFieldDescriptions(
ProxyInterfaceDescription interfaceDescription)
{
Expand Down
1 change: 1 addition & 0 deletions src/Orleans.Core.Abstractions/Runtime/GrainReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ internal struct GrainReferenceSurrogate
/// This is the base class for all grain references.
/// </summary>
[Alias("GrainRef")]
[GenerateProxyFactory(typeof(GrainReferenceFactory))]
[DefaultInvokableBaseType(typeof(ValueTask<>), typeof(Request<>))]
[DefaultInvokableBaseType(typeof(ValueTask), typeof(Request))]
[DefaultInvokableBaseType(typeof(Task<>), typeof(TaskRequest<>))]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Orleans.Runtime;

/// <summary>
/// Constructs a grain reference using shared runtime state and a grain key.
/// </summary>
/// <param name="shared">The runtime state shared by references for a grain type and interface.</param>
/// <param name="key">The key identifying the target grain.</param>
/// <returns>A new grain reference.</returns>
public delegate GrainReference GrainReferenceFactory(GrainReferenceShared shared, IdSpan key);
Loading
Loading