Skip to content

Source Generator Improvement Proposal#31

Open
Seeker1437 wants to merge 2 commits intoTheEightBot:developfrom
Seeker1437:feature/source-generator
Open

Source Generator Improvement Proposal#31
Seeker1437 wants to merge 2 commits intoTheEightBot:developfrom
Seeker1437:feature/source-generator

Conversation

@Seeker1437
Copy link
Copy Markdown
Contributor

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

[ServiceRegistration(Lifetime.Singleton)]
public class MyService { }

Register as interface, scoped

[ServiceRegistration(Lifetime.Scoped, registerInterfaces: true)]
public class MyService : IMyService { }

Register explicit interface, scoped

[ServiceRegistration(Lifetime.Scoped, ServiceType = typeof(IMyService))]
public class MyService : IMyService { }

Register open generic repository as IRepository, scoped

[ServiceRegistration(Lifetime.Scoped, ServiceType = typeof(IRepository<>))]
public class Repository<T> : IRepository<T> { }

Register keyed service (Vip) as IDiscountService

[ServiceRegistration(
    Lifetime.Singleton,
    ServiceType = typeof(IDiscountService),
    Key = "Vip")]
public class VipDiscountService : IDiscountService { }

Register the same implementation under multiple keys

[ServiceRegistration(Lifetime.Singleton, ServiceType = typeof(IDiscountService), Key = "Basic")]
[ServiceRegistration(Lifetime.Singleton, ServiceType = typeof(IDiscountService), Key = "Premium")]
public class DiscountService : IDiscountService { }

Contains changes I meant to PR before parting ways.

Add support for explicit service types, open generics, and keyed service registration
@Seeker1437 Seeker1437 changed the base branch from main to develop December 28, 2025 17:35
@michaelstonis michaelstonis requested a review from Copilot March 28, 2026 03:42
Copy link
Copy Markdown

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

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 ServiceRegistrationGenerator from ISourceGenerator to IIncrementalGenerator.
  • Added generation support for explicit ServiceType, open generics, and keyed registrations (Key).
  • Extended ServiceRegistrationAttribute with ServiceType and Key metadata.

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.

Comment on lines +266 to 280
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('>');
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copilot uses AI. Check for mistakes.
Comment on lines +16 to 21
public ServiceRegistrationAttribute(
Lifetime serviceRegistrationType = Lifetime.Transient,
bool registerInterfaces = false,
Type? serviceType = null,
string? key = null)
{
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copilot uses AI. Check for mistakes.
Comment on lines 10 to +25
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;
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
foreach (var iface in reg.ClassSymbol.Interfaces)
registerInterfaces = ri;
}

Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Comment on lines +231 to +233
return value
.Replace("\\", "\\\\")
.Replace("\"", "\\\"");
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants