Skip to content

Conversation

@yang-xiaodong
Copy link

@yang-xiaodong yang-xiaodong commented Jan 6, 2026

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 CreateStreamingHubConnectMethodHandlerMetadata to use interface method directly from IStreamingHubBase instead of searching by name pattern, which fails in AOT due to GetInterfaceMap not being supported.

  • MagicOnionServicesExtensions.cs: Changed DynamicInMemoryProxyFactory.Instance and DynamicRemoteProxyFactory.Instance to use factory methods for lazy initialization, avoiding static constructor issues in AOT.

MagicOnion.Serialization.MessagePack

  • MessagePackMagicOnionSerializerProvider.cs: Updated WrapFallbackResolverIfNeeded to only throw AOT exception when parameters actually have default values, allowing methods without optional parameters to work in AOT.

Multicaster.SourceGenerator

  • MulticasterSourceGenerator.cs: Fixed GetReceiverInterfaceTypes to handle both single type parameters and params array parameters in [MulticasterProxyGeneration] attribute.

New Sample: AotSample

Added a complete AOT sample demonstrating:

  • Unary service with [MessagePackObject] DTOs
  • StreamingHub with broadcast functionality
  • Custom DynamicArgumentTupleFormatter registration for methods with 2+ parameters
  • MessagePack Source Generator integration with [GeneratedMessagePackResolver]
  • Proper resolver configuration without StandardResolver (which uses reflection)

How to Use AOT

// 1. Create a MessagePack resolver for your DTOs
[GeneratedMessagePackResolver]
public partial class MyResolver;

// 2. Register DynamicArgumentTuple formatters for methods with 2+ params
public class MagicOnionAotFormatterResolver : IFormatterResolver
{
    public static readonly MagicOnionAotFormatterResolver Instance = new();
    
    public IMessagePackFormatter<T>? GetFormatter<T>()
        => FormatterCache<T>.Formatter;

    private static class FormatterCache<T>
    {
        public static readonly IMessagePackFormatter<T>? Formatter =
            (IMessagePackFormatter<T>?)MagicOnionAotFormatters.GetFormatter(typeof(T));
    }
}

public static class MagicOnionAotFormatters
{
    private static readonly DynamicArgumentTupleFormatter<int, int> _intIntFormatter = new(default, default);

    public static object? GetFormatter(Type type)
    {
        if (type == typeof(DynamicArgumentTuple<int, int>)) return _intIntFormatter;
        return null;
    }
}

// 3. Configure MessagePack without StandardResolver
var options = MessagePackSerializerOptions.Standard.WithResolver(
    CompositeResolver.Create(
        MagicOnionAotFormatterResolver.Instance,
        MyResolver.Instance,
        BuiltinResolver.Instance,
        AttributeFormatterResolver.Instance,
        PrimitiveObjectResolver.Instance
    ));

// 4. Use static providers
builder.Services.AddMagicOnion(opt => 
    opt.MessageSerializer = MessagePackMagicOnionSerializerProvider.Default.WithOptions(options))
    .UseStaticMethodProvider<MagicOnionMethodProvider>()
    .UseStaticProxyFactory<MulticasterProxyFactory>();

Requirements

  • MagicOnion.Server.SourceGenerator - generates static method providers
  • Multicaster.SourceGenerator - generates static proxy factories for StreamingHub
  • Manual registration of DynamicArgumentTupleFormatter<T1, T2, ...> for service methods with 2+ parameters

Project Configuration

<PropertyGroup>
    <PublishAot>true</PublishAot>
    <InvariantGlobalization>true</InvariantGlobalization>
    <!-- Suppress AOT warnings from libraries using dynamic code -->
    <SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
    <NoWarn>$(NoWarn);IL3050;IL3053</NoWarn>
</PropertyGroup>

<ItemGroup>
    <!-- Source Generators -->
    <ProjectReference Include="MagicOnion.Server.SourceGenerator" OutputItemType="Analyzer" />
    <ProjectReference Include="Multicaster.SourceGenerator" OutputItemType="Analyzer" />
</ItemGroup>

