-
-
Notifications
You must be signed in to change notification settings - Fork 459
feat: Add Native AOT support for MagicOnion Server #1022
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Implement a source generator that enables AOT-compatible server-side code generation, eliminating runtime reflection for service discovery and method handler creation. Changes: - Add MagicOnion.Server.SourceGenerator project with incremental generator - Add [MagicOnionServerGeneration] attribute to trigger code generation - Add UseStaticMethodProvider<T>() extension method for DI registration - Generate static IMagicOnionGrpcMethodProvider implementations at compile time - Support both explicit service type specification and auto-discovery - Add unit tests for the source generator - Add AOT sample project demonstrating usage - Add documentation for Native AOT support The generator produces: - Static method lists for Unary/ClientStreaming/ServerStreaming/DuplexStreaming - Static StreamingHub method handlers - Static lambda invokers (replacing Expression.Compile()) - Pre-computed service type mappings (replacing assembly scanning) This enables publishing MagicOnion servers with PublishAot=true.
…ation - Add Multicaster.SourceGenerator integration for AOT-compatible StreamingHub broadcast - Create MulticasterProxyFactory class with [MulticasterProxyGeneration] attribute - Implement ChatHub and IChatHubReceiver interfaces in AotSample for broadcast demonstration - Update MagicOnionMethodProvider to include ChatHub service generation - Configure both MagicOnion and Multicaster static providers in Program.cs - Add comprehensive documentation for StreamingHub broadcast AOT support with complete examples - Update solution file to include Multicaster projects in workspace - Add XML comments explaining proxy factory and method provider generation - Enable ReferenceOutputAssembly for source generators to ensure proper AOT compilation
Replaced NuGet package references with direct project references to Multicaster in solution and project files. Refactored MagicOnionServerBuilderExtensions to use simplified type names and added relevant using statements for proxy factory interfaces. This ensures the solution always uses the latest local Multicaster source and improves maintainability.
… support - Update AotSample.Server target framework from net8.0 to net10.0 - Add MagicOnionMessagePackFormatters.cs with custom AOT-compatible MessagePack resolver - Implement DynamicArgumentTupleFormatter instances for multi-parameter service methods - Add dotnet-tools.json configuration for Entity Framework tooling - Configure MessagePack serialization with CompositeResolver for AOT compatibility - Add MagicOnion.Serialization.MessagePack project reference to server project - Create AotSampleResolver.cs in shared project for source-generated DTO formatters - Add UserProfile.cs data transfer object for sample service - Update GreeterService with enhanced AOT-compatible implementation - Add FolderProfile.pubxml publish profile for AOT deployment - Update Program.cs with AOT-compatible MessagePack configuration and custom serializer provider - Add Multicaster.Distributed.Redis project reference to solution - Suppress AOT/Trim analysis warnings for static provider implementations - Add comprehensive README.md documentation for AOT sample setup and usage - Update source generator StaticMethodProviderGenerator for improved AOT support - Enhance streaming method binders with AOT-compatible metadata handling
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds Native AOT compilation support for MagicOnion Server, enabling deployment scenarios requiring ahead-of-time compilation with minimal startup time and memory footprint.
Key Changes:
- Introduces
MagicOnion.Server.SourceGeneratorto generate static method providers at compile time - Fixes AOT compatibility issues in StreamingHub Connect method resolution and MessagePack serialization
- Adds extension methods for AOT configuration (
UseStaticMethodProvider,UseStaticProxyFactory) - Provides a comprehensive AotSample demonstrating AOT usage
Reviewed changes
Copilot reviewed 49 out of 49 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/MagicOnion.Server.SourceGenerator/* | New source generator package for AOT-compatible static method provider generation |
| src/MagicOnion.Server/Internal/MethodHandlerMetadata.cs | Added AOT-friendly CreateStreamingHubConnectMethodHandlerMetadata method and made factory public |
| src/MagicOnion.Server/Extensions/MagicOnionServicesExtensions.cs | Changed to use factory methods for lazy initialization to support AOT |
| src/MagicOnion.Server/Extensions/MagicOnionServerBuilderExtensions.cs | New file with extension methods for static provider configuration |
| src/MagicOnion.Server/Binder/* | Added constructors accepting pre-computed metadata for AOT scenarios |
| src/MagicOnion.Serialization.MessagePack/MessagePackMagicOnionSerializerProvider.cs | Fixed to only throw AOT exception when parameters have default values |
| src/MagicOnion.Abstractions/MagicOnionServerGenerationAttribute.cs | New attribute to mark classes for source generation |
| samples/AotSample/* | Complete AOT sample with services, hubs, and formatters |
| docs/docs/aot-support.md | Comprehensive AOT documentation |
| tests/MagicOnion.Server.SourceGenerator.Tests/* | New test project for source generator |
| } | ||
|
|
||
| internal class MethodHandlerMetadataFactory | ||
| public class MethodHandlerMetadataFactory |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The visibility of MethodHandlerMetadataFactory has been changed from internal to public. This is necessary for the source generator to access it, but this introduces a breaking API change that exposes an internal implementation detail. Consider if this needs to be documented in the release notes or if an alternative design could keep this internal.
| foreach (var member in interfaceType.GetMembers()) | ||
| { | ||
| if (member is not IMethodSymbol methodSymbol) continue; | ||
| if (methodSymbol.MethodKind != MethodKind.Ordinary) continue; | ||
| if (HasIgnoreAttribute(methodSymbol, referenceSymbols)) continue; | ||
|
|
||
| // Skip well-known methods | ||
| if (IsWellKnownMethod(methodSymbol.Name)) continue; | ||
|
|
||
| if (TryCreateServiceMethodInfo(serviceName, methodSymbol, referenceSymbols, out var methodInfo, out var diagnostic)) | ||
| { | ||
| methods.Add(methodInfo); | ||
| } | ||
|
|
||
| if (diagnostic is not null) | ||
| { | ||
| diagnostics.Add(diagnostic); | ||
| } | ||
| } |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| foreach (var item in arg.Values) | ||
| { | ||
| if (item.Value is INamedTypeSymbol typeSymbol) | ||
| { | ||
| types.Add(typeSymbol); | ||
| } | ||
| } |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| foreach (var symbol in allClassSymbols) | ||
| { | ||
| if (symbol is null) continue; | ||
| if (symbol.IsAbstract) continue; | ||
| if (symbol.TypeKind != TypeKind.Class) continue; | ||
|
|
||
| // Check if implements IService<> or IStreamingHub<,> | ||
| var implementsService = symbol.AllInterfaces.Any(x => | ||
| x.OriginalDefinition.ApproximatelyEqual(referenceSymbols.IService)); | ||
| var implementsHub = symbol.AllInterfaces.Any(x => | ||
| x.OriginalDefinition.ApproximatelyEqual(referenceSymbols.IStreamingHub)); | ||
|
|
||
| if (implementsService || implementsHub) | ||
| { | ||
| services.Add(symbol); | ||
| } | ||
| } |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| foreach (var member in GetAllInterfaceMembers(interfaceType, referenceSymbols)) | ||
| { | ||
| if (member is not IMethodSymbol methodSymbol) continue; | ||
| if (methodSymbol.MethodKind != MethodKind.Ordinary) continue; | ||
| if (HasIgnoreAttribute(methodSymbol, referenceSymbols)) continue; | ||
|
|
||
| // Skip well-known methods | ||
| if (IsWellKnownMethod(methodSymbol.Name)) continue; | ||
|
|
||
| if (TryCreateStreamingHubMethodInfo(serviceName, methodSymbol, referenceSymbols, out var methodInfo, out var diagnostic)) | ||
| { | ||
| // Avoid duplicates | ||
| if (!methods.Any(x => x.MethodName == methodInfo.MethodName)) | ||
| { | ||
| methods.Add(methodInfo); | ||
| } | ||
| } | ||
|
|
||
| if (diagnostic is not null) | ||
| { | ||
| diagnostics.Add(diagnostic); | ||
| } | ||
| } |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.
| var safeTypeName = GetSafeTypeName(service.ImplementationType); | ||
| var implTypeName = service.ImplementationType.FullName; | ||
| var serviceName = service.ServiceInterfaceType.Name; | ||
| var serviceInterfaceTypeName = service.ServiceInterfaceType.FullName; |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to serviceInterfaceTypeName is useless, since its value is never read.
| var returnType = MagicOnionTypeInfo.CreateFromSymbol(methodSymbol.ReturnType); | ||
| var parameters = CreateParameterInfoList(methodSymbol); | ||
| var requestType = CreateRequestType(parameters); | ||
| var responseType = MagicOnionTypeInfo.KnownTypes.MessagePack_Nil; |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to responseType is useless, since its value is never read.
| var responseType = MagicOnionTypeInfo.KnownTypes.MessagePack_Nil; | |
| MagicOnionTypeInfo responseType; |
| if (TryCreateStreamingHubMethodInfo(serviceName, methodSymbol, referenceSymbols, out var methodInfo, out var diagnostic)) | ||
| { | ||
| // Avoid duplicates | ||
| if (!methods.Any(x => x.MethodName == methodInfo.MethodName)) | ||
| { | ||
| methods.Add(methodInfo); | ||
| } | ||
| } |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These 'if' statements can be combined.
| if (l.Kind == SymbolDisplayPartKind.FieldName) | ||
| { | ||
| return l.Symbol!.ToDisplayString(); | ||
| } | ||
| else | ||
| { | ||
| return l.ToString(); | ||
| } |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both branches of this 'if' statement return - consider using '?' to express intent better.
| if (left is IErrorTypeSymbol || right is IErrorTypeSymbol) | ||
| { | ||
| return left.ToDisplayString() == right.ToDisplayString(); | ||
| } | ||
| else | ||
| { | ||
| return SymbolEqualityComparer.Default.Equals(left, right); | ||
| } |
Copilot
AI
Jan 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both branches of this 'if' statement return - consider using '?' to express intent better.
|
Hi @mayuki, Thank you for your contributions to the community. We are using MagicOnion for server-client communication in our HFT application. Currently, since AOT is not supported, this impacts the tail latency in our application as we need to restart it daily. I'm not sure if you would be open to receiving this PR, but I hope you are. Most of the code was generated with the help of AI, but I have conducted comprehensive testing. You can review my repository to ensure it aligns with MagicOnion's coding standards. I referenced the Multicaster project directly in MagicOnion instead of using the NuGet package to facilitate testing the SourceGenerator functionality. If certain preparations are completed, Multicaster.SourceGenerator will need to be published first so that adjustments can be made to the code here. |
Summary
This PR adds Native AOT compilation support for MagicOnion Server, enabling deployment scenarios that require ahead-of-time compilation such as cloud-native microservices with minimal startup time and memory footprint.
Issue: #929
Changes
MagicOnion.Server
MethodHandlerMetadata.cs: Fixed
CreateStreamingHubConnectMethodHandlerMetadatato use interface method directly fromIStreamingHubBaseinstead of searching by name pattern, which fails in AOT due toGetInterfaceMapnot being supported.MagicOnionServicesExtensions.cs: Changed
DynamicInMemoryProxyFactory.InstanceandDynamicRemoteProxyFactory.Instanceto use factory methods for lazy initialization, avoiding static constructor issues in AOT.MagicOnion.Serialization.MessagePack
WrapFallbackResolverIfNeededto only throw AOT exception when parameters actually have default values, allowing methods without optional parameters to work in AOT.Multicaster.SourceGenerator
GetReceiverInterfaceTypesto handle both single type parameters andparamsarray parameters in[MulticasterProxyGeneration]attribute.New Sample: AotSample
Added a complete AOT sample demonstrating:
[MessagePackObject]DTOsDynamicArgumentTupleFormatterregistration for methods with 2+ parameters[GeneratedMessagePackResolver]StandardResolver(which uses reflection)How to Use AOT
Requirements
MagicOnion.Server.SourceGenerator- generates static method providersMulticaster.SourceGenerator- generates static proxy factories for StreamingHubDynamicArgumentTupleFormatter<T1, T2, ...>for service methods with 2+ parametersProject Configuration
Testing
Tested with .NET 10 Preview on Windows:
Verified:
SayHelloAsync,AddAsync)[MessagePackObject]parameters and return types (GetUserAsync,CreateUserAsync)Sample Project Structure
Breaking Changes
None. This is an additive feature that requires opt-in configuration.
Checklist