Skip to content

Commit 120a785

Browse files
committed
Add some docs
1 parent cbd1c26 commit 120a785

2 files changed

Lines changed: 249 additions & 0 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ The library adds two extension methods to `IServiceCollection`:
3030

3131
See **Examples** below for usage examples.
3232

33+
### Source Generation (AOT Support)
34+
35+
Scrutor includes an optional source generator for AOT compatibility and improved startup performance. Enable it in your project file:
36+
37+
```xml
38+
<PropertyGroup>
39+
<ScrutorGenerateServices>true</ScrutorGenerateServices>
40+
</PropertyGroup>
41+
```
42+
43+
When enabled, your `Scan()` calls are analyzed at compile time and replaced with static registrations - no code changes required. See [Source Generation Documentation](docs/source-generation.md) for details.
44+
3345
## Examples
3446

3547
### Scanning

docs/source-generation.md

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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

Comments
 (0)