Testing

Tested with .NET 10 Preview on Windows:

dotnet publish -c Release -r win-x64
./bin/Release/net10.0/win-x64/publish/AotSample.Server.exe

Verified:

  • Unary service calls (SayHelloAsync, AddAsync)
  • Methods with [MessagePackObject] parameters and return types (GetUserAsync, CreateUserAsync)
  • StreamingHub connection and broadcast

Sample Project Structure

samples/AotSample/
├── AotSample.Server/
│   ├── Services/
│   │   ├── GreeterService.cs
│   │   └── ChatHub.cs
│   ├── MagicOnionMessagePackFormatters.cs
│   ├── Program.cs
│   └── AotSample.Server.csproj
├── AotSample.Shared/
│   ├── IGreeterService.cs
│   ├── IChatHub.cs
│   ├── UserProfile.cs
│   ├── AotSampleResolver.cs
│   └── AotSample.Shared.csproj
└── README.md

Breaking Changes

None. This is an additive feature that requires opt-in configuration.

Checklist

  • Added AOT sample project
  • Added README documentation
  • Fixed reflection-based code paths for AOT compatibility
  • Tested with Native AOT publish

yang-xiaodong and others added 4 commits January 5, 2026 21:15
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
Copilot AI review requested due to automatic review settings January 6, 2026 12:57
@yang-xiaodong yang-xiaodong requested a review from mayuki as a code owner January 6, 2026 12:57
Copy link
Contributor

Copilot AI left a 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.SourceGenerator to 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
Copy link

Copilot AI Jan 6, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +128
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);
}
}
Copy link

Copilot AI Jan 6, 2026

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(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +116 to +122
foreach (var item in arg.Values)
{
if (item.Value is INamedTypeSymbol typeSymbol)
{
types.Add(typeSymbol);
}
}
Copy link

Copilot AI Jan 6, 2026

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(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +151
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);
}
}
Copy link

Copilot AI Jan 6, 2026

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(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +162
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);
}
}
Copy link

Copilot AI Jan 6, 2026

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(...)'.

Copilot uses AI. Check for mistakes.
var safeTypeName = GetSafeTypeName(service.ImplementationType);
var implTypeName = service.ImplementationType.FullName;
var serviceName = service.ServiceInterfaceType.Name;
var serviceInterfaceTypeName = service.ServiceInterfaceType.FullName;
Copy link

Copilot AI Jan 6, 2026

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.

Copilot uses AI. Check for mistakes.
var returnType = MagicOnionTypeInfo.CreateFromSymbol(methodSymbol.ReturnType);
var parameters = CreateParameterInfoList(methodSymbol);
var requestType = CreateRequestType(parameters);
var responseType = MagicOnionTypeInfo.KnownTypes.MessagePack_Nil;
Copy link

Copilot AI Jan 6, 2026

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.

Suggested change
var responseType = MagicOnionTypeInfo.KnownTypes.MessagePack_Nil;
MagicOnionTypeInfo responseType;

Copilot uses AI. Check for mistakes.
Comment on lines +149 to +156
if (TryCreateStreamingHubMethodInfo(serviceName, methodSymbol, referenceSymbols, out var methodInfo, out var diagnostic))
{
// Avoid duplicates
if (!methods.Any(x => x.MethodName == methodInfo.MethodName))
{
methods.Add(methodInfo);
}
}
Copy link

Copilot AI Jan 6, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +43
if (l.Kind == SymbolDisplayPartKind.FieldName)
{
return l.Symbol!.ToDisplayString();
}
else
{
return l.ToString();
}
Copy link

Copilot AI Jan 6, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +24
if (left is IErrorTypeSymbol || right is IErrorTypeSymbol)
{
return left.ToDisplayString() == right.ToDisplayString();
}
else
{
return SymbolEqualityComparer.Default.Equals(left, right);
}
Copy link

Copilot AI Jan 6, 2026

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.

Copilot uses AI. Check for mistakes.
@yang-xiaodong
Copy link
Author

yang-xiaodong commented Jan 6, 2026

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant