Source Generator Improvement Proposal#31
Source Generator Improvement Proposal#31Seeker1437 wants to merge 2 commits intoTheEightBot:developfrom
Conversation
Contains changes I meant to PR before parting ways. Add support for explicit service types, open generics, and keyed service registration
There was a problem hiding this comment.
Pull request overview
Improves the service-registration source generator to support explicit service types, open generic registrations, and keyed service registration, while migrating the generator to the incremental generator API.
Changes:
- Migrated
ServiceRegistrationGeneratorfromISourceGeneratortoIIncrementalGenerator. - Added generation support for explicit
ServiceType, open generics, and keyed registrations (Key). - Extended
ServiceRegistrationAttributewithServiceTypeandKeymetadata.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| Stellar.SourceGenerators/ServiceRegistrationGenerator.cs | Incremental generator implementation + new registration emission logic for explicit/open-generic/keyed cases. |
| Stellar.SourceGenerators/ServiceRegistrationAttribute.cs | Adds ServiceType/Key to the attribute contract (used by the generator). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private static void AppendTypeName(StringBuilder sb, INamedTypeSymbol typeSymbol) | ||
| { | ||
| sb.Append(typeSymbol.Name); | ||
|
|
||
| if (typeSymbol.TypeParameters.Length > 0) | ||
| { | ||
| classSymbol = this.ClassSymbol; | ||
| lifetimeValue = this.LifetimeValue; | ||
| registerInterfaces = this.RegisterInterfaces; | ||
| sb.Append('<'); | ||
|
|
||
| if (typeSymbol.TypeParameters.Length > 1) | ||
| { | ||
| sb.Append(',', typeSymbol.TypeParameters.Length - 1); | ||
| } | ||
|
|
||
| sb.Append('>'); | ||
| } |
There was a problem hiding this comment.
GetTypeSyntax/AppendTypeName always renders generic types as unbound (e.g., List<> / IHandler<,>) by only looking at TypeParameters. This breaks registrations for closed constructed generic types (e.g., an interface like IHandler would be emitted as typeof(IHandler<>) which changes behavior and may fail at runtime). Update the formatting to include concrete TypeArguments when they aren’t type parameters, and only fall back to the unbound-generic form when any type argument is a type parameter (or when IsUnboundGenericType is true).
| public ServiceRegistrationAttribute( | ||
| Lifetime serviceRegistrationType = Lifetime.Transient, | ||
| bool registerInterfaces = false, | ||
| Type? serviceType = null, | ||
| string? key = null) | ||
| { |
There was a problem hiding this comment.
The PR description shows applying multiple [ServiceRegistration] attributes to the same class (multiple keys), but this attribute does not set AllowMultiple=true, so consumers will get a compile-time error when stacking the attribute. Update the AttributeUsage to allow multiple (and ensure the runtime copy of this attribute in Stellar/ServiceRegistrationAttribute.cs is updated the same way).
| public bool RegisterInterfaces { get; set; } | ||
|
|
||
| public Type? ServiceType { get; set; } | ||
|
|
||
| public string? Key { get; set; } | ||
|
|
||
| public ServiceRegistrationAttribute() | ||
| { | ||
| ServiceRegistrationType = Lifetime.Transient; | ||
| } | ||
|
|
||
| public ServiceRegistrationAttribute(Lifetime serviceRegistrationType) | ||
| { | ||
| ServiceRegistrationType = serviceRegistrationType; | ||
| } | ||
|
|
||
| public ServiceRegistrationAttribute(Lifetime serviceRegistrationType = Lifetime.Transient, bool registerInterfaces = false) | ||
| public ServiceRegistrationAttribute( | ||
| Lifetime serviceRegistrationType = Lifetime.Transient, | ||
| bool registerInterfaces = false, | ||
| Type? serviceType = null, | ||
| string? key = null) | ||
| { | ||
| ServiceRegistrationType = serviceRegistrationType; | ||
| RegisterInterfaces = registerInterfaces; | ||
| ServiceType = serviceType; | ||
| Key = key; |
There was a problem hiding this comment.
This project defines ServiceRegistrationAttribute here, but consuming projects reference Stellar.SourceGenerators as an analyzer-only reference (e.g., Stellar.MauiSample.csproj sets ReferenceOutputAssembly="false"), meaning the attribute type used in user code comes from the runtime assembly (Stellar/ServiceRegistrationAttribute.cs). That runtime attribute currently lacks ServiceType/Key and the updated ctor signature, so the new usage examples in the PR description won’t compile until the runtime attribute is updated to match these new members.
| foreach (var iface in reg.ClassSymbol.Interfaces) | ||
| registerInterfaces = ri; | ||
| } | ||
|
|
There was a problem hiding this comment.
GetRegistrationsForClass only reads the first two constructor arguments (lifetime/registerInterfaces). Since ServiceRegistrationAttribute’s constructor now also accepts serviceType and key positionally, using positional args (e.g., [ServiceRegistration(Lifetime.Scoped, false, typeof(IMyService), "Vip")]) will be silently ignored by the generator. Parse constructor arguments 3 and 4 as well (TypedConstantKind.Type / string) to keep positional and named usage consistent.
| if (ad.ConstructorArguments.Length >= 3) | |
| { | |
| TypedConstant serviceTypeArg = ad.ConstructorArguments[2]; | |
| if (serviceTypeArg.Kind == TypedConstantKind.Type && | |
| serviceTypeArg.Value is ITypeSymbol typeSymbol) | |
| { | |
| explicitServiceType = typeSymbol as INamedTypeSymbol; | |
| } | |
| } | |
| if (ad.ConstructorArguments.Length >= 4 && | |
| ad.ConstructorArguments[3].Value is string positionalKey) | |
| { | |
| key = positionalKey; | |
| } |
| return value | ||
| .Replace("\\", "\\\\") | ||
| .Replace("\"", "\\\""); |
There was a problem hiding this comment.
EscapeStringLiteral only escapes backslashes and quotes; keys containing newlines, tabs, or other control characters will produce invalid generated C# string literals. Prefer using SymbolDisplay.FormatLiteral(value, quote:true) (or equivalent) to emit a correct C# string literal for any input.
| return value | |
| .Replace("\\", "\\\\") | |
| .Replace("\"", "\\\""); | |
| // Use Roslyn's SymbolDisplay to correctly escape all characters for C# string literals | |
| // while returning the content without surrounding quotes to preserve existing behavior. | |
| return SymbolDisplay.FormatLiteral(value, quote: false); |
…generic types - Update ServiceRegistrationAttribute to support positional parameters for ServiceType and Key. - Enhance the source generator to correctly resolve and output syntax for unbound generics and nested type arguments. - Use SyntaxFactory to handle string literal escaping for keyed services.
Contains changes I meant to PR before parting ways.
Add support for explicit service types, open generics, and keyed service registration
Register as self, singleton
Register as interface, scoped
Register explicit interface, scoped
Register open generic repository as IRepository, scoped
Register keyed service (Vip) as IDiscountService
Register the same implementation under multiple keys