diff --git a/docs/site/src/content/docs/implementation/serialization.md b/docs/site/src/content/docs/implementation/serialization.md index 94e2c1a26ee..1ef59da555c 100644 --- a/docs/site/src/content/docs/implementation/serialization.md +++ b/docs/site/src/content/docs/implementation/serialization.md @@ -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 @@ -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 . declares , 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 , 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 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. diff --git a/src/Orleans.CodeGenerator/LibraryTypes.cs b/src/Orleans.CodeGenerator/LibraryTypes.cs index 6f17d26f2fe..d20ab2a97e0 100644 --- a/src/Orleans.CodeGenerator/LibraryTypes.cs +++ b/src/Orleans.CodeGenerator/LibraryTypes.cs @@ -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"); @@ -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; } diff --git a/src/Orleans.CodeGenerator/MetadataGenerator.cs b/src/Orleans.CodeGenerator/MetadataGenerator.cs index 57d5a520060..554721cb9cf 100644 --- a/src/Orleans.CodeGenerator/MetadataGenerator.cs +++ b/src/Orleans.CodeGenerator/MetadataGenerator.cs @@ -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>();")); + 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()) { @@ -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(); diff --git a/src/Orleans.CodeGenerator/Model/ProxyInterfaceDescription.cs b/src/Orleans.CodeGenerator/Model/ProxyInterfaceDescription.cs index 8ef0cde9b4b..31399efa468 100644 --- a/src/Orleans.CodeGenerator/Model/ProxyInterfaceDescription.cs +++ b/src/Orleans.CodeGenerator/Model/ProxyInterfaceDescription.cs @@ -17,6 +17,7 @@ public ProxyInterfaceDescription( INamedTypeSymbol interfaceType) { ValidateBaseClass(generationContext.LibraryTypes, proxyBaseType); + FactorySignature = GetFactorySignature(generationContext.LibraryTypes, proxyBaseType, interfaceType); var prop = interfaceType.GetAllMembers().FirstOrDefault(); if (prop is { }) @@ -112,6 +113,47 @@ static IEnumerable 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) { diff --git a/src/Orleans.CodeGenerator/Model/ProxyInterfaceModel.cs b/src/Orleans.CodeGenerator/Model/ProxyInterfaceModel.cs index 80a424ddc50..ec9e18e4ffb 100644 --- a/src/Orleans.CodeGenerator/Model/ProxyInterfaceModel.cs +++ b/src/Orleans.CodeGenerator/Model/ProxyInterfaceModel.cs @@ -7,7 +7,8 @@ internal sealed record class ProxyBaseModel( TypeRef ProxyBaseType, bool IsExtension, string GeneratedClassNameComponent, - TypeMetadataIdentity MetadataIdentity = default); + TypeMetadataIdentity MetadataIdentity = default, + TypeRef FactoryType = default); /// /// Describes a [GenerateMethodSerializers]-annotated interface for incremental proxy/invokable generation. diff --git a/src/Orleans.CodeGenerator/Model/ProxyInterfaceModelExtractor.cs b/src/Orleans.CodeGenerator/Model/ProxyInterfaceModelExtractor.cs index f0718e31799..9de2ff78f58 100644 --- a/src/Orleans.CodeGenerator/Model/ProxyInterfaceModelExtractor.cs +++ b/src/Orleans.CodeGenerator/Model/ProxyInterfaceModelExtractor.cs @@ -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); diff --git a/src/Orleans.CodeGenerator/ProxyGenerator.cs b/src/Orleans.CodeGenerator/ProxyGenerator.cs index 8da7ad250cf..fc3ab10fed5 100644 --- a/src/Orleans.CodeGenerator/ProxyGenerator.cs +++ b/src/Orleans.CodeGenerator/ProxyGenerator.cs @@ -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( @@ -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; @@ -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( + 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 GetFieldDescriptions( ProxyInterfaceDescription interfaceDescription) { diff --git a/src/Orleans.Core.Abstractions/Runtime/GrainReference.cs b/src/Orleans.Core.Abstractions/Runtime/GrainReference.cs index 143ee50023c..9aa6310c5a2 100644 --- a/src/Orleans.Core.Abstractions/Runtime/GrainReference.cs +++ b/src/Orleans.Core.Abstractions/Runtime/GrainReference.cs @@ -259,6 +259,7 @@ internal struct GrainReferenceSurrogate /// This is the base class for all grain references. /// [Alias("GrainRef")] + [GenerateProxyFactory(typeof(GrainReferenceFactory))] [DefaultInvokableBaseType(typeof(ValueTask<>), typeof(Request<>))] [DefaultInvokableBaseType(typeof(ValueTask), typeof(Request))] [DefaultInvokableBaseType(typeof(Task<>), typeof(TaskRequest<>))] diff --git a/src/Orleans.Core.Abstractions/Runtime/GrainReferenceFactory.cs b/src/Orleans.Core.Abstractions/Runtime/GrainReferenceFactory.cs new file mode 100644 index 00000000000..17f6f730651 --- /dev/null +++ b/src/Orleans.Core.Abstractions/Runtime/GrainReferenceFactory.cs @@ -0,0 +1,9 @@ +namespace Orleans.Runtime; + +/// +/// Constructs a grain reference using shared runtime state and a grain key. +/// +/// The runtime state shared by references for a grain type and interface. +/// The key identifying the target grain. +/// A new grain reference. +public delegate GrainReference GrainReferenceFactory(GrainReferenceShared shared, IdSpan key); diff --git a/src/Orleans.Core/GrainReferences/GrainReferenceActivator.cs b/src/Orleans.Core/GrainReferences/GrainReferenceActivator.cs index cb8cb1554cc..aeb796b509c 100644 --- a/src/Orleans.Core/GrainReferences/GrainReferenceActivator.cs +++ b/src/Orleans.Core/GrainReferences/GrainReferenceActivator.cs @@ -3,8 +3,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; -using System.Reflection.Emit; -using System.Runtime.CompilerServices; using System.Threading; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -180,12 +178,13 @@ public GrainReference CreateReference(GrainId grainId) } /// - /// Provides functionality for mapping from a to the corresponding generated proxy type. + /// Maps a to its grain-reference factory. /// internal class RpcProvider { private readonly TypeConverter _typeConverter; private readonly Dictionary _mapping; + private readonly Dictionary _factories = new(); /// /// Initializes a new instance of the class. @@ -201,8 +200,28 @@ public RpcProvider( _typeConverter = typeConverter; var proxyTypes = config.Value.InterfaceProxyTypes; _mapping = new Dictionary(); + var registrations = config.Value.GetOrCreate>(); + var registeredProxies = registrations.ProxyTypes; + foreach (var (interfaceType, registration) in registrations.Factories) + { + var id = resolver.GetGrainInterfaceType(interfaceType); + if (registration.Factory is { } factory) + { + _factories[id] = factory; + } + else + { + _mapping[id] = registration.ProxyType; + } + } + foreach (var proxyType in proxyTypes) { + if (registeredProxies.Contains(proxyType)) + { + continue; + } + if (!typeof(IAddressable).IsAssignableFrom(proxyType)) { continue; @@ -217,6 +236,7 @@ public RpcProvider( var grainInterface = GetMainInterface(type); var id = resolver.GetGrainInterfaceType(grainInterface); _mapping[id] = type; + _factories.Remove(id); } [UnconditionalSuppressMessage( @@ -251,13 +271,18 @@ static Type GetMainInterface(Type t) } /// - /// Gets the generated proxy object type corresponding to the specified . + /// Gets the factory corresponding to the specified . /// /// The grain interface type. - /// The proxy object type. - /// A value indicating whether a suitable type was found and was able to be constructed. - public bool TryGet(GrainInterfaceType interfaceType, [NotNullWhen(true)] out Type? result) + /// The grain-reference factory. + /// A value indicating whether a suitable factory was found. + public bool TryGet(GrainInterfaceType interfaceType, [NotNullWhen(true)] out GrainReferenceFactory? result) { + if (_factories.TryGetValue(interfaceType, out result)) + { + return true; + } + GrainInterfaceType lookupId; Type[]? args; if (GenericGrainInterfaceType.TryParse(interfaceType, out var genericId)) @@ -271,18 +296,34 @@ public bool TryGet(GrainInterfaceType interfaceType, [NotNullWhen(true)] out Typ args = default; } - if (!_mapping.TryGetValue(lookupId, out result)) + if (!_mapping.TryGetValue(lookupId, out var proxyType)) { + result = null; return false; } if (args is not null) { - result = result.MakeGenericType(args); + proxyType = proxyType.MakeGenericType(args); } + result = CreateLegacyFactory(proxyType); return true; } + + [UnconditionalSuppressMessage( + "Trimming", + "IL2070", + Justification = "Legacy proxy constructors are preserved by AddInterfaceProxy. Custom non-public constructors require explicit preservation.")] + private static GrainReferenceFactory CreateLegacyFactory(Type proxyType) + { + var constructor = proxyType.GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + [typeof(GrainReferenceShared), typeof(IdSpan)]) + ?? throw new SerializerException("Invalid proxy type: " + proxyType); + var invoker = ConstructorInvoker.Create(constructor); + return (shared, key) => (GrainReference)invoker.Invoke(shared, key); + } } /// @@ -324,13 +365,9 @@ public GrainReferenceActivatorProvider( } /// - [UnconditionalSuppressMessage( - "Trimming", - "IL2067", - Justification = "Generated proxies are registered using TypeManifestOptions.AddInterfaceProxy, which preserves their public constructors before they enter RpcProvider's mapping.")] public bool TryGet(GrainType grainType, GrainInterfaceType interfaceType, [NotNullWhen(true)] out IGrainReferenceActivator? activator) { - if (!_rpcProvider.TryGet(interfaceType, out var proxyType)) + if (!_rpcProvider.TryGet(interfaceType, out var factory)) { activator = default; return false; @@ -357,7 +394,7 @@ public bool TryGet(GrainType grainType, GrainInterfaceType interfaceType, [NotNu _codecProvider, _copyContextPool, _serviceProvider); - activator = new GrainReferenceActivator(proxyType, shared); + activator = new GrainReferenceActivator(factory, shared); return true; } @@ -367,41 +404,19 @@ public bool TryGet(GrainType grainType, GrainInterfaceType interfaceType, [NotNu private sealed class GrainReferenceActivator : IGrainReferenceActivator { private readonly GrainReferenceShared _shared; - private readonly Func _create; + private readonly GrainReferenceFactory _create; /// /// Initializes a new instance of the class. /// - /// The generated proxy object type. + /// The grain-reference factory. /// The functionality shared between all grain references for a specified grain type and grain interface type. - [UnconditionalSuppressMessage( - "Trimming", - "IL2070", - Justification = "Generated proxy public constructors are preserved by AddInterfaceProxy. Non-public constructors remain supported for explicitly preserved custom proxies.")] public GrainReferenceActivator( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type referenceType, + GrainReferenceFactory factory, GrainReferenceShared shared) { _shared = shared; - - var ctor = referenceType.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, new[] { typeof(GrainReferenceShared), typeof(IdSpan) }) - ?? throw new SerializerException("Invalid proxy type: " + referenceType); - - if (!RuntimeFeature.IsDynamicCodeSupported) - { - var invoker = ConstructorInvoker.Create(ctor); - _create = (shared, key) => (GrainReference)invoker.Invoke(shared, key); - return; - } - - var method = new DynamicMethod(referenceType.Name, typeof(GrainReference), new[] { typeof(object), typeof(GrainReferenceShared), typeof(IdSpan) }); - var il = method.GetILGenerator(); - // arg0 is unused for better delegate performance (avoids argument shuffling thunk) - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Newobj, ctor); - il.Emit(OpCodes.Ret); - _create = method.CreateDelegate>(); + _create = factory; } public GrainReference CreateReference(GrainId grainId) => _create(_shared, grainId.Key); diff --git a/src/Orleans.Serialization.Abstractions/GenerateProxyFactoryAttribute.cs b/src/Orleans.Serialization.Abstractions/GenerateProxyFactoryAttribute.cs new file mode 100644 index 00000000000..ae33ceda57c --- /dev/null +++ b/src/Orleans.Serialization.Abstractions/GenerateProxyFactoryAttribute.cs @@ -0,0 +1,28 @@ +using System; + +namespace Orleans; + +/// +/// Specifies the construction delegate for generated proxies derived from this base type. +/// +/// +/// The generator emits a static Create method with the delegate's signature and registers +/// it for each concrete proxy interface. The delegate must be closed and return a proxy by value. +/// Its parameters are forwarded to the generated proxy constructor. +/// Derived proxy bases inherit the declaration and can supply their own declaration to override it. +/// +[AttributeUsage(AttributeTargets.Class, Inherited = true)] +public sealed class GenerateProxyFactoryAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The delegate type describing proxy construction. + public GenerateProxyFactoryAttribute(Type delegateType) => + DelegateType = delegateType ?? throw new ArgumentNullException(nameof(delegateType)); + + /// + /// Gets the delegate type describing proxy construction. + /// + public Type DelegateType { get; } +} diff --git a/src/Orleans.Serialization/Configuration/InterfaceProxyFactoryOptions.cs b/src/Orleans.Serialization/Configuration/InterfaceProxyFactoryOptions.cs new file mode 100644 index 00000000000..1ac918c81ee --- /dev/null +++ b/src/Orleans.Serialization/Configuration/InterfaceProxyFactoryOptions.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +#if NET5_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Orleans.Serialization.Configuration; + +/// +/// Registers interface proxies and strongly typed construction delegates. +/// +/// The delegate type used to construct proxies. +public sealed class InterfaceProxyFactoryOptions where TFactory : Delegate +{ + private readonly Dictionary _factories = new(); + + /// + /// Gets proxy registrations keyed by their declared interfaces. + /// + /// + /// Concrete registrations contain a construction delegate. Generic definitions contain a proxy + /// type mapping for runtime-selected arguments. The latest registration for an interface is used. + /// + public IReadOnlyDictionary Factories => _factories; + + // Superseded registrations remain explicit proxies, so legacy discovery cannot displace their replacements. + internal HashSet ProxyTypes { get; } = new(); + + /// + /// Registers a factory for a concrete interface. + /// + /// The interface implemented by the proxy. + /// The proxy type. + /// The delegate which constructs a proxy. + public void Add(Type interfaceType, Type proxyType, TFactory factory) + { + if (interfaceType is null) + { + throw new ArgumentNullException(nameof(interfaceType)); + } + + if (proxyType is null) + { + throw new ArgumentNullException(nameof(proxyType)); + } + + if (factory is null) + { + throw new ArgumentNullException(nameof(factory)); + } + + ProxyTypes.Add(proxyType); + _factories[interfaceType] = (proxyType, factory); + } + + /// + /// Registers an open generic proxy for runtime-selected generic arguments. + /// + /// The open generic interface. + /// The open generic generated proxy type. + /// + /// Statically closed registrations added using take precedence. + /// Runtime-selected generic arguments use reflective proxy construction. + /// + public void Add( + Type interfaceType, +#if NET5_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + Type proxyType) + { + if (interfaceType is null) + { + throw new ArgumentNullException(nameof(interfaceType)); + } + + if (proxyType is null) + { + throw new ArgumentNullException(nameof(proxyType)); + } + + ProxyTypes.Add(proxyType); + _factories[interfaceType] = (proxyType, null); + } +} diff --git a/src/Orleans.Serialization/Configuration/TypeManifestOptions.cs b/src/Orleans.Serialization/Configuration/TypeManifestOptions.cs index 0b2c75ff2fb..3751d83d475 100644 --- a/src/Orleans.Serialization/Configuration/TypeManifestOptions.cs +++ b/src/Orleans.Serialization/Configuration/TypeManifestOptions.cs @@ -31,6 +31,23 @@ public sealed class TypeManifestOptions private readonly HashSet _interfaces = new(); private readonly HashSet _interfaceProxies = new(); private readonly HashSet _interfaceImplementations = new(); + private readonly Dictionary _extensions = new(); + + /// + /// Gets or creates strongly typed configuration associated with this manifest. + /// + /// The configuration type. + /// The configuration instance owned by this manifest. + public T GetOrCreate() where T : class, new() + { + if (!_extensions.TryGetValue(typeof(T), out var value)) + { + value = new T(); + _extensions.Add(typeof(T), value); + } + + return (T)value; + } /// /// Gets or sets a value indicating whether should be enabled. diff --git a/src/api/Orleans.Core.Abstractions/Orleans.Core.Abstractions.cs b/src/api/Orleans.Core.Abstractions/Orleans.Core.Abstractions.cs index 72f59d1cc80..ac668a9a964 100644 --- a/src/api/Orleans.Core.Abstractions/Orleans.Core.Abstractions.cs +++ b/src/api/Orleans.Core.Abstractions/Orleans.Core.Abstractions.cs @@ -2266,6 +2266,7 @@ public static partial class GrainLifecycleStage } [Alias("GrainRef")] + [GenerateProxyFactory(typeof(GrainReferenceFactory))] [DefaultInvokableBaseType(typeof(System.Threading.Tasks.ValueTask<>), typeof(Request<>))] [DefaultInvokableBaseType(typeof(System.Threading.Tasks.ValueTask), typeof(Request))] [DefaultInvokableBaseType(typeof(System.Threading.Tasks.Task<>), typeof(TaskRequest<>))] @@ -2318,6 +2319,7 @@ protected void Invoke(IRequest methodDescription) { } public sealed override string ToString() { throw null; } } + public delegate GrainReference GrainReferenceFactory(GrainReferenceShared shared, IdSpan key); [GenerateSerializer] public sealed partial class GrainReferenceNotBoundException : OrleansException { diff --git a/src/api/Orleans.Serialization.Abstractions/Orleans.Serialization.Abstractions.cs b/src/api/Orleans.Serialization.Abstractions/Orleans.Serialization.Abstractions.cs index 9c816e4f857..70d5b03ef77 100644 --- a/src/api/Orleans.Serialization.Abstractions/Orleans.Serialization.Abstractions.cs +++ b/src/api/Orleans.Serialization.Abstractions/Orleans.Serialization.Abstractions.cs @@ -83,6 +83,14 @@ public GenerateMethodSerializersAttribute(System.Type proxyBase, bool isExtensio public System.Type ProxyBase { get { throw null; } } } + [System.AttributeUsage(System.AttributeTargets.Class, Inherited = true)] + public sealed partial class GenerateProxyFactoryAttribute : System.Attribute + { + public GenerateProxyFactoryAttribute(System.Type delegateType) { } + + public System.Type DelegateType { get { throw null; } } + } + [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct | System.AttributeTargets.Enum)] public sealed partial class GenerateSerializerAttribute : System.Attribute { diff --git a/src/api/Orleans.Serialization/Orleans.Serialization.cs b/src/api/Orleans.Serialization/Orleans.Serialization.cs index 04e673cb8a2..0d1aeb98db8 100644 --- a/src/api/Orleans.Serialization/Orleans.Serialization.cs +++ b/src/api/Orleans.Serialization/Orleans.Serialization.cs @@ -3336,6 +3336,16 @@ public void WriteField(ref Buffers.Writer writer, namespace Orleans.Serialization.Configuration { + public sealed partial class InterfaceProxyFactoryOptions + where TFactory : System.Delegate + { + public System.Collections.Generic.IReadOnlyDictionary Factories { get { throw null; } } + + public void Add(System.Type interfaceType, System.Type proxyType, TFactory factory) { } + + public void Add(System.Type interfaceType, System.Type proxyType) { } + } + public partial interface IProviderMetadataProvider { void ConfigureProviders(System.Collections.Generic.IDictionary<(string Target, string Kind, string Name), System.Type> providers); @@ -3403,6 +3413,9 @@ public void AddInterfaceImplementation(System.Type type) { } public void AddInterfaceProxy(System.Type type) { } public void AddSerializer(System.Type type) { } + + public T GetOrCreate() + where T : class, new() { throw null; } } [System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple = true)] diff --git a/test/Orleans.CodeGenerator.Tests/GrainReferenceFactoryCodegenTests.cs b/test/Orleans.CodeGenerator.Tests/GrainReferenceFactoryCodegenTests.cs new file mode 100644 index 00000000000..cf113c1ebff --- /dev/null +++ b/test/Orleans.CodeGenerator.Tests/GrainReferenceFactoryCodegenTests.cs @@ -0,0 +1,504 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Orleans.CodeGenerator.Diagnostics; + +namespace Orleans.CodeGenerator.Tests; + +public class GrainReferenceFactoryCodegenTests +{ + [Fact] + public async Task GeneratedFactoriesUseDeclaredInterfaceAndDirectConstruction() + { + const string source = """ + using Orleans; + namespace TestProject; + public interface IBase : IGrainWithStringKey { } + public interface IDerived : IBase { } + public interface IGeneric : IBase { } + """; + + var (result, output) = Generate(await TestCompilationHelper.CreateCompilation(source)); + AssertCompiles(result, output); + var generated = GetSource(result); + Assert.Contains( + ".Add(typeof(global::TestProject.IDerived), typeof(OrleansCodeGen.TestProject.Proxy_IDerived), OrleansCodeGen.TestProject.Proxy_IDerived.Create)", + generated, StringComparison.Ordinal); + Assert.Contains( + "public static global::Orleans.Runtime.GrainReference Create(global::Orleans.Runtime.GrainReferenceShared arg0, global::Orleans.Runtime.IdSpan arg1) => new Proxy_IDerived(arg0, arg1);", + generated, StringComparison.Ordinal); + Assert.Contains(".Add(typeof(global::TestProject.IGeneric<>), typeof(OrleansCodeGen.TestProject.Proxy_IGeneric<>))", generated, StringComparison.Ordinal); + Assert.DoesNotContain("GetConstructor", generated, StringComparison.Ordinal); + Assert.DoesNotContain("GetInterfaces", generated, StringComparison.Ordinal); + Assert.Contains("InterfaceProxyFactoryOptions", generated, StringComparison.Ordinal); + } + + [Fact] + public async Task ClosedGenericFactoriesUseOrdinaryTypedRegistration() + { + const string source = """ + using Orleans; + using Orleans.Runtime; + using Orleans.Serialization.Configuration; + using Factories = Orleans.Serialization.Configuration.InterfaceProxyFactoryOptions< + Orleans.Runtime.GrainReferenceFactory>; + namespace TestProject; + public interface IGeneric : IGrainWithStringKey where T : struct { } + public class Container where T : class + { + public interface INested : IGrainWithStringKey where U : struct { } + } + public static class Registration + { + public static void Configure(TypeManifestOptions options) + { + options.GetOrCreate().Add( + typeof(IGeneric), + typeof(OrleansCodeGen.TestProject.Proxy_IGeneric), + OrleansCodeGen.TestProject.Proxy_IGeneric.Create); + options.GetOrCreate().Add( + typeof(Container.INested), + typeof(OrleansCodeGen.TestProject.Container.Proxy_INested), + OrleansCodeGen.TestProject.Container.Proxy_INested.Create); + } + } + """; + + var (result, output) = Generate(await TestCompilationHelper.CreateCompilation(source)); + AssertCompiles(result, output); + var generated = GetSource(result); + Assert.Contains(".Add(typeof(global::TestProject.IGeneric<>), typeof(OrleansCodeGen.TestProject.Proxy_IGeneric<>))", generated, StringComparison.Ordinal); + Assert.Contains("internal sealed class Proxy_INested", generated, StringComparison.Ordinal); + } + + [Fact] + public async Task NestedInterfacesCaptureContainingGenericParameters() + { + const string source = """ + using Orleans; + namespace TestProject; + public class Container where T : class + { + public interface INested : IGrainWithStringKey + { + System.Threading.Tasks.Task Echo(T value); + } + } + """; + + var (result, output) = Generate(await TestCompilationHelper.CreateCompilation(source)); + + AssertCompiles(result, output); + var generated = GetSource(result); + Assert.Contains( + ".Add(typeof(global::TestProject.Container<>.INested), typeof(OrleansCodeGen.TestProject.Container.Proxy_INested<>))", + generated, StringComparison.Ordinal); + Assert.DoesNotContain("new OrleansCodeGen.TestProject.Container.Proxy_INested<>", generated, StringComparison.Ordinal); + } + + [Fact] + public async Task ClosedGenericFactoriesSupportReferencedInterfaces() + { + var contracts = await TestCompilationHelper.CreateCompilation(""" + using Orleans; + namespace Contracts; + public interface IGeneric : IGrainWithStringKey + { + System.Threading.Tasks.Task Echo(T value); + } + """, "Contracts"); + var (contractResult, contractOutput) = Generate(contracts); + AssertCompiles(contractResult, contractOutput); + using var stream = new MemoryStream(); + var emit = contractOutput.Emit(stream, cancellationToken: TestContext.Current.CancellationToken); + Assert.True(emit.Success, string.Join(Environment.NewLine, emit.Diagnostics)); + var consumer = await TestCompilationHelper.CreateCompilation(""" + using Orleans; + using Orleans.Serialization.Configuration; + using Factories = Orleans.Serialization.Configuration.InterfaceProxyFactoryOptions< + Orleans.Runtime.GrainReferenceFactory>; + [assembly: GenerateCodeForDeclaringAssembly(typeof(Contracts.IGeneric<>))] + public static class Registration + { + public static void Configure(TypeManifestOptions options) => options.GetOrCreate().Add( + typeof(Contracts.IGeneric), + typeof(OrleansCodeGen.Contracts.Proxy_IGeneric), + OrleansCodeGen.Contracts.Proxy_IGeneric.Create); + } + """, "Consumer", MetadataReference.CreateFromImage(stream.ToArray())); + + var (result, output) = Generate(consumer); + + AssertCompiles(result, output); + var generated = GetSource(result); + Assert.Contains("internal sealed class Proxy_IGeneric", generated, StringComparison.Ordinal); + Assert.Contains( + ".Add(typeof(global::Contracts.IGeneric<>), typeof(OrleansCodeGen.Contracts.Proxy_IGeneric<>))", + generated, StringComparison.Ordinal); + } + + [Fact] + public async Task NonGrainProxyBasesKeepTypeOnlyRegistration() + { + const string source = """ + using Orleans; + using Orleans.Serialization.Invocation; + using System.Threading.Tasks; + namespace TestProject; + public class PlainProxy + { + public PlainProxy() { } + public ValueTask InvokeAsync(IInvokable request) => default; + public ValueTask InvokeAsync(IInvokable request) => default; + } + [GenerateMethodSerializers(typeof(PlainProxy))] + public interface IPlain { } + [GenerateMethodSerializers(typeof(PlainProxy))] + public interface IPlain { } + """; + + var (result, output) = Generate(await TestCompilationHelper.CreateCompilation(source)); + + AssertCompiles(result, output); + var generated = GetSource(result); + Assert.Contains("AddInterfaceProxy(typeof(OrleansCodeGen.TestProject.Proxy_IPlain))", generated, StringComparison.Ordinal); + Assert.DoesNotContain("InterfaceProxyFactoryOptions", generated, StringComparison.Ordinal); + Assert.DoesNotContain("GrainReference Create", generated, StringComparison.Ordinal); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task GrainReferenceProxyFactoryUsesProtectedConstructor(bool generic, bool baseFactory) + { + var compilation = await TestCompilationHelper.CreateCompilation(CreateCustomProxySource( + "protected CustomReference(GrainReferenceShared shared, IdSpan key) : base(shared, key) { }", generic, baseFactory)); + + var (result, output) = Generate(compilation); + + AssertCompiles(result, output); + Assert.DoesNotContain(output.GetDiagnostics(TestContext.Current.CancellationToken), static diagnostic => diagnostic.Id == "CS0108"); + var generated = GetSource(result); + Assert.Contains( + generic ? "=> new Proxy_ICustom(arg0, arg1);" : "=> new Proxy_ICustom(arg0, arg1);", + generated, StringComparison.Ordinal); + Assert.Contains( + generic + ? ".Add(typeof(global::TestProject.ICustom<>), typeof(OrleansCodeGen.TestProject.Proxy_ICustom<>))" + : ".Add(typeof(global::TestProject.ICustom), typeof(OrleansCodeGen.TestProject.Proxy_ICustom), OrleansCodeGen.TestProject.Proxy_ICustom.Create)", + generated, StringComparison.Ordinal); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task GrainReferenceProxyWithoutRequiredConstructorFailsCompilation(bool generic, bool privateConstructor) + { + var constructor = "protected CustomReference(GrainReferenceShared shared) : base(shared, default) { }"; + if (privateConstructor) + { + constructor += "\nprivate CustomReference(GrainReferenceShared shared, IdSpan key) : base(shared, key) { }"; + } + + var compilation = await TestCompilationHelper.CreateCompilation(CreateCustomProxySource(constructor, generic)); + Assert.Empty(compilation.GetDiagnostics(TestContext.Current.CancellationToken).Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)); + + var (result, output) = Generate(compilation); + + Assert.Empty(result.Diagnostics); + var diagnostic = Assert.Single(output.GetDiagnostics(TestContext.Current.CancellationToken), + static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error); + Assert.Equal("CS1729", diagnostic.Id); + Assert.Contains("Proxy_ICustom", diagnostic.GetMessage(), StringComparison.Ordinal); + Assert.Contains("proxy", diagnostic.Location.SourceTree!.FilePath, StringComparison.Ordinal); + var construction = diagnostic.Location.SourceTree.GetRoot(TestContext.Current.CancellationToken) + .FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); + var errorText = Assert.IsType(construction).ToString(); + Assert.Contains("Proxy_ICustom", errorText, StringComparison.Ordinal); + Assert.Contains("arg0, arg1", errorText, StringComparison.Ordinal); + } + + [Fact] + public async Task NonGrainProxyFactoryUsesBaseDeclaredDelegate() + { + const string source = """ + using Orleans; + using Orleans.Serialization.Configuration; + using Orleans.Serialization.Invocation; + using System.Threading.Tasks; + namespace TestProject; + [GenerateProxyFactory(typeof(System.Func))] + public class PlainProxy + { + public PlainProxy(string label, int number, bool enabled) + { + Label = label; Number = number; Enabled = enabled; + } + public string Label { get; } + public int Number { get; } + public bool Enabled { get; } + public ValueTask InvokeAsync(IInvokable request) => default; + public ValueTask InvokeAsync(IInvokable request) => default; + } + [GenerateMethodSerializers(typeof(PlainProxy))] + public interface IPlain { } + [GenerateMethodSerializers(typeof(PlainProxy))] + public interface IPlain { } + public interface IGrainLike : IGrainWithStringKey { } + public static class Verification + { + public static bool Verify() + { + var options = new TypeManifestOptions(); + ((ITypeManifestProvider)new OrleansCodeGen.TestProject.Metadata_TestProject()).Configure(options); + var factories = options.GetOrCreate>>(); + factories.Add(typeof(IPlain), typeof(OrleansCodeGen.TestProject.Proxy_IPlain), + OrleansCodeGen.TestProject.Proxy_IPlain.Create); + var registration = factories.Factories[typeof(IPlain)]; + var proxy = registration.Factory("plain", 42, true); + var generic = factories.Factories[typeof(IPlain)].Factory("generic", 17, false); + return proxy is IPlain && proxy.Label == "plain" && proxy.Number == 42 && proxy.Enabled + && registration.ProxyType == proxy.GetType() + && registration.Factory.Method.DeclaringType == proxy.GetType() + && generic is IPlain && generic.Label == "generic" && generic.Number == 17 && !generic.Enabled + && factories.Factories[typeof(IPlain<>)].ProxyType == typeof(OrleansCodeGen.TestProject.Proxy_IPlain<>) + && factories.Factories[typeof(IPlain<>)].Factory is null + && options.GetOrCreate>() + .Factories[typeof(IGrainLike)].Factory is not null; + } + } + """; + + var (result, output) = Generate(await CreateConfiguredProxyCompilation(source)); + + AssertCompiles(result, output); + Assert.Contains("public static global::TestProject.PlainProxy Create(string arg0, int arg1, bool arg2) => new Proxy_IPlain(arg0, arg1, arg2);", + GetSource(result), StringComparison.Ordinal); + AssertVerificationSucceeds(output); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ProxyFactoryDeclarationIsInheritedAndNearestBaseCanOverrideIt(bool overrideFactory) + { + var declaration = overrideFactory ? "[GenerateProxyFactory(typeof(System.Func))]" : ""; + var constructor = overrideFactory ? "public ChildProxy(string label) { Label = label; }" : "public ChildProxy() { }"; + var invocation = overrideFactory + ? "var proxy = options.GetOrCreate>>().Factories[typeof(IChild)].Factory(\"child\");" + : "var proxy = options.GetOrCreate>>().Factories[typeof(IChild)].Factory();"; + var source = $$""" + using Orleans; + using Orleans.Serialization.Configuration; + using Orleans.Serialization.Invocation; + using System.Threading.Tasks; + namespace TestProject; + [GenerateProxyFactory(typeof(System.Func))] + public class ParentProxy + { + public string Label { get; protected set; } = "parent"; + public ValueTask InvokeAsync(IInvokable request) => default; + public ValueTask InvokeAsync(IInvokable request) => default; + } + {{declaration}} + public class ChildProxy : ParentProxy + { + {{constructor}} + public new ValueTask InvokeAsync(IInvokable request) => default; + public new ValueTask InvokeAsync(IInvokable request) => default; + } + [GenerateMethodSerializers(typeof(ChildProxy))] + public interface IChild { } + public static class Verification + { + public static bool Verify() + { + var options = new TypeManifestOptions(); + ((ITypeManifestProvider)new OrleansCodeGen.TestProject.Metadata_TestProject()).Configure(options); + {{invocation}} + return proxy is IChild && proxy is ChildProxy && proxy.Label == "{{(overrideFactory ? "child" : "parent")}}"; + } + } + """; + + var (result, output) = Generate(await CreateConfiguredProxyCompilation(source)); + + AssertCompiles(result, output); + AssertVerificationSucceeds(output); + } + + [Theory] + [InlineData("ref")] + [InlineData("in")] + [InlineData("out")] + public async Task ProxyFactoryCustomDelegatePreservesReferenceParameters(string mode) + { + var initialization = mode switch + { + "out" => "Number = value = 42;", + "in" => "Number = value + 1;", + _ => "Number = ++value;", + }; + var source = $$""" + using Orleans; + using Orleans.Serialization.Configuration; + using Orleans.Serialization.Invocation; + using System.Threading.Tasks; + namespace TestProject; + public delegate PlainProxy ProxyFactory({{mode}} int value); + [GenerateProxyFactory(typeof(ProxyFactory))] + public class PlainProxy + { + public PlainProxy({{mode}} int value) { {{initialization}} } + public int Number { get; } + public ValueTask InvokeAsync(IInvokable request) => default; + public ValueTask InvokeAsync(IInvokable request) => default; + } + [GenerateMethodSerializers(typeof(PlainProxy))] + public interface IPlain { } + public static class Verification + { + public static bool Verify() + { + var options = new TypeManifestOptions(); + ((ITypeManifestProvider)new OrleansCodeGen.TestProject.Metadata_TestProject()).Configure(options); + var factory = options.GetOrCreate>().Factories[typeof(IPlain)].Factory; + var value = 41; + var proxy = factory({{mode}} value); + return proxy is IPlain && proxy.Number == 42 && value == {{(mode == "in" ? 41 : 42)}}; + } + } + """; + + var (result, output) = Generate(await CreateConfiguredProxyCompilation(source)); + + AssertCompiles(result, output); + Assert.Contains($"Create({mode} int arg0) => new Proxy_IPlain({mode} arg0);", GetSource(result), StringComparison.Ordinal); + AssertVerificationSucceeds(output); + } + + [Theory] + [InlineData("typeof(int)")] + [InlineData("typeof(System.Action)")] + [InlineData("typeof(System.Func<,>)")] + [InlineData("typeof(RefFactory)")] + [InlineData("typeof(System.Func)")] + [InlineData("null")] + public async Task InvalidProxyFactoryDeclarationReportsProxyBaseDiagnostic(string declaration) + { + var source = $$""" + using Orleans; + using Orleans.Serialization.Invocation; + using System.Threading.Tasks; + namespace TestProject; + public delegate ref PlainProxy RefFactory(); + [GenerateProxyFactory({{declaration}})] + public class PlainProxy + { + public PlainProxy() { } + public ValueTask InvokeAsync(IInvokable request) => default; + public ValueTask InvokeAsync(IInvokable request) => default; + } + [GenerateMethodSerializers(typeof(PlainProxy))] + public interface IPlain { } + """; + + var (result, _) = Generate(await TestCompilationHelper.CreateCompilation(source)); + + var diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal(DiagnosticRuleId.IncorrectProxyBaseClassSpecification, diagnostic.Id); + Assert.Contains("GenerateProxyFactory", diagnostic.GetMessage(), StringComparison.Ordinal); + Assert.True(diagnostic.Location.IsInSource); + } + + [Fact] + public async Task ProxyFactoryRejectsUserDefinedResultConversion() + { + const string source = """ + using Orleans; + using Orleans.Serialization.Invocation; + using System.Threading.Tasks; + namespace TestProject; + public sealed class ConvertedProxy { } + [GenerateProxyFactory(typeof(System.Func))] + public class PlainProxy + { + public static implicit operator ConvertedProxy(PlainProxy proxy) => new ConvertedProxy(); + public ValueTask InvokeAsync(IInvokable request) => default; + public ValueTask InvokeAsync(IInvokable request) => default; + } + [GenerateMethodSerializers(typeof(PlainProxy))] + public interface IPlain { } + """; + + var compilation = await TestCompilationHelper.CreateCompilation(source); + Assert.Empty(compilation.GetDiagnostics(TestContext.Current.CancellationToken) + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)); + + var (result, _) = Generate(compilation); + + var diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal(DiagnosticRuleId.IncorrectProxyBaseClassSpecification, diagnostic.Id); + Assert.Contains("identity or implicit reference conversion", diagnostic.GetMessage(), StringComparison.Ordinal); + Assert.True(diagnostic.Location.IsInSource); + } + + private static void AssertVerificationSucceeds(Compilation output) + { + using var stream = new MemoryStream(); + var emit = output.Emit(stream, cancellationToken: TestContext.Current.CancellationToken); + Assert.True(emit.Success, string.Join(Environment.NewLine, emit.Diagnostics)); + var assembly = System.Reflection.Assembly.Load(stream.ToArray()); + var method = assembly.GetType("TestProject.Verification")!.GetMethod("Verify")!; + Assert.True(Assert.IsType(method.Invoke(null, null))); + } + + private static Task CreateConfiguredProxyCompilation(string source) => + TestCompilationHelper.CreateCompilation(source, additionalReferences: + [ + MetadataReference.CreateFromFile(typeof(Microsoft.Extensions.Options.IConfigureOptions<>).Assembly.Location), + ]); + + private static string CreateCustomProxySource(string constructor, bool generic, bool baseFactory = false) => $$""" + using Orleans; + using Orleans.Runtime; + using Orleans.Serialization.Invocation; + using System.Threading.Tasks; + namespace TestProject; + public class CustomReference : GrainReference + { + {{constructor}} + public new ValueTask InvokeAsync(IRequest request) => base.InvokeAsync(request); + public new ValueTask InvokeAsync(IRequest request) => base.InvokeAsync(request); + {{(baseFactory ? "public static GrainReference Create(GrainReferenceShared shared, IdSpan key) => throw new System.NotSupportedException(\"base-factory\");" : "")}} + } + [GenerateMethodSerializers(typeof(CustomReference))] + public interface ICustom{{(generic ? "" : "")}} : IGrainWithStringKey { } + """; + + private static (GeneratorRunResult Result, Compilation Output) Generate(CSharpCompilation compilation) + { + GeneratorDriver driver = CSharpGeneratorDriver.Create( + [new OrleansSerializationSourceGenerator().AsSourceGenerator()], + parseOptions: (CSharpParseOptions)compilation.SyntaxTrees.First().Options, + optionsProvider: TestCompilationHelper.CreateOptionsProvider()); + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var output, out _, TestContext.Current.CancellationToken); + return (Assert.Single(driver.GetRunResult().Results), output); + } + + private static string GetSource(GeneratorRunResult result) => + string.Join(Environment.NewLine, result.GeneratedSources.Select(static source => source.SourceText.ToString())); + + private static void AssertCompiles(GeneratorRunResult result, Compilation output) + { + Assert.Empty(result.Diagnostics); + Assert.Empty(output.GetDiagnostics(TestContext.Current.CancellationToken).Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)); + using var stream = new MemoryStream(); + var emit = output.Emit(stream, cancellationToken: TestContext.Current.CancellationToken); + Assert.True(emit.Success, string.Join(Environment.NewLine, emit.Diagnostics)); + } +} diff --git a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestBasicGrain.verified.cs b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestBasicGrain.verified.cs index c15c91d1335..96de3bae8d9 100644 --- a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestBasicGrain.verified.cs +++ b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestBasicGrain.verified.cs @@ -65,6 +65,7 @@ public Proxy_IBasicGrain(global::Orleans.Runtime.GrainReferenceShared arg0, glob { } + public static global::Orleans.Runtime.GrainReference Create(global::Orleans.Runtime.GrainReferenceShared arg0, global::Orleans.Runtime.IdSpan arg1) => new Proxy_IBasicGrain(arg0, arg1); global::System.Threading.Tasks.Task global::TestProject.IBasicGrain.SayHello(string arg0) { var request = new OrleansCodeGen.TestProject.Invokable_IBasicGrain_GrainReference_6B0E24A1(); @@ -254,6 +255,8 @@ protected override void ConfigureInner(global::Orleans.Serialization.Configurati config.AddCopier(typeof(OrleansCodeGen.TestProject.Copier_Invokable_IBasicGrain_GrainReference_6B0E24A1)); config.AddCopier(typeof(OrleansCodeGen.TestProject.Copier_BasicGrain)); config.AddInterfaceProxy(typeof(OrleansCodeGen.TestProject.Proxy_IBasicGrain)); + var proxyFactories = config.GetOrCreate>(); + proxyFactories.Add(typeof(global::TestProject.IBasicGrain), typeof(OrleansCodeGen.TestProject.Proxy_IBasicGrain), OrleansCodeGen.TestProject.Proxy_IBasicGrain.Create); config.AddInterface(typeof(global::TestProject.IBasicGrain)); config.AddInterfaceImplementation(typeof(global::TestProject.BasicGrain)); config.AddActivator(typeof(OrleansCodeGen.TestProject.Activator_BasicGrain)); diff --git a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestClassWithGenerateMethodSerializersAnnotation.verified.cs b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestClassWithGenerateMethodSerializersAnnotation.verified.cs index c6be9c7cf7d..62de50e5d89 100644 --- a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestClassWithGenerateMethodSerializersAnnotation.verified.cs +++ b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestClassWithGenerateMethodSerializersAnnotation.verified.cs @@ -65,6 +65,7 @@ public Proxy_IMyGrain(global::Orleans.Runtime.GrainReferenceShared arg0, global: { } + public static global::Orleans.Runtime.GrainReference Create(global::Orleans.Runtime.GrainReferenceShared arg0, global::Orleans.Runtime.IdSpan arg1) => new Proxy_IMyGrain(arg0, arg1); global::System.Threading.Tasks.Task global::IMyGrain.SayHello(string arg0) { var request = new OrleansCodeGen.Invokable_IMyGrain_GrainReference_6D39E404(); @@ -163,6 +164,8 @@ protected override void ConfigureInner(global::Orleans.Serialization.Configurati config.AddSerializer(typeof(OrleansCodeGen.Codec_Invokable_IMyGrain_GrainReference_6D39E404)); config.AddCopier(typeof(OrleansCodeGen.Copier_Invokable_IMyGrain_GrainReference_6D39E404)); config.AddInterfaceProxy(typeof(OrleansCodeGen.Proxy_IMyGrain)); + var proxyFactories = config.GetOrCreate>(); + proxyFactories.Add(typeof(global::IMyGrain), typeof(OrleansCodeGen.Proxy_IMyGrain), OrleansCodeGen.Proxy_IMyGrain.Create); config.AddInterface(typeof(global::IMyGrain)); var n1 = config.CompoundTypeAliases.Add("inv"); var n2 = n1.Add(typeof(global::Orleans.Runtime.GrainReference)); diff --git a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainComplexGrain.verified.cs b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainComplexGrain.verified.cs index 171c11aef6e..21fa9605b9f 100644 --- a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainComplexGrain.verified.cs +++ b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainComplexGrain.verified.cs @@ -110,6 +110,7 @@ public Proxy_IComplexGrain(global::Orleans.Runtime.GrainReferenceShared arg0, gl _copier_ComplexData_765A40ED2309AF35 = OrleansGeneratedCodeHelper.GetService(this, CodecProvider); } + public static global::Orleans.Runtime.GrainReference Create(global::Orleans.Runtime.GrainReferenceShared arg0, global::Orleans.Runtime.IdSpan arg1) => new Proxy_IComplexGrain(arg0, arg1); global::System.Threading.Tasks.Task global::TestProject.IComplexGrain.ProcessData(int arg0, string arg1, global::TestProject.ComplexData arg2, global::System.Threading.CancellationToken arg3) { var request = new OrleansCodeGen.TestProject.Invokable_IComplexGrain_GrainReference_67FE5808(); @@ -449,6 +450,8 @@ protected override void ConfigureInner(global::Orleans.Serialization.Configurati config.AddCopier(typeof(OrleansCodeGen.TestProject.Copier_Invokable_IComplexGrain_GrainReference_67FE5808)); config.AddCopier(typeof(OrleansCodeGen.TestProject.Copier_ComplexGrain)); config.AddInterfaceProxy(typeof(OrleansCodeGen.TestProject.Proxy_IComplexGrain)); + var proxyFactories = config.GetOrCreate>(); + proxyFactories.Add(typeof(global::TestProject.IComplexGrain), typeof(OrleansCodeGen.TestProject.Proxy_IComplexGrain), OrleansCodeGen.TestProject.Proxy_IComplexGrain.Create); config.AddInterface(typeof(global::TestProject.IComplexGrain)); config.AddInterfaceImplementation(typeof(global::TestProject.ComplexGrain)); config.AddActivator(typeof(OrleansCodeGen.TestProject.Activator_ComplexData)); diff --git a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainMethodAnnotatedWithInvokableBaseType.verified.cs b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainMethodAnnotatedWithInvokableBaseType.verified.cs index a532a1a5df9..f868886dfed 100644 --- a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainMethodAnnotatedWithInvokableBaseType.verified.cs +++ b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainMethodAnnotatedWithInvokableBaseType.verified.cs @@ -70,6 +70,7 @@ public Proxy_IHelloGrain(global::Orleans.Runtime.GrainReferenceShared arg0, glob { } + public static global::Orleans.Runtime.GrainReference Create(global::Orleans.Runtime.GrainReferenceShared arg0, global::Orleans.Runtime.IdSpan arg1) => new Proxy_IHelloGrain(arg0, arg1); global::System.Threading.Tasks.Task global::TestProject.IHelloGrain.SayHello(string arg0) { var request = new OrleansCodeGen.TestProject.Invokable_IHelloGrain_GrainReference_5336307F(); @@ -259,6 +260,8 @@ protected override void ConfigureInner(global::Orleans.Serialization.Configurati config.AddCopier(typeof(OrleansCodeGen.TestProject.Copier_Invokable_IHelloGrain_GrainReference_5336307F)); config.AddCopier(typeof(OrleansCodeGen.TestProject.Copier_HelloGrain)); config.AddInterfaceProxy(typeof(OrleansCodeGen.TestProject.Proxy_IHelloGrain)); + var proxyFactories = config.GetOrCreate>(); + proxyFactories.Add(typeof(global::TestProject.IHelloGrain), typeof(OrleansCodeGen.TestProject.Proxy_IHelloGrain), OrleansCodeGen.TestProject.Proxy_IHelloGrain.Create); config.AddInterface(typeof(global::TestProject.IHelloGrain)); config.AddInterfaceImplementation(typeof(global::TestProject.HelloGrain)); config.AddActivator(typeof(OrleansCodeGen.TestProject.Activator_HelloGrain)); diff --git a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainMethodAnnotatedWithResponseTimeout.verified.cs b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainMethodAnnotatedWithResponseTimeout.verified.cs index 72fd2c84373..700d9dea605 100644 --- a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainMethodAnnotatedWithResponseTimeout.verified.cs +++ b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainMethodAnnotatedWithResponseTimeout.verified.cs @@ -67,6 +67,7 @@ public Proxy_IResponseTimeoutGrain(global::Orleans.Runtime.GrainReferenceShared { } + public static global::Orleans.Runtime.GrainReference Create(global::Orleans.Runtime.GrainReferenceShared arg0, global::Orleans.Runtime.IdSpan arg1) => new Proxy_IResponseTimeoutGrain(arg0, arg1); global::System.Threading.Tasks.Task global::TestProject.IResponseTimeoutGrain.LongRunningMethod(string arg0) { var request = new OrleansCodeGen.TestProject.Invokable_IResponseTimeoutGrain_GrainReference_6BE752C8(); @@ -159,6 +160,8 @@ protected override void ConfigureInner(global::Orleans.Serialization.Configurati config.AddSerializer(typeof(OrleansCodeGen.TestProject.Codec_Invokable_IResponseTimeoutGrain_GrainReference_6BE752C8)); config.AddCopier(typeof(OrleansCodeGen.TestProject.Copier_Invokable_IResponseTimeoutGrain_GrainReference_6BE752C8)); config.AddInterfaceProxy(typeof(OrleansCodeGen.TestProject.Proxy_IResponseTimeoutGrain)); + var proxyFactories = config.GetOrCreate>(); + proxyFactories.Add(typeof(global::TestProject.IResponseTimeoutGrain), typeof(OrleansCodeGen.TestProject.Proxy_IResponseTimeoutGrain), OrleansCodeGen.TestProject.Proxy_IResponseTimeoutGrain.Create); config.AddInterface(typeof(global::TestProject.IResponseTimeoutGrain)); config.AddInterfaceImplementation(typeof(global::TestProject.ResponseTimeoutGrain)); var n1 = config.CompoundTypeAliases.Add("inv"); diff --git a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainWithDifferentKeyTypes.verified.cs b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainWithDifferentKeyTypes.verified.cs index d0428fc3d68..2e9dc14d692 100644 --- a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainWithDifferentKeyTypes.verified.cs +++ b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainWithDifferentKeyTypes.verified.cs @@ -38,6 +38,7 @@ public Proxy_IMyGrainWithGuidKey(global::Orleans.Runtime.GrainReferenceShared ar { } + public static global::Orleans.Runtime.GrainReference Create(global::Orleans.Runtime.GrainReferenceShared arg0, global::Orleans.Runtime.IdSpan arg1) => new Proxy_IMyGrainWithGuidKey(arg0, arg1); global::System.Threading.Tasks.Task global::TestProject.IMyGrainWithGuidKey.GetGuidValue() { var request = new OrleansCodeGen.TestProject.Invokable_IMyGrainWithGuidKey_GrainReference_8F0FEC0E(); @@ -73,6 +74,7 @@ public Proxy_IMyGrainWithStringKey(global::Orleans.Runtime.GrainReferenceShared { } + public static global::Orleans.Runtime.GrainReference Create(global::Orleans.Runtime.GrainReferenceShared arg0, global::Orleans.Runtime.IdSpan arg1) => new Proxy_IMyGrainWithStringKey(arg0, arg1); global::System.Threading.Tasks.Task global::TestProject.IMyGrainWithStringKey.GetStringKey() { var request = new OrleansCodeGen.TestProject.Invokable_IMyGrainWithStringKey_GrainReference_43570316(); @@ -108,6 +110,7 @@ public Proxy_IMyGrainWithGuidCompoundKey(global::Orleans.Runtime.GrainReferenceS { } + public static global::Orleans.Runtime.GrainReference Create(global::Orleans.Runtime.GrainReferenceShared arg0, global::Orleans.Runtime.IdSpan arg1) => new Proxy_IMyGrainWithGuidCompoundKey(arg0, arg1); global::System.Threading.Tasks.Task> global::TestProject.IMyGrainWithGuidCompoundKey.GetGuidAndStringKey() { var request = new OrleansCodeGen.TestProject.Invokable_IMyGrainWithGuidCompoundKey_GrainReference_A9FEF7AF(); @@ -143,6 +146,7 @@ public Proxy_IMyGrainWithIntegerCompoundKey(global::Orleans.Runtime.GrainReferen { } + public static global::Orleans.Runtime.GrainReference Create(global::Orleans.Runtime.GrainReferenceShared arg0, global::Orleans.Runtime.IdSpan arg1) => new Proxy_IMyGrainWithIntegerCompoundKey(arg0, arg1); global::System.Threading.Tasks.Task> global::TestProject.IMyGrainWithIntegerCompoundKey.GetIntegerAndStringKey() { var request = new OrleansCodeGen.TestProject.Invokable_IMyGrainWithIntegerCompoundKey_GrainReference_9814021A(); @@ -787,6 +791,11 @@ protected override void ConfigureInner(global::Orleans.Serialization.Configurati config.AddInterfaceProxy(typeof(OrleansCodeGen.TestProject.Proxy_IMyGrainWithStringKey)); config.AddInterfaceProxy(typeof(OrleansCodeGen.TestProject.Proxy_IMyGrainWithGuidCompoundKey)); config.AddInterfaceProxy(typeof(OrleansCodeGen.TestProject.Proxy_IMyGrainWithIntegerCompoundKey)); + var proxyFactories = config.GetOrCreate>(); + proxyFactories.Add(typeof(global::TestProject.IMyGrainWithGuidKey), typeof(OrleansCodeGen.TestProject.Proxy_IMyGrainWithGuidKey), OrleansCodeGen.TestProject.Proxy_IMyGrainWithGuidKey.Create); + proxyFactories.Add(typeof(global::TestProject.IMyGrainWithStringKey), typeof(OrleansCodeGen.TestProject.Proxy_IMyGrainWithStringKey), OrleansCodeGen.TestProject.Proxy_IMyGrainWithStringKey.Create); + proxyFactories.Add(typeof(global::TestProject.IMyGrainWithGuidCompoundKey), typeof(OrleansCodeGen.TestProject.Proxy_IMyGrainWithGuidCompoundKey), OrleansCodeGen.TestProject.Proxy_IMyGrainWithGuidCompoundKey.Create); + proxyFactories.Add(typeof(global::TestProject.IMyGrainWithIntegerCompoundKey), typeof(OrleansCodeGen.TestProject.Proxy_IMyGrainWithIntegerCompoundKey), OrleansCodeGen.TestProject.Proxy_IMyGrainWithIntegerCompoundKey.Create); config.AddInterface(typeof(global::TestProject.IMyGrainWithGuidKey)); config.AddInterface(typeof(global::TestProject.IMyGrainWithStringKey)); config.AddInterface(typeof(global::TestProject.IMyGrainWithGuidCompoundKey)); diff --git a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainWithMultipleInterfaces.verified.cs b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainWithMultipleInterfaces.verified.cs index 032a288b6e3..d3834ab2ad3 100644 --- a/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainWithMultipleInterfaces.verified.cs +++ b/test/Orleans.CodeGenerator.Tests/snapshots/OrleansSourceGeneratorTests.TestGrainWithMultipleInterfaces.verified.cs @@ -65,6 +65,7 @@ public Proxy_IGrainA(global::Orleans.Runtime.GrainReferenceShared arg0, global:: { } + public static global::Orleans.Runtime.GrainReference Create(global::Orleans.Runtime.GrainReferenceShared arg0, global::Orleans.Runtime.IdSpan arg1) => new Proxy_IGrainA(arg0, arg1); global::System.Threading.Tasks.Task global::TestProject.IGrainA.MethodA(string arg0) { var request = new OrleansCodeGen.TestProject.Invokable_IGrainA_GrainReference_11405B98(); @@ -128,6 +129,7 @@ public Proxy_IGrainB(global::Orleans.Runtime.GrainReferenceShared arg0, global:: { } + public static global::Orleans.Runtime.GrainReference Create(global::Orleans.Runtime.GrainReferenceShared arg0, global::Orleans.Runtime.IdSpan arg1) => new Proxy_IGrainB(arg0, arg1); global::System.Threading.Tasks.Task global::TestProject.IGrainB.MethodB(string arg0) { var request = new OrleansCodeGen.TestProject.Invokable_IGrainB_GrainReference_6B5D7809(); @@ -299,6 +301,9 @@ protected override void ConfigureInner(global::Orleans.Serialization.Configurati config.AddCopier(typeof(OrleansCodeGen.TestProject.Copier_Invokable_IGrainB_GrainReference_6B5D7809)); config.AddInterfaceProxy(typeof(OrleansCodeGen.TestProject.Proxy_IGrainA)); config.AddInterfaceProxy(typeof(OrleansCodeGen.TestProject.Proxy_IGrainB)); + var proxyFactories = config.GetOrCreate>(); + proxyFactories.Add(typeof(global::TestProject.IGrainA), typeof(OrleansCodeGen.TestProject.Proxy_IGrainA), OrleansCodeGen.TestProject.Proxy_IGrainA.Create); + proxyFactories.Add(typeof(global::TestProject.IGrainB), typeof(OrleansCodeGen.TestProject.Proxy_IGrainB), OrleansCodeGen.TestProject.Proxy_IGrainB.Create); config.AddInterface(typeof(global::TestProject.IGrainA)); config.AddInterface(typeof(global::TestProject.IGrainB)); config.AddInterfaceImplementation(typeof(global::TestProject.RealGrain)); diff --git a/test/Orleans.Core.Tests/GrainReferences/GrainReferenceActivatorTests.cs b/test/Orleans.Core.Tests/GrainReferences/GrainReferenceActivatorTests.cs index db8bc7302f2..2ea888aa2bf 100644 --- a/test/Orleans.Core.Tests/GrainReferences/GrainReferenceActivatorTests.cs +++ b/test/Orleans.Core.Tests/GrainReferences/GrainReferenceActivatorTests.cs @@ -5,9 +5,11 @@ using Orleans.Runtime; using Orleans.Serialization; using Orleans.Serialization.Cloning; +using Orleans.Serialization.Configuration; using Orleans.Serialization.Serializers; using TestExtensions; using Xunit; +using ReferenceFactories = Orleans.Serialization.Configuration.InterfaceProxyFactoryOptions; namespace UnitTests.GrainReferences; @@ -91,4 +93,180 @@ public void CreateReference_NonPublicProxyConstructorPreservesGrainId() GrainId.Create(ReferenceConstructionFixture.GrainType, IdSpan.Create("private-constructor")), reference.GrainId); } + + [Fact] + public void CreateReference_StaticallyClosedGenericFactoriesPreserveIdentityAndSharedState() + { + using var fixture = new ReferenceConstructionFixture(); + var first = fixture.CreateReference("int-key"); + var second = fixture.CreateReference("second-key"); + var text = fixture.CreateReference("text-key"); + + Assert.IsAssignableFrom>(first); + Assert.IsAssignableFrom>(text); + Assert.NotEqual(first.GetType(), text.GetType()); + Assert.Equal(fixture.Resolver.GetGrainInterfaceType(typeof(IGenericConstructionGrain)), first.InterfaceType); + Assert.Equal(fixture.Resolver.GetGrainInterfaceType(typeof(IGenericConstructionGrain)), text.InterfaceType); + Assert.Equal(IdSpan.Create("int-key"), first.GrainId.Key); + Assert.Equal(IdSpan.Create("text-key"), text.GrainId.Key); + Assert.Same(first.Shared, second.Shared); + Assert.NotSame(first.Shared, text.Shared); + var factories = fixture.ManifestOptions.GetOrCreate().Factories; + Assert.NotNull(factories[typeof(IGenericConstructionGrain)].Factory); + Assert.NotNull(factories[typeof(IGenericConstructionGrain)].Factory); + } + + [Fact] + public void CreateReference_RuntimeSelectedGenericPreservesJitCompatibility() + { + using var fixture = new ReferenceConstructionFixture(); + + var reference = fixture.CreateReference("generic-key"); + + Assert.IsAssignableFrom>(reference); + Assert.Equal(IdSpan.Create("generic-key"), reference.GrainId.Key); + Assert.Equal(fixture.Resolver.GetGrainInterfaceType(typeof(IGenericConstructionGrain)), reference.InterfaceType); + } + + [Fact] + public void CreateReference_LegacyGeneratedManifestPreservesCompatibility() + { + using var fixture = new ReferenceConstructionFixture(legacy: true); + + var reference = fixture.CreateReference("legacy-key"); + + Assert.IsAssignableFrom(reference); + Assert.Equal(IdSpan.Create("legacy-key"), reference.GrainId.Key); + Assert.Equal(17, reference.InterfaceVersion); + } + + [Fact] + public void CreateReference_RegisteredFactoryUsesDeclaredInterfaceAndPropagatesConstructorExceptions() + { + using var fixture = new ReferenceConstructionFixture( + proxyType: typeof(ThrowingConstructorProxy), factory: static (shared, key) => new ThrowingConstructorProxy(shared, key)); + + var exception = Assert.Throws(() => fixture.CreateReference("key")); + + Assert.Equal("proxy-constructor", exception.Message); + } + + [Fact] + public void CreateReference_RegisteredFactorySupportsPrivateConstructor() + { + using var fixture = new ReferenceConstructionFixture( + proxyType: typeof(NonPublicConstructorProxy), factory: NonPublicConstructorProxy.Create); + + var reference = Assert.IsType(fixture.CreateReference("private-key")); + + Assert.Equal(IdSpan.Create("private-key"), reference.GrainId.Key); + } + + [Fact] + public void CreateReference_LegacyCustomProxyOverridesGeneratedFactory() + { + using var fixture = new ReferenceConstructionFixture( + proxyType: typeof(InspectableConstructionProxy), includeGeneratedFactories: true); + var generated = fixture.ManifestOptions.GetOrCreate().Factories[typeof(IConstructionGrain)]; + Assert.NotNull(generated.Factory); + Assert.NotEqual(typeof(InspectableConstructionProxy), generated.ProxyType); + + var reference = Assert.IsType(fixture.CreateReference("override-key")); + + Assert.Equal(IdSpan.Create("override-key"), reference.GrainId.Key); + Assert.Equal(17, reference.InterfaceVersion); + } + + [Fact] + public void CreateReference_RegisteredCustomFactoryOverridesGeneratedFactory() + { + using var fixture = new ReferenceConstructionFixture( + proxyType: typeof(ThrowingConstructorProxy), + factory: static (shared, key) => new ThrowingConstructorProxy(shared, key), + includeGeneratedFactories: true); + + var exception = Assert.Throws(() => fixture.CreateReference("override-key")); + + Assert.Equal("proxy-constructor", exception.Message); + } + + [Fact] + public void CreateReference_RegisteredFactoryUsesExplicitInterfaceForMultipleInterfaceProxy() + { + using var fixture = new ReferenceConstructionFixture( + proxyType: typeof(MultipleInterfaceConstructionProxy), + factory: static (shared, key) => new MultipleInterfaceConstructionProxy(shared, key)); + + var reference = Assert.IsType(fixture.CreateReference("multiple-key")); + + Assert.IsAssignableFrom(reference); + Assert.Equal(ReferenceConstructionFixture.InterfaceType, reference.InterfaceType); + Assert.Equal(17, reference.InterfaceVersion); + Assert.Equal(IdSpan.Create("multiple-key"), reference.GrainId.Key); + } + + [Fact] + public void ManifestExtensionsPreserveTypedConfigurationPerManifest() + { + var first = new TypeManifestOptions(); + var second = new TypeManifestOptions(); + var factories = first.GetOrCreate(); + factories.Add(typeof(IConstructionGrain), typeof(InspectableConstructionProxy), + static (shared, key) => new InspectableConstructionProxy(shared, key)); + + Assert.Same(factories, first.GetOrCreate()); + Assert.NotSame(factories, second.GetOrCreate()); + Assert.Empty(second.GetOrCreate().Factories); + Assert.Single(first.GetOrCreate().Factories); + } + + [Fact] + public void InterfaceProxyFactoriesSupportIndependentDelegateSignatures() + { + var manifest = new TypeManifestOptions(); + var factories = manifest.GetOrCreate>>(); + factories.Add(typeof(IFormattable), typeof(int), static (prefix, value) => value); + factories.Add(typeof(IFormattable), typeof(int), static (prefix, value) => int.Parse(prefix) + value); + var other = manifest.GetOrCreate>>(); + other.Add(typeof(IComparable), typeof(string), static value => value.ToString()); + + var registration = Assert.Single(factories.Factories); + Assert.Equal(typeof(IFormattable), registration.Key); + Assert.Equal(typeof(int), registration.Value.ProxyType); + Assert.Equal(42, Assert.IsType>(registration.Value.Factory)("40", 2)); + Assert.Equal("42", Assert.IsType>(other.Factories[typeof(IComparable)].Factory)(42)); + Assert.Single(factories.ProxyTypes); + Assert.Single(other.Factories); + Assert.Equal(typeof(Serializer).Assembly, typeof(InterfaceProxyFactoryOptions<>).Assembly); + } + + [Theory] + [InlineData(true, "interfaceType")] + [InlineData(true, "proxyType")] + [InlineData(true, "factory")] + [InlineData(false, "interfaceType")] + [InlineData(false, "proxyType")] + public void InterfaceProxyFactoriesRejectNullArgumentsWithoutChangingRegistrations(bool registerFactory, string parameter) + { + var factories = new InterfaceProxyFactoryOptions>(); + var interfaceType = parameter == "interfaceType" ? null! : typeof(IComparable); + var proxyType = parameter == "proxyType" ? null! : typeof(string); + Func factory = parameter == "factory" ? null! : static () => string.Empty; + + var exception = Assert.Throws(() => + { + if (registerFactory) + { + factories.Add(interfaceType, proxyType, factory); + } + else + { + factories.Add(interfaceType, proxyType); + } + }); + + Assert.Equal(parameter, exception.ParamName); + Assert.Empty(factories.Factories); + Assert.Empty(factories.ProxyTypes); + } } diff --git a/test/Orleans.Core.Tests/GrainReferences/ReferenceConstructionFixture.cs b/test/Orleans.Core.Tests/GrainReferences/ReferenceConstructionFixture.cs index 78c7fdd14ea..ec03628e541 100644 --- a/test/Orleans.Core.Tests/GrainReferences/ReferenceConstructionFixture.cs +++ b/test/Orleans.Core.Tests/GrainReferences/ReferenceConstructionFixture.cs @@ -14,6 +14,7 @@ using Orleans.Serialization.Invocation; using Orleans.Serialization.Serializers; using Orleans.Serialization.TypeSystem; +using ReferenceFactories = Orleans.Serialization.Configuration.InterfaceProxyFactoryOptions; namespace UnitTests.GrainReferences; @@ -24,7 +25,10 @@ internal sealed class ReferenceConstructionFixture : IDisposable public ReferenceConstructionFixture( bool unordered = false, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type? proxyType = null) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type? proxyType = null, + GrainReferenceFactory? factory = null, + bool legacy = false, + bool includeGeneratedFactories = false) { Runtime = new RecordingReferenceRuntime(); Services = new ServiceCollection() @@ -35,20 +39,47 @@ public ReferenceConstructionFixture( .AddSingleton(Runtime) .BuildServiceProvider(); var options = Services.GetRequiredService>(); + ManifestOptions = options.Value; + // This fixture owns the generated proxies in the same assembly and supplies concrete AOT factory roots. + var factories = ManifestOptions.GetOrCreate(); + factories.Add( + typeof(IGenericConstructionGrain), + typeof(OrleansCodeGen.UnitTests.GrainReferences.Proxy_IGenericConstructionGrain), + OrleansCodeGen.UnitTests.GrainReferences.Proxy_IGenericConstructionGrain.Create); + factories.Add( + typeof(IGenericConstructionGrain), + typeof(OrleansCodeGen.UnitTests.GrainReferences.Proxy_IGenericConstructionGrain), + OrleansCodeGen.UnitTests.GrainReferences.Proxy_IGenericConstructionGrain.Create); + if (proxyType is not null) { - var manifestOptions = new TypeManifestOptions(); + var manifestOptions = includeGeneratedFactories ? ManifestOptions : new TypeManifestOptions(); manifestOptions.AddInterfaceProxy(proxyType); + if (factory is not null) + { + manifestOptions.GetOrCreate().Add(typeof(IConstructionGrain), proxyType, factory); + } + + options = Options.Create(manifestOptions); + } + else if (legacy) + { + var manifestOptions = new TypeManifestOptions(); + foreach (var type in ManifestOptions.InterfaceProxyTypes) + { + manifestOptions.InterfaceProxies.Add(type); + } + options = Options.Create(manifestOptions); } var typeConverter = Services.GetRequiredService(); - var resolver = new GrainInterfaceTypeResolver([new ConstructionInterfaceTypeProvider()], typeConverter); + Resolver = new GrainInterfaceTypeResolver([new ConstructionInterfaceTypeProvider()], typeConverter); var manifestProvider = new ConstructionManifestProvider(unordered); Provider = new GrainReferenceActivatorProvider( Services, new GrainPropertiesResolver(manifestProvider), - new RpcProvider(options, resolver, typeConverter), + new RpcProvider(options, Resolver, typeConverter), Services.GetRequiredService(), Services.GetRequiredService(), new GrainVersionManifest(manifestProvider)); @@ -59,10 +90,15 @@ public ReferenceConstructionFixture( public RecordingReferenceRuntime Runtime { get; } public GrainReferenceActivatorProvider Provider { get; } public GrainReferenceActivator Activator { get; } + public TypeManifestOptions ManifestOptions { get; } + public GrainInterfaceTypeResolver Resolver { get; } public GrainReference CreateReference(string key) => Activator.CreateReference(GrainId.Create(GrainType, IdSpan.Create(key)), InterfaceType); + public GrainReference CreateReference(string key) => + Activator.CreateReference(GrainId.Create(GrainType, IdSpan.Create(key)), Resolver.GetGrainInterfaceType(typeof(IGenericConstructionGrain))); + public void Dispose() => Services.Dispose(); private sealed class ConstructionInterfaceTypeProvider : IGrainInterfaceTypeProvider @@ -100,6 +136,8 @@ public ConstructionManifestProvider(bool unordered) public interface IConstructionBaseGrain : IGrainWithStringKey; public interface IConstructionGrain : IConstructionBaseGrain; +public interface IGenericConstructionGrain : IConstructionBaseGrain; +public interface IOtherConstructionGrain : IGrainWithStringKey; internal sealed class RecordingReferenceRuntime : IGrainReferenceRuntime { @@ -145,6 +183,11 @@ public ThrowingConstructorProxy(GrainReferenceShared shared, IdSpan key) : base( internal sealed class NonPublicConstructorProxy : GrainReference, IConstructionGrain { private NonPublicConstructorProxy(GrainReferenceShared shared, IdSpan key) : base(shared, key) { } + + internal static GrainReference Create(GrainReferenceShared shared, IdSpan key) => new NonPublicConstructorProxy(shared, key); } internal sealed class ConstructionException(string message) : Exception(message); + +internal sealed class MultipleInterfaceConstructionProxy(GrainReferenceShared shared, IdSpan key) + : GrainReference(shared, key), IOtherConstructionGrain, IConstructionGrain; diff --git a/test/Orleans.NativeAotSmoke/GrainReferences.cs b/test/Orleans.NativeAotSmoke/GrainReferences.cs index e1bb54af8e0..69af50fcfaf 100644 --- a/test/Orleans.NativeAotSmoke/GrainReferences.cs +++ b/test/Orleans.NativeAotSmoke/GrainReferences.cs @@ -1,6 +1,8 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Orleans.CodeGeneration; +using Orleans.Serialization.Configuration; +using Orleans.Serialization.Invocation; using Orleans.Runtime; using Orleans.Serialization; using Orleans.Serialization.Cloning; @@ -14,6 +16,8 @@ internal static class Program private static void Main() { ValidateGeneratedProxy(); + ValidateClosedGenericFactories(); + ValidateNonGrainFactory(); ValidateSharedState(unordered: false); ValidateSharedState(unordered: true); ValidateConstructorFailures(); @@ -21,6 +25,49 @@ private static void Main() Console.WriteLine("Production grain-reference provider constructed generated proxies under NativeAOT."); } + private static void ValidateNonGrainFactory() + { + using var fixture = new ReferenceConstructionFixture(); + var registration = fixture.ManifestOptions + .GetOrCreate>>() + .Factories[typeof(INonGrainProxy)]; + var factory = registration.Factory ?? throw new InvalidOperationException("The non-grain factory was not registered."); + var proxy = factory("non-grain", 42); + + Ensure(proxy is INonGrainProxy, "The non-grain factory did not construct the generated interface proxy."); + Ensure(proxy.Label == "non-grain" && proxy.Number == 42, "The non-grain factory changed its arguments."); + Ensure(registration.ProxyType == proxy.GetType(), "The non-grain factory changed its registered proxy type."); + } + + private static void ValidateClosedGenericFactories() + { + using var fixture = new ReferenceConstructionFixture(); + var number = fixture.CreateReference("number-key"); + var text = fixture.CreateReference("text-key"); + var repeated = fixture.CreateReference("number-key"); + + Ensure(number is IGenericConstructionGrain, "The statically closed value-type proxy was not constructed."); + Ensure(text is IGenericConstructionGrain, "The statically closed reference-type proxy was not constructed."); + Ensure(number.GetType() != text.GetType(), "Closed proxy types must retain their type arguments."); + Ensure(number.InterfaceType == fixture.Resolver.GetGrainInterfaceType(typeof(IGenericConstructionGrain)), "Closed value-type interface identity changed."); + Ensure(text.InterfaceType == fixture.Resolver.GetGrainInterfaceType(typeof(IGenericConstructionGrain)), "Closed reference-type interface identity changed."); + Ensure(number.GrainId.Key == IdSpan.Create("number-key") && text.GrainId.Key == IdSpan.Create("text-key"), "Closed generic grain keys changed."); + Ensure(!ReferenceEquals(number, repeated) && number.Equals(repeated), "Closed generic reference identity changed."); + } + + [GenerateProxyFactory(typeof(Func))] + public class NonGrainProxyBase(string label, int number) + { + public string Label { get; } = label; + public int Number { get; } = number; + + public ValueTask InvokeAsync(IInvokable request) => throw new NotSupportedException(); + public ValueTask InvokeAsync(IInvokable request) => throw new NotSupportedException(); + } + + [GenerateMethodSerializers(typeof(NonGrainProxyBase))] + public interface INonGrainProxy; + private static void ValidateGeneratedProxy() { using var fixture = new ReferenceConstructionFixture(); @@ -46,7 +93,9 @@ private static void ValidateGeneratedProxy() private static void ValidateSharedState(bool unordered) { - using var fixture = new ReferenceConstructionFixture(unordered, proxyType: typeof(InspectableConstructionProxy)); + using var fixture = new ReferenceConstructionFixture( + unordered, proxyType: typeof(InspectableConstructionProxy), + factory: static (shared, key) => new InspectableConstructionProxy(shared, key), includeGeneratedFactories: true); var first = (InspectableConstructionProxy)fixture.CreateReference("first-key"); var second = (InspectableConstructionProxy)fixture.CreateReference("second-key"); var shared = first.ConstructionShared; @@ -69,14 +118,16 @@ private static void ValidateConstructorFailures() () => invalid.CreateReference("key"), "Invalid proxy type: " + typeof(MissingConstructorProxy)); - using var throwing = new ReferenceConstructionFixture(unordered: false, proxyType: typeof(ThrowingConstructorProxy)); + using var throwing = new ReferenceConstructionFixture( + unordered: false, proxyType: typeof(ThrowingConstructorProxy), + factory: static (shared, key) => new ThrowingConstructorProxy(shared, key), includeGeneratedFactories: true); ExpectException(() => throwing.CreateReference("key"), "proxy-constructor"); } - [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicConstructors, typeof(NonPublicConstructorProxy))] private static void ValidateNonPublicConstructor() { - using var fixture = new ReferenceConstructionFixture(unordered: false, proxyType: typeof(NonPublicConstructorProxy)); + using var fixture = new ReferenceConstructionFixture( + unordered: false, proxyType: typeof(NonPublicConstructorProxy), factory: NonPublicConstructorProxy.Create); var reference = fixture.CreateReference("private-constructor"); Ensure(reference is NonPublicConstructorProxy, "The preserved non-public proxy constructor was not invoked."); Ensure(reference.GrainId.Key == IdSpan.Create("private-constructor"), "The non-public constructor changed the grain key.");