-
Notifications
You must be signed in to change notification settings - Fork 5
Add Source Generator for AOT-compatible proxy generation #30
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
- Add Multicaster.SourceGenerator project with source generator implementation - Implement MulticasterSourceGenerator to analyze receiver interfaces and generate proxy code - Add DiagnosticDescriptors for source generator diagnostics and error reporting - Add ReceiverInterfaceCollector to discover and validate receiver interface types - Add InMemoryProxyGenerator to generate in-memory proxy implementations - Add RemoteProxyGenerator to generate remote proxy implementations - Add ProxyFactoryGenerator to generate factory method implementations - Add helper utilities (RoslynExtensions, StringBuilderExtensions, FNV1A32) - Add MulticasterProxyGenerationAttribute for marking proxy factory classes - Add comprehensive source generator tests with SourceGeneratorTests project - Add AOT support documentation explaining usage and benefits - Update Directory.Packages.props with Microsoft.CodeAnalysis.CSharp and PolySharp dependencies - Update solution file to include new projects - Enables AOT compilation scenarios (Native AOT, trimming, Blazor WASM) by generating proxies at compile time instead of runtime
Updated Multicaster.SourceGenerator.csproj to: - Add required NuGet package references for source generation (Microsoft.CodeAnalysis.CSharp, Microsoft.CodeAnalysis.Analyzers, PolySharp) with PrivateAssets="all". - Include the built analyzer DLL in the NuGet package under analyzers/dotnet/cs. - Change file encoding (likely to UTF-8 with BOM). These changes improve packaging and dependency management for the source generator.
- Add generated/ directory to gitignore to exclude source generator output - Prevents accidental commits of auto-generated code files - Keeps repository clean by ignoring build artifacts from source generators
…uide - Add MagicOnion StreamingHub to list of supported AOT scenarios - Document project reference setup for source generator during development - Add comprehensive MagicOnion integration example with StreamingHub receiver - Include configuration example for static proxy factory with MagicOnion - Add reference to MagicOnion AOT Sample for complete implementation details - Improve documentation completeness for AOT adoption and integration patterns
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 a new Multicaster.SourceGenerator package that enables AOT (Ahead-of-Time) compilation support for Multicaster by generating static proxy implementations at compile time, replacing runtime System.Reflection.Emit usage.
Key Changes:
- New source generator that analyzes
[MulticasterProxyGeneration]attributes and generates both InMemory and Remote proxy implementations - New marker attribute for triggering source generation at compile time
- Comprehensive test suite covering proxy generation and invocation scenarios
Reviewed changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
src/Multicaster/MulticasterProxyGenerationAttribute.cs |
Marker attribute for source generation, accepts params array of receiver interface types |
src/Multicaster.SourceGenerator/MulticasterSourceGenerator.cs |
Main incremental generator entry point using Roslyn's ForAttributeWithMetadataName |
src/Multicaster.SourceGenerator/CodeAnalysis/ReceiverInterfaceCollector.cs |
Collects interface metadata including methods, parameters, and return types |
src/Multicaster.SourceGenerator/CodeAnalysis/ReceiverInterfaceInfo.cs |
Data models representing receiver interfaces and their methods |
src/Multicaster.SourceGenerator/CodeAnalysis/DiagnosticDescriptors.cs |
Defines diagnostic errors and warnings for validation |
src/Multicaster.SourceGenerator/CodeGen/ProxyFactoryGenerator.cs |
Generates factory class implementing both IInMemoryProxyFactory and IRemoteProxyFactory |
src/Multicaster.SourceGenerator/CodeGen/InMemoryProxyGenerator.cs |
Generates in-memory proxy classes that iterate over receivers |
src/Multicaster.SourceGenerator/CodeGen/RemoteProxyGenerator.cs |
Generates remote proxy classes that serialize method calls |
src/Multicaster.SourceGenerator/Helpers/RoslynExtensions.cs |
Helper extensions for Roslyn symbol operations |
src/Multicaster.SourceGenerator/Helpers/StringBuilderExtensions.cs |
StringBuilder utilities for code generation |
src/Multicaster.SourceGenerator/Internal/FNV1A32.cs |
Hash function for generating method IDs |
test/Multicaster.SourceGenerator.Tests/GeneratedProxyFactory.cs |
Test factory annotated with [MulticasterProxyGeneration] attribute |
test/Multicaster.SourceGenerator.Tests/TestReceivers.cs |
Test interface definitions for chat, game, and async client results |
test/Multicaster.SourceGenerator.Tests/SourceGeneratorTests.cs |
Unit tests verifying proxy creation and invocation |
docs/aot-support.md |
Comprehensive documentation including usage, migration guide, and MagicOnion integration |
Multicaster.sln |
Added new source generator and test projects to solution |
Directory.Packages.props |
Added Roslyn analyzer dependencies |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public static readonly DiagnosticDescriptor NoReceiverInterfacesFound = new( | ||
| id: "MULT004", | ||
| title: "No receiver interfaces found", | ||
| messageFormat: "No receiver interfaces were specified or found for proxy generation", | ||
| category: Category, | ||
| defaultSeverity: DiagnosticSeverity.Warning, | ||
| isEnabledByDefault: true | ||
| ); | ||
|
|
||
| public static readonly DiagnosticDescriptor NoReceiverInterfacesSpecified = new( | ||
| id: "MULT006", | ||
| title: "No receiver interfaces specified", | ||
| messageFormat: "The [MulticasterProxyGeneration] attribute must specify at least one receiver interface type", | ||
| category: Category, | ||
| defaultSeverity: DiagnosticSeverity.Error, | ||
| isEnabledByDefault: true | ||
| ); | ||
|
|
||
| public static readonly DiagnosticDescriptor TooManyParameters = new( | ||
| id: "MULT005", | ||
| title: "Too many parameters", | ||
| messageFormat: "The method '{0}' has {1} parameters, but the maximum supported is 15", | ||
| category: Category, | ||
| defaultSeverity: DiagnosticSeverity.Error, | ||
| isEnabledByDefault: true | ||
| ); |
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 diagnostic descriptors TooManyParameters and NoReceiverInterfacesFound are defined but never used in the codebase. If they are intended for future use, consider removing them for now to reduce code clutter, or implement the validation logic that would use them. For TooManyParameters specifically, the documentation mentions a 15 parameter limit but no validation enforces this constraint.
| public static readonly DiagnosticDescriptor NoReceiverInterfacesSpecified = new( | ||
| id: "MULT006", | ||
| title: "No receiver interfaces specified", | ||
| messageFormat: "The [MulticasterProxyGeneration] attribute must specify at least one receiver interface type", | ||
| category: Category, | ||
| defaultSeverity: DiagnosticSeverity.Error, | ||
| isEnabledByDefault: true | ||
| ); | ||
|
|
||
| public static readonly DiagnosticDescriptor TooManyParameters = new( | ||
| id: "MULT005", | ||
| title: "Too many parameters", | ||
| messageFormat: "The method '{0}' has {1} parameters, but the maximum supported is 15", | ||
| category: Category, | ||
| defaultSeverity: DiagnosticSeverity.Error, | ||
| isEnabledByDefault: true | ||
| ); |
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 diagnostic IDs are not in sequential order (MULT001, MULT002, MULT003, MULT004, MULT006, MULT005). Consider reordering MULT005 (TooManyParameters) to come before MULT006 (NoReceiverInterfacesSpecified) for better maintainability and consistency.
| public static StringBuilder AppendLineWithFormat(this StringBuilder sb, [InterpolatedStringHandlerArgument(nameof(sb))] StringBuilderInterpolatedStringHandler s) | ||
| { | ||
| sb.AppendLine(); | ||
| return sb; | ||
| } | ||
|
|
||
| [InterpolatedStringHandler] | ||
| public readonly ref struct StringBuilderInterpolatedStringHandler | ||
| { | ||
| readonly StringBuilder sb; | ||
|
|
||
| public StringBuilderInterpolatedStringHandler(int literalLength, int formattedCount, StringBuilder sb) | ||
| { | ||
| this.sb = sb; | ||
| } | ||
|
|
||
| public void AppendLiteral(string s) | ||
| { | ||
| sb.Append(s); | ||
| } | ||
|
|
||
| public void AppendFormatted<T>(T t) | ||
| { | ||
| sb.Append(t); | ||
| } | ||
| } |
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 AppendLineWithFormat method and its associated StringBuilderInterpolatedStringHandler are defined but never used in the codebase. Consider removing these to reduce unnecessary code, or use them in the code generation logic if they were intended to simplify string interpolation.
| public static StringBuilder AppendLineWithFormat(this StringBuilder sb, [InterpolatedStringHandlerArgument(nameof(sb))] StringBuilderInterpolatedStringHandler s) | |
| { | |
| sb.AppendLine(); | |
| return sb; | |
| } | |
| [InterpolatedStringHandler] | |
| public readonly ref struct StringBuilderInterpolatedStringHandler | |
| { | |
| readonly StringBuilder sb; | |
| public StringBuilderInterpolatedStringHandler(int literalLength, int formattedCount, StringBuilder sb) | |
| { | |
| this.sb = sb; | |
| } | |
| public void AppendLiteral(string s) | |
| { | |
| sb.Append(s); | |
| } | |
| public void AppendFormatted<T>(T t) | |
| { | |
| sb.Append(t); | |
| } | |
| } |
| static string GetSafeTypeName(string typeName) | ||
| { | ||
| return typeName.Replace(".", "_").Replace("<", "_").Replace(">", "_").Replace(",", "_"); | ||
| } |
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 GetSafeTypeName method is duplicated across InMemoryProxyGenerator, RemoteProxyGenerator, and ProxyFactoryGenerator. Consider extracting this to a shared helper class to eliminate code duplication and ensure consistent behavior across all generators.
| public static AttributeData? FindAttributeShortName(this IEnumerable<AttributeData> attributeDataList, string typeName) | ||
| { | ||
| return attributeDataList.FirstOrDefault(x => x.AttributeClass?.Name == typeName); | ||
| } | ||
|
|
||
| public static bool ApproximatelyEqual(this ITypeSymbol? left, ITypeSymbol? right) | ||
| { | ||
| if (left == null && right == null) return true; | ||
| if (left == null || right == null) return false; | ||
|
|
||
| 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.
The extension methods FindAttributeShortName and ApproximatelyEqual are defined but never used in the codebase. Consider removing these unused methods to reduce code clutter, or use them if they were intended for specific logic.
| public static AttributeData? FindAttributeShortName(this IEnumerable<AttributeData> attributeDataList, string typeName) | |
| { | |
| return attributeDataList.FirstOrDefault(x => x.AttributeClass?.Name == typeName); | |
| } | |
| public static bool ApproximatelyEqual(this ITypeSymbol? left, ITypeSymbol? right) | |
| { | |
| if (left == null && right == null) return true; | |
| if (left == null || right == null) return false; | |
| if (left is IErrorTypeSymbol || right is IErrorTypeSymbol) | |
| { | |
| return left.ToDisplayString() == right.ToDisplayString(); | |
| } | |
| else | |
| { | |
| return SymbolEqualityComparer.Default.Equals(left, right); | |
| } | |
| } |
| static IEnumerable<IMethodSymbol> GetAllInterfaceMethods(INamedTypeSymbol interfaceType) | ||
| { | ||
| foreach (var member in interfaceType.GetMembers()) | ||
| { | ||
| if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary) | ||
| { | ||
| yield return method; | ||
| } | ||
| } | ||
|
|
||
| foreach (var baseInterface in interfaceType.AllInterfaces) | ||
| { | ||
| foreach (var member in baseInterface.GetMembers()) | ||
| { | ||
| if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary) | ||
| { | ||
| yield return method; | ||
| } | ||
| } | ||
| } | ||
| } |
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 method GetAllInterfaceMethods may return duplicate methods when an interface inherits from base interfaces. For example, if two base interfaces declare the same method, or if a method is redeclared in the derived interface, this will yield the same method multiple times, leading to duplicate method implementations in the generated proxy class. Consider deduplicating methods by their signature or using SymbolEqualityComparer to track already-seen methods.
| foreach (var member in interfaceType.GetMembers()) | ||
| { | ||
| if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary) | ||
| { | ||
| yield return method; | ||
| } | ||
| } |
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 baseInterface.GetMembers()) | ||
| { | ||
| if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary) | ||
| { | ||
| yield return method; | ||
| } | ||
| } |
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(...)'.
| 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.
Summary
This PR adds a new
Multicaster.SourceGeneratorpackage that generates static proxy implementations at compile time, enabling Native AOT compilation support for Multicaster.Motivation
The current
DynamicInMemoryProxyFactoryandDynamicRemoteProxyFactoryuseSystem.Reflection.Emitto generate proxy classes at runtime. This approach is incompatible with:New Package: Multicaster.SourceGenerator
A Roslyn source generator that analyzes
[MulticasterProxyGeneration]attributes and generates:IInMemoryProxyFactoryandIRemoteProxyFactoryProject Structure
Usage
1. Add the package
2. Define receiver interfaces
3. Create a proxy factory
4. Use the generated factory
Features
IInMemoryProxyFactoryimplementationIRemoteProxyFactoryimplementationTaskandTask<T>return types (client results)CancellationTokenparametersparamsarray in attribute constructorChanges
New Files
src/Multicaster.SourceGenerator/- Complete source generator implementationsrc/Multicaster/MulticasterProxyGenerationAttribute.cs- Marker attribute for code generationtest/Multicaster.SourceGenerator.Tests/- Unit tests for the generatordocs/aot-support.md- DocumentationModified Files
Multicaster.sln- Added new projectsDirectory.Packages.props- Added analyzer dependencies.gitignore- Added**/generated/for source generator outputTesting
dotnet test test/Multicaster.SourceGenerator.TestsThe tests verify:
Integration with MagicOnion
This source generator is designed to work with MagicOnion's AOT support:
Documentation
Added
docs/aot-support.mdwith:Breaking Changes
None. This is an additive feature. Existing code using
DynamicInMemoryProxyFactoryandDynamicRemoteProxyFactorycontinues to work unchanged.Checklist