|
| 1 | +# Source Generation Support |
| 2 | + |
| 3 | +Scrutor includes an optional source generator that performs assembly scanning at compile time instead of runtime. This enables AOT (Ahead-of-Time) compilation compatibility and improves startup performance by eliminating runtime reflection. |
| 4 | + |
| 5 | +## How It Works |
| 6 | + |
| 7 | +When enabled, the source generator analyzes your `Scan()` method chains and `[ServiceDescriptor]` attributes at compile time. It then generates **interceptor methods** that replace the runtime `Scan()` calls with pre-computed service registrations. |
| 8 | + |
| 9 | +This means your existing code continues to work unchanged - the generator transparently intercepts the `Scan()` calls and substitutes optimized, statically-generated registrations. |
| 10 | + |
| 11 | +## Enabling Source Generation |
| 12 | + |
| 13 | +Add the following property to your project file: |
| 14 | + |
| 15 | +```xml |
| 16 | +<PropertyGroup> |
| 17 | + <ScrutorGenerateServices>true</ScrutorGenerateServices> |
| 18 | +</PropertyGroup> |
| 19 | +``` |
| 20 | + |
| 21 | +That's it. No code changes are required - your existing `Scan()` calls are automatically optimized at compile time. |
| 22 | + |
| 23 | +## Usage Patterns |
| 24 | + |
| 25 | +### Pattern 1: Scan() Method Chains |
| 26 | + |
| 27 | +The most common pattern - your existing Scan() calls are intercepted automatically: |
| 28 | + |
| 29 | +```csharp |
| 30 | +services.Scan(scan => scan |
| 31 | + .FromAssemblyOf<IMyService>() |
| 32 | + .AddClasses(c => c.AssignableTo<IMyService>()) |
| 33 | + .AsImplementedInterfaces() |
| 34 | + .WithScopedLifetime()); |
| 35 | +``` |
| 36 | + |
| 37 | +At compile time, this is replaced with direct registration calls like: |
| 38 | + |
| 39 | +```csharp |
| 40 | +services.AddScoped<IMyService, MyServiceImpl>(); |
| 41 | +services.AddScoped<IMyService, AnotherServiceImpl>(); |
| 42 | +// ... all discovered types |
| 43 | +``` |
| 44 | + |
| 45 | +### Pattern 2: ServiceDescriptor Attributes |
| 46 | + |
| 47 | +Mark classes with `[ServiceDescriptor]` attributes and use `UsingAttributes()`: |
| 48 | + |
| 49 | +```csharp |
| 50 | +[ServiceDescriptor(typeof(IEmailService), ServiceLifetime.Scoped)] |
| 51 | +public class EmailService : IEmailService { } |
| 52 | + |
| 53 | +// Generic version |
| 54 | +[ServiceDescriptor<INotificationService>(ServiceLifetime.Singleton)] |
| 55 | +public class NotificationService : INotificationService { } |
| 56 | + |
| 57 | +// With service key |
| 58 | +[ServiceDescriptor<ICache>(ServiceLifetime.Singleton, "redis")] |
| 59 | +public class RedisCache : ICache { } |
| 60 | +``` |
| 61 | + |
| 62 | +Then scan with attribute-based registration: |
| 63 | + |
| 64 | +```csharp |
| 65 | +services.Scan(scan => scan |
| 66 | + .FromAssemblyOf<EmailService>() |
| 67 | + .AddClasses() |
| 68 | + .UsingAttributes()); |
| 69 | +``` |
| 70 | + |
| 71 | +### Pattern 3: Keyed Services |
| 72 | + |
| 73 | +Keyed service registration is fully supported: |
| 74 | + |
| 75 | +```csharp |
| 76 | +services.Scan(scan => scan |
| 77 | + .FromAssemblyOf<IHandler>() |
| 78 | + .AddClasses(c => c.AssignableTo<IHandler>()) |
| 79 | + .AsImplementedInterfaces() |
| 80 | + .WithSingletonLifetime() |
| 81 | + .WithServiceKey("handlers")); |
| 82 | +``` |
| 83 | + |
| 84 | +Generates: |
| 85 | + |
| 86 | +```csharp |
| 87 | +services.AddKeyedSingleton<IHandler, MyHandler>("handlers"); |
| 88 | +``` |
| 89 | + |
| 90 | +## Supported Methods |
| 91 | + |
| 92 | +### Assembly Sources |
| 93 | + |
| 94 | +| Method | Support | |
| 95 | +|--------|---------| |
| 96 | +| `FromAssemblyOf<T>()` | Full | |
| 97 | +| `FromAssemblies(...)` | Full | |
| 98 | + |
| 99 | +### Type Filtering |
| 100 | + |
| 101 | +| Method | Support | |
| 102 | +|--------|---------| |
| 103 | +| `AddClasses()` | Full | |
| 104 | +| `AddClasses(publicOnly: false)` | Full | |
| 105 | +| `AssignableTo<T>()` | Full | |
| 106 | +| `AssignableTo(typeof(T))` | Full | |
| 107 | +| `InNamespace(string)` | Full | |
| 108 | +| `InNamespaces(params string[])` | Full | |
| 109 | +| `InExactNamespace(string)` | Full | |
| 110 | +| `InExactNamespaces(params string[])` | Full | |
| 111 | +| `WithAttribute<T>()` | Full | |
| 112 | +| `WithoutAttribute<T>()` | Full | |
| 113 | + |
| 114 | +### Service Mapping |
| 115 | + |
| 116 | +| Method | Support | |
| 117 | +|--------|---------| |
| 118 | +| `AsImplementedInterfaces()` | Full | |
| 119 | +| `AsSelf()` | Full | |
| 120 | +| `As<T>()` | Full | |
| 121 | +| `AsMatchingInterface()` | Full | |
| 122 | +| `AsSelfWithInterfaces()` | Full | |
| 123 | + |
| 124 | +### Lifetime |
| 125 | + |
| 126 | +| Method | Support | |
| 127 | +|--------|---------| |
| 128 | +| `WithSingletonLifetime()` | Full | |
| 129 | +| `WithScopedLifetime()` | Full | |
| 130 | +| `WithTransientLifetime()` | Full | |
| 131 | +| `WithLifetime(ServiceLifetime)` | Full | |
| 132 | + |
| 133 | +### Registration Strategy |
| 134 | + |
| 135 | +| Method | Support | |
| 136 | +|--------|---------| |
| 137 | +| `UsingRegistrationStrategy(RegistrationStrategy.Skip)` | Full | |
| 138 | +| `UsingRegistrationStrategy(RegistrationStrategy.Append)` | Full | |
| 139 | +| `UsingRegistrationStrategy(RegistrationStrategy.Replace)` | Full | |
| 140 | +| `UsingRegistrationStrategy(RegistrationStrategy.Throw)` | Full | |
| 141 | + |
| 142 | +### Other |
| 143 | + |
| 144 | +| Method | Support | |
| 145 | +|--------|---------| |
| 146 | +| `WithServiceKey(object)` | Full (literal values) | |
| 147 | +| `UsingAttributes()` | Full | |
| 148 | + |
| 149 | +## Unsupported Patterns |
| 150 | + |
| 151 | +The following patterns require runtime evaluation and will **fall back to runtime scanning** (with a diagnostic warning): |
| 152 | + |
| 153 | +| Pattern | Reason | |
| 154 | +|---------|--------| |
| 155 | +| `FromCallingAssembly()` | Dynamic assembly resolution | |
| 156 | +| `FromExecutingAssembly()` | Dynamic assembly resolution | |
| 157 | +| `FromEntryAssembly()` | Dynamic assembly resolution | |
| 158 | +| `FromApplicationDependencies()` | Dynamic dependency scanning | |
| 159 | +| `Where(predicate)` | Custom predicate evaluation | |
| 160 | +| `AssignableToAny(...)` | Multiple type checks | |
| 161 | +| `WithLifetime(Func<Type, ServiceLifetime>)` | Dynamic lifetime selection | |
| 162 | +| `WithServiceKey(Func<Type, object?>)` | Dynamic key selection | |
| 163 | + |
| 164 | +When an unsupported pattern is detected, the generator emits a `SCRUT001` warning and the original runtime `Scan()` behavior is preserved. |
| 165 | + |
| 166 | +## Diagnostics |
| 167 | + |
| 168 | +| Code | Severity | Description | |
| 169 | +|------|----------|-------------| |
| 170 | +| SCRUT001 | Warning | Unsupported pattern in Scan() configuration - falls back to runtime | |
| 171 | +| SCRUT002 | Error | Invalid ServiceDescriptor configuration | |
| 172 | +| SCRUT003 | Error | Type mismatch in service registration | |
| 173 | +| SCRUT004 | Warning | Duplicate service registration detected | |
| 174 | +| SCRUT005 | Info | Source generation information | |
| 175 | + |
| 176 | +## Generated Code |
| 177 | + |
| 178 | +The generator creates an interceptor file (visible in your IDE under Dependencies > Analyzers > Scrutor.Generators). Example output: |
| 179 | + |
| 180 | +```csharp |
| 181 | +// <auto-generated/> |
| 182 | +#nullable enable |
| 183 | + |
| 184 | +namespace Scrutor.Generated |
| 185 | +{ |
| 186 | + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Scrutor.Generators", "1.0.0")] |
| 187 | + file static class MyProjectScanInterceptors |
| 188 | + { |
| 189 | + /// <summary> |
| 190 | + /// Intercepts Scan() call at Program.cs:15 |
| 191 | + /// </summary> |
| 192 | + [global::System.Runtime.CompilerServices.InterceptsLocation("Program.cs", 15, 10)] |
| 193 | + public static IServiceCollection Scan_0( |
| 194 | + this IServiceCollection services, |
| 195 | + Action<ITypeSourceSelector> action) |
| 196 | + { |
| 197 | + services.AddScoped<IMyService, MyServiceImpl>(); |
| 198 | + services.AddScoped<IMyService, AnotherServiceImpl>(); |
| 199 | + return services; |
| 200 | + } |
| 201 | + } |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +## Benefits |
| 206 | + |
| 207 | +1. **AOT Compatibility**: Works with Native AOT compilation in .NET 8+ |
| 208 | +2. **Faster Startup**: No runtime reflection or assembly scanning |
| 209 | +3. **Trimming Safe**: No dynamic type loading that could be trimmed away |
| 210 | +4. **Zero Code Changes**: Existing Scan() calls work transparently |
| 211 | +5. **Fallback Support**: Unsupported patterns gracefully fall back to runtime |
| 212 | + |
| 213 | +## Requirements |
| 214 | + |
| 215 | +- .NET 8.0 or later |
| 216 | +- C# 12 or later (for interceptor support) |
| 217 | +- `ScrutorGenerateServices` property set to `true` |
| 218 | + |
| 219 | +## Troubleshooting |
| 220 | + |
| 221 | +### Generated code not appearing |
| 222 | + |
| 223 | +1. Ensure `<ScrutorGenerateServices>true</ScrutorGenerateServices>` is in your project file |
| 224 | +2. Rebuild the project (source generators run on build) |
| 225 | +3. Check for analyzer errors in the Error List |
| 226 | + |
| 227 | +### Scan() not being intercepted |
| 228 | + |
| 229 | +1. Verify the pattern is supported (see tables above) |
| 230 | +2. Check for SCRUT001 warnings indicating fallback to runtime |
| 231 | +3. Ensure the assembly source uses `FromAssemblyOf<T>()` with a concrete type |
| 232 | + |
| 233 | +### Build errors after enabling |
| 234 | + |
| 235 | +1. Check SCRUT002/SCRUT003 errors for invalid configurations |
| 236 | +2. Verify service types are accessible from the generated code |
| 237 | +3. Ensure all referenced types are in the compilation |
0 commit comments