diff --git a/readme.md b/readme.md index d77bed5..abecb0f 100644 --- a/readme.md +++ b/readme.md @@ -105,6 +105,75 @@ It doesn't support anything, but it should 2. applying the attribute somewhere in the inheritance hierarchy 3. applying the attribute multiple times in inheritance hierarchy (it will use the most derived). +## How the source generator stitches things together +- The generator scans for `[ResiliencePipeline]` on interface or class methods and validates they are awaitable (`ValueTask`/`ValueTask`/`Task`/`Task` and not async enumerables). +- It also inspects DI registrations that use the generic `ServiceCollectionServiceExtensions` helpers (`AddTransient`, `AddScoped`, `AddSingleton`, keyed variants, etc.). +- For interface registrations, it emits a new wrapper type per `(implementation, interface)` pair and maps the interface to that generated type. +- For concrete-only registrations it derives from the concrete type (requires the class and method to be non-sealed and virtual). +- It then emits tiny interceptor methods that the compiler wires up (via `InterceptsLocation`) when you call `RegisterKinetic2()`, so your existing DI registrations stay untouched while the generator swaps in the wrapped type. + +## What the generated class looks like +After turning on `true` you will find code similar to the following under `obj/.../generated/Kinetic2.Analyzers/`: + +```csharp +[System.CodeDom.Compiler.GeneratedCode("Kinetic2", "1.0.0")] +internal sealed class NotificationService__INotificationService__K2p : INotificationService +{ + private readonly IServiceProvider _serviceProvider; + private readonly INotificationService _instance; + + internal NotificationService__INotificationService__K2p( + IServiceProvider serviceProvider, + NotificationService instance) + { + _serviceProvider = serviceProvider; + _instance = (INotificationService)instance; + } + + public ValueTask NotifyAsync(string to, string subject, string body) => + ResilienceExtensions.ExecuteResiliencePipeline( + _serviceProvider, + "NotifyAsyncFromInterface", + _ => _instance.NotifyAsync(to, subject, body), + CancellationToken.None); +} +``` + +The wrapper keeps the original implementation instance, resolves the named `ResiliencePipeline` from DI for each call, and executes the original method through that pipeline without changing your calling code. + +## Injection / override mechanism +The generator also emits helper methods that re-run your DI registration using the generated wrapper type. Conceptually, when it sees `AddTransient()` it produces an interceptor like this: + +```csharp +[GeneratedCode("Kinetic2", "1.0.0")] +internal static class K2p_0 +{ + internal static void AddTransient( + this IServiceCollection services, + Func? factory = default) + where TService : INotificationService + { + if (factory is not null) + { + Func wrapped = + sp => new NotificationService__INotificationService__K2p(sp, factory(sp)); + ServiceCollectionServiceExtensions.AddTransient(services, wrapped); + return; + } + + var key = "generated-guid"; + ServiceCollectionServiceExtensions.AddKeyedTransient(services, key); + ServiceCollectionServiceExtensions.AddTransient( + services, + sp => new NotificationService__INotificationService__K2p( + sp, + sp.GetKeyedService(key)!)); + } +} +``` + +Because the generator uses interceptors, the signature of your registration call does not change; the call site is rewritten to the generated helper which first preserves your original factory (or registers the original implementation under a keyed service) and then registers the generated wrapper as the implementation for the requested service/interface. + ## Generated source code In your .csproj, add