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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
<PackageVersion Include="MessagePack" Version="$(MessagePackVersion)" />
<PackageVersion Include="Microsoft.Bcl.TimeProvider" Version="8.0.1" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="$(MicrosoftCodeAnalysisVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics" Version="8.0.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
Expand Down
11 changes: 11 additions & 0 deletions MagicOnion.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
<File Path="NuGet.Config" />
<File Path="README.md" />
</Folder>
<Folder Name="/Multicast/">
<Project Path="../Multicaster/src/Multicaster.Distributed.Redis/Multicaster.Distributed.Redis.csproj" />
<Project Path="../Multicaster/src/Multicaster.SourceGenerator/Multicaster.SourceGenerator.csproj" />
<Project Path="../Multicaster/src/Multicaster/Multicaster.csproj" />
</Folder>
<Folder Name="/perf/" />
<Folder Name="/perf/BenchmarkApp/">
<Project Path="perf/BenchmarkApp/PerformanceTest.Client/PerformanceTest.Client.csproj" />
Expand All @@ -31,6 +36,10 @@
<Project Path="perf/SourceGeneratorPerf/SourceGeneratorPerf/SourceGeneratorPerf.csproj" />
</Folder>
<Folder Name="/samples/" />
<Folder Name="/samples/AotSample/">
<Project Path="samples/AotSample/AotSample.Server/AotSample.Server.csproj" />
<Project Path="samples/AotSample/AotSample.Shared/AotSample.Shared.csproj" />
</Folder>
<Folder Name="/samples/ChatApp/">
<Project Path="samples/ChatApp/ChatApp.Console/ChatApp.Console.csproj" />
<Project Path="samples/ChatApp/ChatApp.Server/ChatApp.Server.csproj" />
Expand All @@ -50,6 +59,7 @@
<Project Path="src/MagicOnion.Abstractions/MagicOnion.Abstractions.csproj" />
<Project Path="src/MagicOnion.Client.SourceGenerator/MagicOnion.Client.SourceGenerator.csproj" />
<Project Path="src/MagicOnion.Client/MagicOnion.Client.csproj" />
<Project Path="src/MagicOnion.Server.SourceGenerator/MagicOnion.Server.SourceGenerator.csproj" />
<Project Path="src/MagicOnion.Internal/MagicOnion.Internal.csproj" />
<Project Path="src/MagicOnion.Serialization.MemoryPack/MagicOnion.Serialization.MemoryPack.csproj" />
<Project Path="src/MagicOnion.Serialization.MessagePack/MagicOnion.Serialization.MessagePack.csproj" />
Expand All @@ -71,6 +81,7 @@
<Project Path="tests/MagicOnion.Server.InternalTesting/MagicOnion.Server.InternalTesting.csproj" />
<Project Path="tests/MagicOnion.Server.JsonTranscoding.Tests/MagicOnion.Server.JsonTranscoding.Tests.csproj" />
<Project Path="tests/MagicOnion.Server.Redis.Tests/MagicOnion.Server.Redis.Tests.csproj" />
<Project Path="tests/MagicOnion.Server.SourceGenerator.Tests/MagicOnion.Server.SourceGenerator.Tests.csproj" />
<Project Path="tests/MagicOnion.Server.Tests/MagicOnion.Server.Tests.csproj" />
</Folder>
<Folder Name="/tests/samples/">
Expand Down
259 changes: 259 additions & 0 deletions docs/docs/aot-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
# Native AOT Support

MagicOnion supports .NET Native AOT publishing through the `MagicOnion.Server.SourceGenerator` package. This allows you to publish your MagicOnion server applications as native executables with faster startup times and smaller memory footprint.

## Overview

By default, MagicOnion uses reflection to discover services and generate method handlers at runtime. This approach is incompatible with Native AOT because:

1. Assembly scanning requires runtime reflection
2. `Expression.Compile()` is not supported in AOT
3. `Activator.CreateInstance()` with generic types requires runtime code generation

The `MagicOnion.Server.SourceGenerator` solves these issues by generating all necessary code at compile time.

## Getting Started

### 1. Install the Source Generator

Add the `MagicOnion.Server.SourceGenerator` package to your server project:

```xml
<ItemGroup>
<PackageReference Include="MagicOnion.Server.SourceGenerator"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
```

### 2. Create a Method Provider Class

Create a partial class decorated with `[MagicOnionServerGeneration]`:

```csharp
using MagicOnion;

// Option 1: Explicit service types
[MagicOnionServerGeneration(typeof(GreeterService), typeof(ChatHub))]
public partial class MagicOnionMethodProvider { }

// Option 2: Auto-discover all services in the compilation
[MagicOnionServerGeneration]
public partial class MagicOnionMethodProvider { }
```

### 3. Configure the Server

Use the `UseStaticMethodProvider` extension method to register the generated provider:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();
builder.Services.AddMagicOnion()
.UseStaticMethodProvider<MagicOnionMethodProvider>();

var app = builder.Build();

app.MapMagicOnionService([typeof(GreeterService), typeof(ChatHub)]);

app.Run();
```

## StreamingHub Broadcast Support (Multicaster)

If your application uses StreamingHub with broadcast features (like `Broadcast.All.OnMessage(...)`), you also need to configure the Multicaster proxy factory for AOT.

### 1. Install Multicaster.SourceGenerator

Add the `Multicaster.SourceGenerator` package:

```xml
<ItemGroup>
<PackageReference Include="Multicaster.SourceGenerator"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
```

### 2. Create a Proxy Factory Class

Create a partial class decorated with `[MulticasterProxyGeneration]` that lists all your StreamingHub receiver interfaces:

```csharp
using Cysharp.Runtime.Multicast;

[MulticasterProxyGeneration(typeof(IChatHubReceiver), typeof(IGameHubReceiver))]
public partial class MulticasterProxyFactory { }
```

### 3. Configure Both Providers

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();
builder.Services.AddMagicOnion()
.UseStaticMethodProvider<MagicOnionMethodProvider>()
.UseStaticProxyFactory<MulticasterProxyFactory>(); // Add this for StreamingHub broadcast

var app = builder.Build();

app.MapMagicOnionService([typeof(GreeterService), typeof(ChatHub)]);

app.Run();
```

### Complete AOT Example

Here's a complete example for a chat application:

```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();
builder.Services.AddMagicOnion()
.UseStaticMethodProvider<MagicOnionMethodProvider>()
.UseStaticProxyFactory<MulticasterProxyFactory>();

var app = builder.Build();
app.MapMagicOnionService([typeof(ChatHub)]);
app.Run();

// MagicOnionMethodProvider.cs
[MagicOnionServerGeneration(typeof(ChatHub))]
public partial class MagicOnionMethodProvider { }

// MulticasterProxyFactory.cs
[MulticasterProxyGeneration(typeof(IChatHubReceiver))]
public partial class MulticasterProxyFactory { }

// ChatHub.cs
public interface IChatHubReceiver
{
void OnMessage(string user, string message);
void OnUserJoined(string user);
}

public interface IChatHub : IStreamingHub<IChatHub, IChatHubReceiver>
{
Task JoinAsync(string roomName, string userName);
Task SendMessageAsync(string message);
}

public class ChatHub : StreamingHubBase<IChatHub, IChatHubReceiver>, IChatHub
{
IGroup<IChatHubReceiver>? room;
string userName = "";

public async Task JoinAsync(string roomName, string userName)
{
this.userName = userName;
room = await Group.AddAsync(roomName);
Broadcast(room).OnUserJoined(userName);
}

public Task SendMessageAsync(string message)
{
Broadcast(room!).OnMessage(userName, message);
return Task.CompletedTask;
}
}
```

### 4. Enable AOT Publishing

Add the following properties to your project file:

```xml
<PropertyGroup>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
```

## How It Works

The source generator analyzes your service implementations at compile time and generates:

1. **Static method lists** - Pre-computed lists of `IMagicOnionGrpcMethod` instances
2. **Static invokers** - Lambda expressions that call service methods directly
3. **Hub method registrations** - Pre-computed StreamingHub method handlers

This eliminates all runtime reflection and dynamic code generation.

## Generated Code Example

For a service like:

```csharp
public interface IGreeterService : IService<IGreeterService>
{
UnaryResult<string> SayHello(string name);
}

public class GreeterService : ServiceBase<IGreeterService>, IGreeterService
{
public UnaryResult<string> SayHello(string name)
=> UnaryResult.FromResult($"Hello, {name}!");
}
```

The generator produces:

```csharp
partial class MagicOnionMethodProvider : IMagicOnionGrpcMethodProvider
{
public void MapAllSupportedServiceTypes(MagicOnionGrpcServiceMappingContext context)
{
context.Map<GreeterService>();
}

public IReadOnlyList<IMagicOnionGrpcMethod> GetGrpcMethods<TService>() where TService : class
{
if (typeof(TService) == typeof(GreeterService))
return __GreeterService_ServiceMethods.Methods;
return Array.Empty<IMagicOnionGrpcMethod>();
}

// ...
}

file static class __GreeterService_ServiceMethods
{
public static readonly IReadOnlyList<IMagicOnionGrpcMethod> Methods = new IMagicOnionGrpcMethod[]
{
new MagicOnionUnaryMethod<GreeterService, string, string, Box<string>, Box<string>>(
"IGreeterService",
"SayHello",
static (instance, context, request) => instance.SayHello(request)),
};
}
```

## Limitations

- All service types must be known at compile time
- Generic service implementations are not supported
- Dynamic service registration is not available

## Serialization

Ensure your serializer also supports AOT:

- **MessagePack**: Use `MessagePackSerializer` with source-generated formatters
- **MemoryPack**: Fully AOT-compatible by design

## Troubleshooting

### "No service implementation found"

Ensure your service classes:
- Are non-abstract
- Implement `IService<T>` or `IStreamingHub<T, TReceiver>`
- Are included in the `[MagicOnionServerGeneration]` attribute or are in the same compilation

### Trimming Warnings

If you see trimming warnings, ensure all types used in service methods are preserved. You may need to add `[DynamicallyAccessedMembers]` attributes or use `rd.xml` files.
13 changes: 13 additions & 0 deletions samples/AotSample/AotSample.Server/.config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.1",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}
25 changes: 25 additions & 0 deletions samples/AotSample/AotSample.Server/AotSample.Server.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Enable AOT publishing -->
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
<!-- Suppress AOT/Trim warnings - we use static providers so dynamic code paths are not executed -->
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
<NoWarn>$(NoWarn);IL3050;IL3053</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\AotSample.Shared\AotSample.Shared.csproj" />
<ProjectReference Include="..\..\..\src\MagicOnion.Server\MagicOnion.Server.csproj" />
<ProjectReference Include="..\..\..\src\MagicOnion.Serialization.MessagePack\MagicOnion.Serialization.MessagePack.csproj" />
<!-- MagicOnion Source Generator for AOT-compatible service methods -->
<ProjectReference Include="..\..\..\src\MagicOnion.Server.SourceGenerator\MagicOnion.Server.SourceGenerator.csproj" OutputItemType="Analyzer" />
<!-- Multicaster Source Generator for AOT-compatible StreamingHub broadcast -->
<ProjectReference Include="..\..\..\..\Multicaster\src\Multicaster.SourceGenerator\Multicaster.SourceGenerator.csproj" OutputItemType="Analyzer" />
</ItemGroup>

</Project>
Loading