diff --git a/agents/dotnet-aot-optimizer.agent.md b/agents/dotnet-aot-optimizer.agent.md new file mode 100644 index 0000000000..021dee64e5 --- /dev/null +++ b/agents/dotnet-aot-optimizer.agent.md @@ -0,0 +1,52 @@ +--- +description: "Use this agent when the user wants to make .NET code compatible with Native AOT, fix trimming or AOT warnings, or evaluate library compatibility for AOT deployment.\n\nTrigger phrases include:\n- 'I'm getting IL2026 / IL3050 / IL2070 warnings'\n- 'How do I make this work with PublishAot?'\n- 'Is this library compatible with Native AOT?'\n- 'Help me fix these trimming warnings'\n- 'I need to migrate from Newtonsoft.Json for AOT'\n- 'How do I annotate this reflection code for trimming?'\n- 'My app crashes after publishing with Native AOT'\n- 'How do I set up my project for AOT?'\n\nExamples:\n- User says 'I get IL3050 when calling MakeGenericType' → invoke this agent to diagnose generic instantiation safety and provide AOT-compatible alternatives\n- User shares code using JsonConvert and asks about AOT → invoke this agent to provide System.Text.Json source generation migration\n- User asks 'Can I use EF Core with Native AOT?' → invoke this agent to explain experimental compiled models and precompiled queries\n- User gets IL2026 on Activator.CreateInstance → invoke this agent to walk through DynamicallyAccessedMembers annotation workflow\n- User asks how to set up CI for AOT validation → invoke this agent to provide test app pattern and CI workflow" +name: dotnet-aot-optimizer +tools: ['shell', 'read', 'search', 'edit', 'task', 'skill', 'web_search', 'web_fetch', 'ask_user'] +--- + +# dotnet-aot-optimizer instructions + +You are a .NET Native AOT compatibility expert with deep knowledge of the trimmer, ILC, static analysis warnings, and annotation attributes (`DynamicallyAccessedMembers`, `RequiresUnreferencedCode`, `RequiresDynamicCode`). + +## Mission + +Help developers achieve **zero-warning Native AOT publishes** by diagnosing warnings, providing concrete code fixes, and guiding migration from reflection-heavy patterns to source generators. + +## Methodology + +1. **Start with warnings**: AOT warnings are the source of truth — zero warnings = guaranteed correctness +2. **Identify the root pattern**: Reflection, dynamic code gen, assembly loading, or library dependency? +3. **Choose the right fix**: Eliminate reflection → Annotate with `[DynamicallyAccessedMembers]` → Mark with `[RequiresUnreferencedCode]` → Suppress with `[UnconditionalSuppressMessage]` (last resort) +4. **Show concrete code**: ❌ before → ✅ after, with .NET version requirements +5. **Verify**: Guide developer to run `dotnet publish -r ` to confirm fix + +## Key Principles + +- **Warnings are not suggestions** — each represents a potential runtime failure +- **`[UnconditionalSuppressMessage]` is a promise** — breaking it breaks downstream consumers +- **Never recommend leaving .NET** — all solutions stay within the .NET ecosystem +- **Annotate bottom-up** — fix dependencies first, annotations propagate upward +- **Use both analyzers** — Roslyn for fast feedback, ILC publish for completeness + +## Skills + +Use these skills for progressive disclosure of deep technical knowledge: + +- **diagnosing-dotnet-aot**: Load first for any AOT compatibility review. Detects 13 critical patterns that cause hard failures (Reflection.Emit, dynamic assembly loading, reflection-based serialization, MakeGenericType issues), then loads topic-specific references based on detected code signals. Uses tiered severity (🔴 Critical / 🟡 Warning / ℹ️ Info) with progressive disclosure. + +## When to Ask for Clarification + +- App vs library? (different annotation strategies) +- Target .NET version? (affects available source generators) +- Hot path? (affects Expression.Compile severity) + +## Escalation + +Acknowledge honestly when: +- Architecture fundamentally requires dynamic loading (plugin systems) +- A library has no AOT-compatible .NET alternative +- EF Core AOT support is too experimental for the user's scenario + +## Tone + +Be precise about warning codes, concrete in fixes (exact code, not vague guidance), honest about limitations, and version-aware. diff --git a/skills/diagnosing-dotnet-aot/SKILL.md b/skills/diagnosing-dotnet-aot/SKILL.md new file mode 100644 index 0000000000..c6bcde5e0e --- /dev/null +++ b/skills/diagnosing-dotnet-aot/SKILL.md @@ -0,0 +1,131 @@ +--- +name: diagnosing-dotnet-aot +description: >- + Diagnoses .NET Native AOT and trimming compatibility issues and provides + concrete fixes. Activates when preparing an application or library for + PublishAot, fixing IL warnings (IL2026, IL3050, IL2070), migrating reflection + to source generators, or evaluating library AOT compatibility. +--- + +# Diagnosing .NET AOT Issues + +Scan C# code for Native AOT and trimming incompatibilities and produce prioritized fixes. Goal: **zero-warning AOT publish**. + +## Why This Skill Exists + +LLMs already know general AOT concepts. This skill adds knowledge that base models **consistently get wrong**: + +| What Claude Gets Wrong Without This Skill | Correct Answer | +|------------------------------------------|----------------| +| Suggests `#pragma warning disable` for trim warnings | `#pragma` is **not preserved in IL** — trimmer ignores it. Must use `[UnconditionalSuppressMessage]` | +| Flags ALL `MakeGenericType` as dangerous | Safe for **reference types** (shared canonical code). Only value types need pre-generated code | +| Misses `Expression.Compile()` perf cliff | Falls back to **10-100x slower interpreter** in AOT — **no warning emitted** | +| Suggests fixing `EventSource.WriteEvent` IL2026 | False positive for >3 params with **primitive types** — safe to suppress | +| Doesn't know `IsAotCompatible=true` cascades | Automatically enables `IsTrimmable` + `EnableTrimAnalyzer` + `EnableSingleFileAnalyzer` + `EnableAotAnalyzer` | +| Gives incomplete annotation workflow | Must propagate `[DynamicallyAccessedMembers]` through **entire call chain** — leaf → caller → caller → call site | +| Inconsistent on EF Core AOT status | Experimental: requires compiled models + precompiled queries, not production-ready | + +## When to Use + +- Preparing an app for `true` or a library for `true` +- Diagnosing IL trimming or AOT warnings (IL2026, IL2070, IL3050, IL3058, IL2104) +- Migrating reflection-heavy code to source generators +- Evaluating dependency AOT compatibility +- Reviewing PRs touching reflection, serialization, DI, or generic type construction + +## When Not to Use + +- **Runtime performance tuning** — use `dotnet-performance-patterns` +- **Projects that will never use AOT** +- **WPF or Windows Forms** — not AOT-compatible + +## Inputs + +| Input | Required | Description | +|-------|----------|-------------| +| Source code | Yes | C# files, project files, or repo paths | +| Target framework | Recommended | .NET version (affects available source generators) | +| Project type | Recommended | App vs library (different annotation strategy) | +| Scan depth | Optional | `critical-only`, `standard` (default), `comprehensive` | + +## Workflow + +### Step 1: Load Critical Patterns + +Always load [critical-patterns.md](references/critical-patterns.md) — hard failures that **will** crash or corrupt data in AOT. + +### Step 2: Detect Code Signals and Load Topic References + +Scan for signals and load only the relevant reference files: + +| Signal in Code | Load Reference | +|----------------|----------------| +| `JsonSerializer`, `Newtonsoft`, `JsonConvert`, `IConfiguration`, `IOptions<`, `Bind(`, `[JsonSerializable` | [serialization-and-config.md](references/serialization-and-config.md) | +| `Type.GetType`, `GetMethod(`, `Activator.CreateInstance`, `Assembly.Load`, `[DynamicallyAccessedMembers`, `IServiceCollection`, DI registration | [reflection-and-di.md](references/reflection-and-di.md) | +| `MakeGenericType`, `MakeGenericMethod`, `Expression.`, `Linq.Expressions`, struct generics | [generics-and-types.md](references/generics-and-types.md) | +| `` | +| Marking a library `IsAotCompatible` without testing | Set up an AOT test app that publishes and exercises all APIs | +| Expecting EF Core to fully work with AOT | EF Core AOT support is experimental — use compiled models + precompiled queries and test thoroughly | + +## Further Reading + +- [Native AOT Deployment](https://learn.microsoft.com/dotnet/core/deploying/native-aot/) +- [Fixing AOT Warnings](https://learn.microsoft.com/dotnet/core/deploying/native-aot/fixing-warnings) +- [Fixing Trim Warnings](https://learn.microsoft.com/dotnet/core/deploying/trimming/fixing-warnings) +- [Making Libraries AOT-Compatible](https://devblogs.microsoft.com/dotnet/creating-aot-compatible-libraries/) +- [Trimming Incompatibilities](https://learn.microsoft.com/dotnet/core/deploying/trimming/incompatibilities) diff --git a/skills/diagnosing-dotnet-aot/references/critical-patterns.md b/skills/diagnosing-dotnet-aot/references/critical-patterns.md new file mode 100644 index 0000000000..4303edd53a --- /dev/null +++ b/skills/diagnosing-dotnet-aot/references/critical-patterns.md @@ -0,0 +1,277 @@ +# Critical .NET AOT Incompatibilities + +Patterns that **will** crash, throw, or produce corrupt data in a Native AOT application. + +## Contents +- [Dynamic Code Generation](#dynamic-code-generation) — Reflection.Emit, dynamic loading, `dynamic` keyword +- [Reflection](#reflection) — Type.GetType, static constructors +- [Serialization](#serialization) — JSON, BinaryFormatter +- [Expressions](#expressions) — Expression.Compile +- [Configuration](#configuration) — Config binding +- [Regex](#regex) — Compiled regex +- [COM and Platform](#com-and-platform) — COM, C++/CLI + +## Dynamic Code Generation + +### 1. System.Reflection.Emit is Unavailable +🔴 **AVOID** | .NET 7+ + +`System.Reflection.Emit` requires a JIT compiler. Native AOT has no JIT — all `Emit` calls throw `PlatformNotSupportedException`. + +❌ +```csharp +var method = new DynamicMethod("Add", typeof(int), new[] { typeof(int), typeof(int) }); +var il = method.GetILGenerator(); +il.Emit(OpCodes.Ldarg_0); +// PlatformNotSupportedException in AOT +``` +✅ +```csharp +// Use a static method or source generator instead +static int Add(int a, int b) => a + b; + +// If dynamic dispatch is truly needed, use RuntimeFeature check: +if (RuntimeFeature.IsDynamicCodeSupported) + UseDynamicMethod(); +else + UseFallback(); +``` +**Impact: PlatformNotSupportedException at runtime. Warning IL3050.** + +### 2. Dynamic Assembly Loading is Unsupported +🔴 **AVOID** | .NET 7+ + +`Assembly.LoadFrom`, `Assembly.LoadFile`, and `Assembly.Load(byte[])` cannot load new assemblies at runtime in AOT — all code must be present at compile time. + +❌ +```csharp +var asm = Assembly.LoadFrom("/plugins/MyPlugin.dll"); // fails in AOT +var type = asm.GetType("MyPlugin.Handler"); +``` +✅ +```csharp +// Register plugins at compile time via direct references +// or use a compiled plugin manifest +[RequiresUnreferencedCode("Plugin loading is not compatible with AOT")] +void LoadPlugin(string path) { /* ... */ } +``` +**Impact: FileNotFoundException or PlatformNotSupportedException. Warning IL2026.** + +### 3. The `dynamic` Keyword Uses DLR (Runtime Code Gen) +🔴 **AVOID** | .NET 7+ + +The `dynamic` keyword relies on the Dynamic Language Runtime which emits IL at runtime. This fails in AOT. + +❌ +```csharp +dynamic obj = GetResponse(); +string name = obj.Name; // DLR tries to emit call site — fails in AOT +``` +✅ +```csharp +var obj = GetResponse(); +string name = ((JsonElement)obj).GetProperty("Name").GetString(); +// or deserialize to a strongly-typed class +``` +**Impact: RuntimeBinderException or PlatformNotSupportedException. Warning IL3050.** + +## Reflection + +### 4. Type.GetType with Runtime-Determined Strings +🔴 **AVOID** | .NET 7+ + +When the type name comes from config, user input, or external data, the trimmer cannot know which types to preserve. + +❌ +```csharp +string typeName = config["HandlerType"]; // runtime value +Type t = Type.GetType(typeName); // trimmer can't see this +var handler = Activator.CreateInstance(t); // may be trimmed away +``` +✅ +```csharp +// Use a factory with statically known types +Type t = handlerName switch +{ + "OrderHandler" => typeof(OrderHandler), + "PaymentHandler" => typeof(PaymentHandler), + _ => throw new NotSupportedException($"Unknown handler: {handlerName}") +}; +var handler = Activator.CreateInstance(t); +``` +**Impact: TypeLoadException or MissingMethodException. Warning IL2026.** + +### 5. Unbounded Reflection in Static Constructors +🔴 **AVOID** | .NET 7+ + +Reflection in static constructors propagates `[RequiresUnreferencedCode]` warnings to **every member** of the class — making the entire class unusable in trimmed code. + +❌ +```csharp +class MyService +{ + static readonly PropertyInfo[] Props; + static MyService() + { + Props = typeof(MyService).GetProperties(); // warning on ALL members + } +} +``` +✅ +```csharp +class MyService +{ + // Move reflection to a dedicated method with proper annotation + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + private static Type SelfType => typeof(MyService); + + static PropertyInfo[] GetProps() => SelfType.GetProperties(); +} +``` +**Impact: Cascading warnings across all class members. Warning IL2026/IL2070.** + +## Serialization + +### 6. Reflection-Based JSON Serialization +🔴 **DO** use source generation | .NET 6+ + +`JsonSerializer.Serialize(obj)` without a `JsonTypeInfo` or `JsonSerializerContext` uses reflection to discover properties. This is incompatible with trimming. + +❌ +```csharp +string json = JsonSerializer.Serialize(myObj); // reflection-based +var obj = JsonSerializer.Deserialize(json); // reflection-based +``` +✅ +```csharp +[JsonSerializable(typeof(MyType))] +internal partial class AppJsonContext : JsonSerializerContext { } + +string json = JsonSerializer.Serialize(myObj, AppJsonContext.Default.MyType); +var obj = JsonSerializer.Deserialize(json, AppJsonContext.Default.MyType); +``` +**Impact: MissingMetadataException or incorrect serialization. Warning IL2026 + IL3050.** + +### 7. Newtonsoft.Json is Fundamentally Incompatible +🔴 **AVOID** | .NET 7+ + +Newtonsoft.Json uses deep reflection and is not designed for AOT. It will not be updated for AOT compatibility. Migrate to source-generated `System.Text.Json`. + +❌ +```csharp +var obj = JsonConvert.DeserializeObject(json); // Newtonsoft — not AOT safe +``` +✅ +```csharp +[JsonSerializable(typeof(MyType))] +internal partial class AppJsonContext : JsonSerializerContext { } +var obj = JsonSerializer.Deserialize(json, AppJsonContext.Default.MyType); +``` +**Impact: Missing type metadata, incorrect deserialization, or crashes. Multiple IL2026 warnings.** + +### 8. BinaryFormatter is Removed +🔴 **AVOID** | .NET 7+ (throws by default), .NET 9+ (removed) + +`BinaryFormatter` was disabled by default in .NET 7 and fully removed in .NET 9 due to security and compatibility flaws. + +❌ +```csharp +var formatter = new BinaryFormatter(); +formatter.Serialize(stream, obj); // throws PlatformNotSupportedException +``` +✅ +```csharp +// Use System.Text.Json, protobuf-net, or MessagePack with source generation +``` +**Impact: PlatformNotSupportedException. SYSLIB0011 obsolete warning.** + +## Expressions + +### 9. Expression.Compile() Uses Interpreter in AOT +🔴 **AVOID** on hot paths | .NET 7+ + +`Expression.Compile()` falls back to an interpreter in AOT instead of JIT-compiled delegates. This is 10-100x slower and may cause unexpected behavior for complex expressions. + +❌ +```csharp +Expression> expr = x => x * 2; +var compiled = expr.Compile(); // uses slow interpreter in AOT +int result = compiled(5); +``` +✅ +```csharp +// Use a regular method or static lambda +static int Double(int x) => x * 2; + +// If Expression trees are required (e.g., EF Core queries), they work +// for query translation but avoid Compile() on hot paths +``` +**Impact: 10-100x performance degradation. No warning emitted — silent slowdown.** + +## Configuration + +### 10. Reflection-Based Configuration Binding +🔴 **DO** use source generator | .NET 8+ + +`ConfigurationBinder.Bind()` and `services.Configure()` without the source generator use reflection to set properties. Enable the configuration binding source generator. + +❌ +```csharp +services.Configure(config.GetSection("MySection")); // reflection-based +``` +✅ +```xml + + + true + +``` +```csharp +// Code stays the same — the source generator intercepts the call +services.Configure(config.GetSection("MySection")); +``` +**Impact: MissingMetadataException at runtime. Warning IL2026.** + +## Regex + +### 11. Compiled Regex is Unavailable in AOT +🔴 **DO** use source generation | .NET 7+ + +`RegexOptions.Compiled` requires JIT. In AOT, it silently falls back to interpretation. Use `[GeneratedRegex]` for ahead-of-time compiled patterns. + +❌ +```csharp +var re = new Regex(@"\d+", RegexOptions.Compiled); // falls back to interpreter in AOT +``` +✅ +```csharp +[GeneratedRegex(@"\d+")] +private static partial Regex DigitsRegex(); +``` +**Impact: Silent fallback to interpreted regex — slower but functional. No crash.** + +## COM and Platform + +### 12. Built-in COM Marshalling is Not Supported +🔴 **AVOID** | .NET 7+ + +Automatic COM interop uses runtime code generation. Use `ComWrappers` API instead. + +❌ +```csharp +[ComImport, Guid("...")] +interface IMyComInterface { void DoWork(); } +``` +✅ +```csharp +// Use ComWrappers for AOT-compatible COM interop +// See: https://learn.microsoft.com/dotnet/standard/native-interop/com-wrappers +``` +**Impact: TypeLoadException or MarshalDirectiveException. Warning IL2050.** + +### 13. C++/CLI is Not Supported +🔴 **AVOID** | .NET 7+ + +C++/CLI assemblies cannot be compiled with Native AOT. Use P/Invoke or COM Wrappers for native interop. + +**Impact: Build failure. No AOT compilation possible.** diff --git a/skills/diagnosing-dotnet-aot/references/generics-and-types.md b/skills/diagnosing-dotnet-aot/references/generics-and-types.md new file mode 100644 index 0000000000..e103eb1139 --- /dev/null +++ b/skills/diagnosing-dotnet-aot/references/generics-and-types.md @@ -0,0 +1,210 @@ +# Generic Types and Expressions in AOT + +How generic instantiation, `MakeGenericType`, and `System.Linq.Expressions` behave under Native AOT. + +## Contents +- [Generic Value Type Specialization](#generic-value-type-specialization) — The fundamental rule, MakeGenericType safety, rooting instantiations +- [MakeGenericMethod](#makegenericmethod) — Same rules as MakeGenericType +- [Generic Virtual Methods](#generic-virtual-methods) — Binary size impact +- [System.Linq.Expressions](#systemlinqexpressions) — Expression.Compile limitations, safe vs unsafe usage +- [Nullable Value Types and Generics](#nullable-value-types-and-generics) + +## Generic Value Type Specialization + +### The Fundamental Rule +🔴 **UNDERSTAND** | .NET 7+ + +In Native AOT, **every generic instantiation with a value type (struct) gets its own unique machine code**. Unlike reference types (which share code via canonical forms), value types like `int`, `float`, `double`, and custom structs each require dedicated compiled code. + +This means: +- `List` and `List` produce separate compiled methods +- The AOT compiler must see all value-type instantiations at build time +- Dynamically constructing `GenericType` at runtime will fail if the compiler didn't generate code for it + +### MakeGenericType — When It's Safe + +| Scenario | Safe in AOT? | Why | +|----------|:---:|-----| +| `typeof(List<>).MakeGenericType(typeof(string))` | ✅ | Reference types share code — only one canonical form needed | +| `typeof(List<>).MakeGenericType(typeof(int))` | ⚠️ | Works only if `List` is statically referenced somewhere | +| `typeof(List<>).MakeGenericType(runtimeType)` where `runtimeType` is a class | ✅ | Reference types share code | +| `typeof(List<>).MakeGenericType(runtimeType)` where `runtimeType` could be a struct | 🔴 | May fail — specific struct instantiation not generated | + +### Rooting Generic Value Type Instantiations +🟡 **DO** when using MakeGenericType with value types | .NET 7+ + +If you must use `MakeGenericType` with value types, ensure all needed instantiations are statically referenced: + +❌ +```csharp +// runtimeType might be int, float, or double — compiler may not generate code for all +Type closedType = typeof(Converter<>).MakeGenericType(runtimeType); +var converter = Activator.CreateInstance(closedType); +``` +✅ +```csharp +// Root all needed instantiations so the compiler generates code for them +static void PreserveGenericInstantiations() +{ + _ = typeof(Converter); + _ = typeof(Converter); + _ = typeof(Converter); +} + +// Better: use a static dispatch pattern +IConverter GetConverter(Type t) => t switch +{ + _ when t == typeof(int) => new Converter(), + _ when t == typeof(float) => new Converter(), + _ when t == typeof(double) => new Converter(), + _ => throw new NotSupportedException($"No converter for {t}") +}; +``` + +### Reference Types Are Safe — Use This Knowledge +🟡 **DO** | .NET 7+ + +When you must use `MakeGenericType` at runtime and the type argument is always a reference type (class), the call is safe. The runtime shares one code path for all reference-type instantiations. + +```csharp +// ✅ Safe — only reference types are passed +Type closedType = typeof(Repository<>).MakeGenericType(entityType); +// entityType is always a class (Customer, Order, etc.) + +// ✅ Verify with a runtime check +if (runtimeType.IsValueType) + throw new NotSupportedException("Value types require static registration"); +Type closedType = typeof(Handler<>).MakeGenericType(runtimeType); +``` + +### Bridging Generic Constraints + +Sometimes `MakeGenericType` is used to bridge constraints (e.g., calling `Method` where `T : unmanaged` from code with an unconstrained `T`). This is a known limitation of C# generics. + +**Preferred solution**: Remove the constraint if possible, or use an interface-based approach: + +```csharp +// Instead of: MakeGenericType to bridge constraint +// Use: Interface dispatch +interface IProcessor { void Process(ReadOnlySpan data); } + +class IntProcessor : IProcessor { /* ... */ } +class FloatProcessor : IProcessor { /* ... */ } + +// Register all processors statically +Dictionary processors = new() +{ + [typeof(int)] = new IntProcessor(), + [typeof(float)] = new FloatProcessor(), +}; +``` + +## MakeGenericMethod + +### Same Rules as MakeGenericType +🔴 **UNDERSTAND** | .NET 7+ + +`MethodInfo.MakeGenericMethod(Type[])` has identical constraints — value type arguments need pre-generated code. + +❌ +```csharp +var method = typeof(Utils).GetMethod("Parse")!.MakeGenericMethod(runtimeType); +// If runtimeType is a struct, may fail in AOT +``` +✅ +```csharp +// Use a static dispatch pattern +object Parse(Type t, string input) => t switch +{ + _ when t == typeof(int) => Utils.Parse(input), + _ when t == typeof(DateTime) => Utils.Parse(input), + _ => throw new NotSupportedException() +}; +``` + +## Generic Virtual Methods + +### Binary Size Impact +ℹ️ **UNDERSTAND** | .NET 7+ + +Generic virtual methods (and generic interface methods) in AOT generate specialized code for **every combination** of implementing type × type argument. This can significantly increase binary size. + +```csharp +interface ISerializer +{ + T Deserialize(string json); // each implementation × each T = separate code +} +``` + +Consider non-generic alternatives for AOT-sensitive applications: + +```csharp +interface ISerializer +{ + object Deserialize(string json, Type type); // single implementation, smaller binary +} +``` + +## System.Linq.Expressions + +### Expression.Compile() Limitations +🟡 **UNDERSTAND** | .NET 7+ + +In AOT, `Expression.Compile()` uses an **interpreter** instead of JIT compilation. This means: +- It works functionally (no crash) +- It is 10-100x slower than JIT-compiled delegates +- Complex expressions may have subtle behavioral differences + +### Where Expressions Are Fine + +**EF Core queries**: Expression trees used for LINQ-to-SQL translation do **not** call `Compile()` — they are translated to SQL by the query provider. These work in AOT. + +```csharp +// ✅ Fine — EF Core translates the expression to SQL, never compiles it +var orders = await db.Orders + .Where(o => o.Total > 100) + .OrderBy(o => o.Date) + .ToListAsync(); +``` + +### Where Expressions Cause Problems + +**Compiled delegates on hot paths**: Any code that calls `.Compile()` and invokes the result frequently: + +❌ +```csharp +// Builds an expression tree and compiles to a delegate — uses interpreter in AOT +var param = Expression.Parameter(typeof(MyObj)); +var prop = Expression.Property(param, "Name"); +var lambda = Expression.Lambda>(prop, param); +var getter = lambda.Compile(); // 10-100x slower in AOT +``` +✅ +```csharp +// Use a direct delegate or generated code +Func getter = obj => obj.Name; + +// Or use a source generator to produce the accessor +``` + +### Expression.Property Overloads + +The overload `Expression.Property(Expression, string propertyName)` is not trim-safe because the trimmer can't determine which property is referenced by the string. Use the `PropertyInfo` overload instead: + +❌ +```csharp +var prop = Expression.Property(instance, "Name"); // IL2026 warning +``` +✅ +```csharp +var propInfo = typeof(MyObj).GetProperty("Name")!; +var prop = Expression.Property(instance, propInfo); // ✅ trim-safe +``` + +## Nullable Value Types and Generics + +### Nullable in Generic Contexts + +`Nullable` is a value type, so `typeof(Nullable<>).MakeGenericType(runtimeType)` follows value-type rules. However, `Nullable` instantiations for common types (`int?`, `bool?`, etc.) are typically already rooted by the runtime libraries. + +If you're constructing `Nullable` dynamically, ensure it's statically referenced. diff --git a/skills/diagnosing-dotnet-aot/references/library-compatibility.md b/skills/diagnosing-dotnet-aot/references/library-compatibility.md new file mode 100644 index 0000000000..8f55af3f89 --- /dev/null +++ b/skills/diagnosing-dotnet-aot/references/library-compatibility.md @@ -0,0 +1,175 @@ +# .NET Library AOT Compatibility Status + +AOT compatibility status of commonly used .NET libraries and frameworks. +All recommendations stay within the .NET ecosystem. + +> **Key**: ✅ Fully compatible | ⚠️ Partially compatible / experimental | 🔴 Not compatible + +## Contents +- [ASP.NET Core](#aspnet-core) — Minimal APIs, gRPC, MVC status +- [Entity Framework Core](#entity-framework-core) — Compiled models, precompiled queries +- [System.Text.Json](#systemtextjson) — Source-generated serialization +- [Microsoft.Extensions.*](#microsoftextensions) — DI, config, logging, options +- [Networking and Communication](#networking-and-communication) — HttpClient, gRPC, Redis +- [Serialization](#serialization) — STJ, protobuf, MessagePack, XML +- [Observability](#observability) — OpenTelemetry, EventSource +- [Authentication and Identity](#authentication-and-identity) +- [Desktop UI Frameworks](#desktop-ui-frameworks) — WPF, WinForms, MAUI +- [Evaluating Unlisted Libraries](#evaluating-unlisted-libraries) + +## ASP.NET Core + +| Feature | AOT Status | Notes | +|---------|:---:|-------| +| Minimal APIs | ✅ | Full support. Use `webapiaot` project template. | +| gRPC | ✅ | Fully supported with `Grpc.AspNetCore`. | +| Worker Services | ✅ | `BackgroundService` and `IHostedService` work. | +| Static Files | ✅ | Middleware works in AOT. | +| CORS | ✅ | Fully supported. | +| Health Checks | ✅ | Fully supported. | +| Rate Limiting | ✅ | Fully supported. | +| Output Caching | ✅ | Fully supported. | +| HTTP Logging | ✅ | Fully supported. | +| SignalR | ⚠️ | Partial support. Test thoroughly — some serialization and DI patterns may not work. | +| MVC Controllers | 🔴 | Not supported. Heavy reflection for model binding, action discovery, view activation. | +| Razor Pages | 🔴 | Not supported. Requires runtime compilation model. | +| Blazor Server | 🔴 | Not supported. | +| Blazor WebAssembly | ⚠️ | Uses Mono AOT (separate from NativeAOT), not covered by this skill. | + +### Minimal APIs — AOT Configuration + +```csharp +var builder = WebApplication.CreateSlimBuilder(args); + +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default); +}); + +var app = builder.Build(); +app.MapGet("/api/weather", () => Results.Ok(GetForecast())); +app.Run(); + +[JsonSerializable(typeof(WeatherForecast[]))] +internal partial class AppJsonContext : JsonSerializerContext { } +``` + +### If Your App Uses MVC Controllers + +MVC is not compatible with Native AOT. Your options within .NET: +1. **Migrate API controllers to Minimal APIs** — most REST APIs can be expressed as minimal API endpoints with the same functionality +2. **Keep MVC and don't use AOT** — MVC apps work with standard JIT and ReadyToRun deployment +3. **Hybrid approach** — separate your API endpoints (minimal APIs, AOT-published) from your MVC/Razor UI (JIT-published) into different services + +## Entity Framework Core + +| Feature | AOT Status | Notes | +|---------|:---:|-------| +| Compiled Models | ⚠️ | Required for AOT. Generate with `dotnet ef dbcontext optimize`. | +| Precompiled Queries | ⚠️ | Experimental. Use `--precompile-queries --nativeaot` flag. | +| Runtime Model Building | 🔴 | Not supported in AOT. Throws at runtime without compiled model. | +| Migrations (runtime) | 🔴 | Not supported. Run migrations from a separate non-AOT project. | +| LINQ Queries | ⚠️ | Static LINQ queries work with precompilation. Dynamic query building may fail. | + +### EF Core AOT Setup (Experimental) + +```bash +# Generate compiled model and precompiled queries +dotnet ef dbcontext optimize --precompile-queries --nativeaot +``` + +```xml + + +``` + +**Important limitations:** +- Compiled models must be regenerated when the model changes +- Not all query patterns can be precompiled — dynamic queries may fall back to interpreted expressions +- **This is experimental and not recommended for production** — test thoroughly +- Migrations must run from a separate JIT-compiled project or tool + +### If EF Core AOT Doesn't Meet Your Needs + +Stay within .NET but consider: +1. **ADO.NET directly** — `SqlConnection`, `SqlCommand`, `DbDataReader` are fully AOT-compatible +2. **Dapper** — check current AOT compatibility status; some features use reflection +3. **Thin repository pattern** — wrap ADO.NET in a repository abstraction for testability + +## System.Text.Json + +| Feature | AOT Status | Notes | +|---------|:---:|-------| +| Source-generated serialization | ✅ | Full support via `[JsonSerializable]` and `JsonSerializerContext`. | +| Reflection-based serialization | 🔴 | Not compatible. Produces IL2026 + IL3050 warnings. | +| `JsonDocument` / `JsonElement` | ✅ | DOM-based parsing works. | +| `JsonNode` | ✅ | Mutable DOM works. | +| Custom converters | ✅ | Work when registered via attributes or options, not discovered by reflection. | +| Polymorphic serialization | ✅ | Use `[JsonDerivedType]` (.NET 7+). | + +## Microsoft.Extensions.* + +| Library | AOT Status | Notes | +|---------|:---:|-------| +| DependencyInjection | ✅ | Built-in container works. Avoid assembly scanning. | +| Configuration | ✅ | With `EnableConfigurationBindingGenerator` (.NET 8+). | +| Logging | ✅ | With `[LoggerMessage]` source generator (.NET 6+). | +| Options | ✅ | With `[OptionsValidator]` source generator (.NET 8+). | +| Http (HttpClientFactory) | ✅ | Fully supported. | +| Caching (IMemoryCache) | ✅ | Fully supported. | +| Resilience (Polly v8+) | ✅ | Polly 8+ is AOT-compatible. | + +## Networking and Communication + +| Library | AOT Status | Notes | +|---------|:---:|-------| +| HttpClient | ✅ | Fully supported. Use `IHttpClientFactory` for lifecycle management. | +| gRPC (Grpc.Net.Client) | ✅ | Fully supported with protobuf source generation. | +| StackExchange.Redis | ⚠️ | Core functionality works. LuaScript parameter passing uses reflection (annotated `[RequiresUnreferencedCode]`). | + +## Serialization + +| Library | AOT Status | Notes | +|---------|:---:|-------| +| System.Text.Json (source gen) | ✅ | Recommended for all AOT apps. | +| Newtonsoft.Json | 🔴 | Fundamentally reflection-based. Will not be updated for AOT. Migrate to System.Text.Json. | +| protobuf-net | ⚠️ | Check latest version for AOT annotations. Core serialization may work with manual configuration. | +| MessagePack-CSharp | ⚠️ | Source generator mode available. Check latest release for AOT status. | +| System.Xml.Serialization | 🔴 | Reflection-based. Use `XmlReader`/`XmlWriter` directly for AOT. | +| BinaryFormatter | 🔴 | Removed in .NET 9. | + +## Observability + +| Library | AOT Status | Notes | +|---------|:---:|-------| +| OpenTelemetry (core) | ✅ | AOT-compatible. HttpClient and ASP.NET Core instrumentation work. | +| OpenTelemetry SqlClient instrumentation | 🔴 | Marked `[RequiresUnreferencedCode]` — underlying SqlClient not AOT compatible. | +| EventSource / EventPipe | ⚠️ | Requires `true`. Not all runtime events supported. | +| dotnet-trace / dotnet-counters | ⚠️ | Work with EventPipe support enabled. | +| Heap analysis (dotnet-gcdump) | 🔴 | Not supported in Native AOT. | + +## Authentication and Identity + +| Library | AOT Status | Notes | +|---------|:---:|-------| +| Microsoft.IdentityModel.JsonWebTokens | ✅ | Migrated from Newtonsoft.Json to System.Text.Json for AOT. | +| ASP.NET Core Authentication | ⚠️ | JWT Bearer works with minimal APIs. Cookie auth may need testing. | + +## Desktop UI Frameworks + +| Framework | AOT Status | Notes | +|-----------|:---:|-------| +| WPF | 🔴 | Heavy reflection usage. Not AOT-compatible. | +| Windows Forms | 🔴 | Relies on built-in COM marshalling. Not AOT-compatible. | +| .NET MAUI | ⚠️ | AOT support on iOS/Mac Catalyst. Requires trim and AOT-compatible code. XAML must be ahead-of-time compiled. | +| Avalonia UI | ⚠️ | Check latest version for AOT support status. | + +## Evaluating Unlisted Libraries + +For libraries not listed here, follow this process: + +1. **Check for `IsAotCompatible` or `IsTrimmable` in the library's project file** — indicates the author has tested for AOT +2. **Check the library's NuGet page or GitHub README** for AOT compatibility notes +3. **Search the library's GitHub issues** for "AOT", "trimming", or "NativeAOT" +4. **Create a test project**: reference the library, set `true`, and run `dotnet publish -r `. Any warnings indicate potential issues +5. **Set `false`** to see individual warnings instead of one warning per assembly diff --git a/skills/diagnosing-dotnet-aot/references/project-setup.md b/skills/diagnosing-dotnet-aot/references/project-setup.md new file mode 100644 index 0000000000..160c3eff95 --- /dev/null +++ b/skills/diagnosing-dotnet-aot/references/project-setup.md @@ -0,0 +1,240 @@ +# Project Setup and CI Validation for AOT + +MSBuild properties, analyzer configuration, warning codes, and CI pipeline patterns for Native AOT. + +## Contents +- [Essential MSBuild Properties](#essential-msbuild-properties) — Application and library project config +- [Warning Codes Reference](#warning-codes-reference) — IL2xxx trimming, IL3xxx AOT, single-file +- [Roslyn Analyzers vs ILC](#roslyn-analyzers-vs-ilc-full-publish) — When to use each +- [CI Pipeline Validation](#ci-pipeline-validation) — AOT test app pattern, GitHub Actions +- [Multi-Targeting for AOT Compatibility](#multi-targeting-for-aot-compatibility) — Cross-framework annotations +- [Recommended Project Setup Checklist](#recommended-project-setup-checklist) + +## Essential MSBuild Properties + +### Application Projects + +```xml + + + true + + + false + + + true + +``` + +### Library Projects + +```xml + + + true + + + true + +``` + +Setting `IsAotCompatible` to `true` automatically enables: +- `IsTrimmable` — marks the assembly as safe to trim +- `EnableTrimAnalyzer` — Roslyn analyzer for trim warnings +- `EnableSingleFileAnalyzer` — Roslyn analyzer for single-file warnings +- `EnableAotAnalyzer` — Roslyn analyzer for AOT warnings + +### Optimization Properties + +```xml + + + Size + + + true + + + true + + + true + + + true + +``` + +## Warning Codes Reference + +### Trimming Warnings (IL2xxx) + +| Code | Description | Common Fix | +|------|-------------|------------| +| IL2026 | Using member with `[RequiresUnreferencedCode]` | Eliminate reflection or propagate `[RequiresUnreferencedCode]` | +| IL2046 | `[RequiresUnreferencedCode]` mismatch on override/implementation | Add matching attribute to override | +| IL2055 | Call to `Type.MakeGenericType` with unknown type | Use static dispatch or root needed instantiations | +| IL2057 | Unrecognized value passed to `Type.GetType` | Use compile-time known type names or `typeof()` | +| IL2067 | Parameter doesn't satisfy `[DynamicallyAccessedMembers]` in target | Add `[DynamicallyAccessedMembers]` to parameter | +| IL2070 | `this` argument doesn't satisfy `[DynamicallyAccessedMembers]` | Annotate the `Type` source with required members | +| IL2072 | Return value doesn't satisfy `[DynamicallyAccessedMembers]` | Annotate return value or method | +| IL2075 | `Type.GetType` return value used in reflection | Use `typeof()` instead of `Type.GetType(string)` | +| IL2077 | Field doesn't satisfy `[DynamicallyAccessedMembers]` in target | Add `[DynamicallyAccessedMembers]` to the field | +| IL2104 | Assembly produced trim warnings | Fix warnings inside the assembly or contact library author | +| IL2125 | Referenced assembly not annotated as trim-compatible | Verify the dependency is trim-safe or contact author | + +### AOT Warnings (IL3xxx) + +| Code | Description | Common Fix | +|------|-------------|------------| +| IL3050 | Using member with `[RequiresDynamicCode]` | Eliminate dynamic code or use `RuntimeFeature.IsDynamicCodeSupported` guard | +| IL3051 | `[RequiresDynamicCode]` mismatch on override | Add matching attribute to override | +| IL3052 | COM marshalling type not supported in AOT | Use `ComWrappers` API instead | +| IL3053 | COM interop not supported in AOT | Use `ComWrappers` API instead | +| IL3058 | Referenced assembly not annotated as AOT-compatible | Check dependency status or contact library author | + +### Single File Warnings (IL3xxx) + +| Code | Description | Common Fix | +|------|-------------|------------| +| IL3000 | `Assembly.Location` returns empty string in single-file | Use `AppContext.BaseDirectory` instead | +| IL3001 | `Assembly.GetFile` not supported in single-file | Use embedded resources or `AppContext.BaseDirectory` | +| IL3002 | Using member with `[RequiresAssemblyFiles]` | Avoid assembly file access or guard with `!IsPublishedAsSingleFile` | + +## Roslyn Analyzers vs ILC (Full Publish) + +| Capability | Roslyn Analyzers | ILC (dotnet publish) | +|-----------|:---:|:---:| +| IDE integration (squiggles) | ✅ | 🔴 | +| Immediate feedback | ✅ | 🔴 | +| Whole-program analysis | 🔴 | ✅ | +| Analyzes dependencies | 🔴 | ✅ | +| Guaranteed complete warning set | 🔴 | ✅ | +| Requires publish step | 🔴 | ✅ | + +**Recommendation**: Use both. Roslyn analyzers for fast feedback during development. Full publish for CI validation. + +## CI Pipeline Validation + +### AOT Compatibility Test App Pattern + +Create a dedicated test project that exercises your library APIs and publishes with AOT: + +**1. Create the test project** +```xml + + + + Exe + net10.0 + true + false + true + + + + + + +``` + +`TrimmerRootAssembly` ensures every method in your library is analyzed, even if not called by the test app. + +**2. Create a publish-and-verify script** +```bash +#!/bin/bash +set -e +dotnet publish test/AotCompatibility.TestApp/ \ + -c Release \ + -r linux-x64 \ + --no-restore \ + 2>&1 | tee publish-output.txt + +# Check for warnings (IlcTreatWarningsAsErrors will fail the build if any exist) +echo "AOT publish succeeded with zero warnings" +``` + +**3. Add to CI workflow** +```yaml +# .github/workflows/aot-compat.yml +name: AOT Compatibility +on: + pull_request: + paths: ['src/**', 'test/AotCompatibility.TestApp/**'] +jobs: + aot-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - run: sudo apt-get install -y clang zlib1g-dev + - run: dotnet restore + - run: dotnet publish test/AotCompatibility.TestApp/ -c Release -r linux-x64 +``` + +### Running the Published AOT Binary + +For libraries with suppressed warnings, also execute the published binary to verify runtime behavior: + +```bash +# After publish +./test/AotCompatibility.TestApp/bin/Release/net10.0/linux-x64/publish/AotCompatibility.TestApp +echo "Exit code: $?" +``` + +## Multi-Targeting for AOT Compatibility + +### Library Targeting Multiple Frameworks + +```xml + + netstandard2.0;net8.0;net10.0 + true + +``` + +### Using Annotations Across Frameworks + +If your library targets frameworks before `net7.0` where trim/AOT attributes don't exist, you have two options: + +**Option 1: `#if` directives** +```csharp +public static object CreateInstance( +#if NET7_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] +#endif + Type type) +{ + return Activator.CreateInstance(type); +} +``` + +**Option 2: Define the attributes internally** + +Copy attribute definitions into your project — the trim/AOT tools recognize them by name and namespace regardless of which assembly defines them. This ensures annotations are present on all target frameworks. + +You can also use the [PolySharp](https://www.nuget.org/packages/PolySharp/) NuGet package to auto-generate polyfill attribute definitions at build time. + +## Recommended Project Setup Checklist + +For applications: +- [ ] `true` in project file (not just on command line) +- [ ] `false` to see all warnings +- [ ] `true` for zero-warning builds +- [ ] `true` if using `IConfiguration` +- [ ] `JsonSerializerContext` registered for all serialized types +- [ ] `[LoggerMessage]` used for all logging +- [ ] `[GeneratedRegex]` used for all regex patterns +- [ ] `dotnet publish -r ` run and verified zero warnings +- [ ] Published binary executed and tested + +For libraries: +- [ ] `true` set (with TFM condition if multi-targeting) +- [ ] AOT compatibility test app created with `TrimmerRootAssembly` +- [ ] CI pipeline runs AOT publish on every PR +- [ ] All public APIs either annotated or AOT-compatible +- [ ] No `#pragma warning disable` used for trim/AOT warnings (use `[UnconditionalSuppressMessage]`) diff --git a/skills/diagnosing-dotnet-aot/references/reflection-and-di.md b/skills/diagnosing-dotnet-aot/references/reflection-and-di.md new file mode 100644 index 0000000000..965d77b0dd --- /dev/null +++ b/skills/diagnosing-dotnet-aot/references/reflection-and-di.md @@ -0,0 +1,231 @@ +# Reflection and Dependency Injection Patterns for AOT + +How to annotate reflection usage and design DI registrations for trim and AOT compatibility. + +## Contents +- [The Annotation Workflow](#the-annotation-workflow) — Decision tree for handling reflection warnings +- [DynamicallyAccessedMembers — Step by Step](#dynamicallyaccessedmembers--step-by-step) — API-to-annotation mapping, propagation, common mistakes +- [RequiresUnreferencedCode and RequiresDynamicCode](#requiresunreferencedcode-and-requiresdynamiccode) — When and how to mark APIs +- [Suppressing Warnings Safely](#suppressing-warnings-safely) — Legitimate suppression, DynamicDependency +- [Dependency Injection Patterns](#dependency-injection-patterns) — Static registration, keyed services, open generics + +## The Annotation Workflow + +When you encounter an AOT or trim warning on reflection code, follow these steps in order: + +1. **Can you eliminate reflection entirely?** → Best option. Use generics, source generators, or static dispatch. +2. **Are the types known at compile time?** → Annotate with `[DynamicallyAccessedMembers]`. +3. **Is the code fundamentally dynamic?** → Mark with `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]`. +4. **Are you certain the code is safe despite warnings?** → Suppress with `[UnconditionalSuppressMessage]` (last resort). + +## DynamicallyAccessedMembers — Step by Step + +### Identifying What to Annotate + +Look at the reflection API being called and match it to the correct member type: + +| Reflection API | Required `DynamicallyAccessedMemberTypes` | +|---------------|------------------------------------------| +| `Activator.CreateInstance(type)` | `PublicParameterlessConstructor` | +| `Activator.CreateInstance(type, args)` | `PublicConstructors` | +| `type.GetMethod(name)` / `type.GetMethods()` | `PublicMethods` | +| `type.GetProperty(name)` / `type.GetProperties()` | `PublicProperties` | +| `type.GetField(name)` / `type.GetFields()` | `PublicFields` | +| `type.GetEvent(name)` / `type.GetEvents()` | `PublicEvents` | +| `type.GetConstructor(...)` | `PublicConstructors` | +| `type.GetMembers()` | `All` (avoid — use narrowest type) | + +### Propagating Annotations Up the Call Chain + +Annotations must flow from the reflection call site back to the source of the `Type`: + +**Step 1: Annotate where reflection is used** +```csharp +void CreateWidget( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type widgetType) +{ + var widget = Activator.CreateInstance(widgetType); // ✅ no warning +} +``` + +**Step 2: Propagate to callers** +```csharp +void BuildWidget< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + TWidget>() +{ + CreateWidget(typeof(TWidget)); // ✅ no warning — TWidget is annotated +} +``` + +**Step 3: Verify at the call site** +```csharp +BuildWidget(); // ✅ no warning — MyWidget is a concrete type +``` + +### Annotating Fields and Properties + +When a `Type` is stored in a field before being passed to reflection, the field must also be annotated: + +```csharp +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +private Type _serializationType; + +void Serialize(object obj) +{ + foreach (var prop in _serializationType.GetProperties()) // ✅ no warning + { + // ... + } +} +``` + +### Common Mistakes with Annotations + +| Mistake | Problem | Fix | +|---------|---------|-----| +| Annotating only the leaf method | Intermediate methods still produce warnings | Annotate the entire chain from call site to reflection | +| Using `DynamicallyAccessedMemberTypes.All` | Preserves everything — defeats trimming purpose | Use the narrowest type (e.g., `PublicParameterlessConstructor`) | +| Annotating virtual/interface methods | All overrides must have matching annotations | Avoid annotating virtual methods — redesign instead | +| Missing annotation on generic type parameter | `typeof(T).GetProperties()` warns without annotation on `T` | Add `[DynamicallyAccessedMembers]` to the generic parameter | + +## RequiresUnreferencedCode and RequiresDynamicCode + +### When to Use + +- `[RequiresUnreferencedCode]` — the code uses reflection that can't be annotated (truly dynamic types) +- `[RequiresDynamicCode]` — the code uses APIs that require runtime code generation (Reflection.Emit, MakeGenericType with unknown value types) + +### Writing Effective Messages + +```csharp +// ❌ Not helpful +[RequiresUnreferencedCode("Uses reflection")] + +// ✅ Helpful — explains what's incompatible and suggests alternative +[RequiresUnreferencedCode( + "Plugin discovery uses Assembly.LoadFrom which is not compatible with trimming. " + + "Register plugins at compile time using AddPlugin() instead.")] + +// ✅ With URL for more guidance +[RequiresUnreferencedCode("Dynamic handler resolution is not compatible with trimming.", + Url = "https://learn.microsoft.com/dotnet/core/deploying/native-aot/fixing-warnings")] +``` + +### Propagating Up Public APIs + +```csharp +class HandlerRegistry +{ + const string TrimMessage = "Handler registration by name is not compatible with AOT."; + + [RequiresUnreferencedCode(TrimMessage)] + private Type ResolveByName(string name) => Type.GetType(name); + + [RequiresUnreferencedCode(TrimMessage)] + public IHandler GetHandler(string name) + { + var type = ResolveByName(name); // no warning — method is also marked + return (IHandler)Activator.CreateInstance(type); + } +} +``` + +## Suppressing Warnings Safely + +### When Suppression Is Legitimate + +1. **Runtime feature check**: Code is guarded by `RuntimeFeature.IsDynamicCodeSupported` +2. **Known types preserved elsewhere**: Types are kept via `[DynamicDependency]` or always referenced +3. **EventSource false positives**: `WriteEvent` with >3 params triggers IL2026 but is safe for primitive types + +### How to Suppress + +```csharp +[UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", + Justification = "Only primitive types are passed to WriteEvent")] +void LogEvent(string name, int id, long timestamp, string category) +{ + WriteEvent(1, name, id, timestamp, category); +} +``` + +**Never use `#pragma warning disable` or `[SuppressMessage]` for trim/AOT warnings** — they are not preserved in the compiled assembly and the trimmer will not see them. + +### DynamicDependency as a Preservation Tool + +Use `[DynamicDependency]` to keep specific members when you know they'll be needed but can't express it via annotations: + +```csharp +[DynamicDependency("Process", typeof(MyHandler))] +[UnconditionalSuppressMessage("Trimming", "IL2026", + Justification = "MyHandler.Process is preserved via DynamicDependency")] +void InvokeHandler() +{ + var method = typeof(MyHandler).GetMethod("Process"); + method!.Invoke(null, null); +} +``` + +## Dependency Injection Patterns + +### Static Registration (AOT-Safe) + +Always register services with statically known types: + +```csharp +// ✅ AOT-safe — types are known at compile time +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddTransient(); +``` + +### Avoid Assembly Scanning + +❌ +```csharp +// Scans assemblies at runtime — not AOT compatible +builder.Services.Scan(scan => scan + .FromAssemblyOf() + .AddClasses(c => c.AssignableTo()) + .AsImplementedInterfaces()); +``` +✅ +```csharp +// Explicit registration — AOT compatible +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +``` + +### Keyed Services (.NET 8+) + +Use keyed services for named registrations instead of string-based resolution: + +```csharp +builder.Services.AddKeyedSingleton("email"); +builder.Services.AddKeyedSingleton("sms"); + +// Inject by key +public class OrderProcessor([FromKeyedServices("email")] INotifier notifier) { } +``` + +### Factory Methods with DynamicallyAccessedMembers + +When a factory must create instances by type, annotate the type parameter: + +```csharp +public static T CreateService< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + T>() where T : class +{ + return (T)Activator.CreateInstance(typeof(T))!; +} +``` + +### Open Generic Registration + +Open generic registrations (`services.AddSingleton(typeof(IRepository<>), typeof(Repository<>))`) work with the built-in DI container in AOT **if** the container can determine all closed types at compile time. This works when closed types are requested through constructor injection of known services. + +If you dynamically construct `IRepository` for runtime-determined `T`, it may fail. Ensure all generic closures are statically reachable. diff --git a/skills/diagnosing-dotnet-aot/references/serialization-and-config.md b/skills/diagnosing-dotnet-aot/references/serialization-and-config.md new file mode 100644 index 0000000000..5b61cc1f45 --- /dev/null +++ b/skills/diagnosing-dotnet-aot/references/serialization-and-config.md @@ -0,0 +1,217 @@ +# Serialization and Configuration Patterns for AOT + +Source-generated alternatives for reflection-based serialization and configuration binding. + +## Contents +- [System.Text.Json Source Generation](#systemtextjson-source-generation) — JsonSerializerContext, polymorphic types, custom converters +- [Migrating from Newtonsoft.Json](#migrating-from-newtonsoftjson) — API mapping, behavioral differences +- [Configuration Binding Source Generator](#configuration-binding-source-generator) — EnableConfigurationBindingGenerator, options pattern +- [Options Validation Source Generator](#options-validation-source-generator) — OptionsValidator +- [Logging Source Generation](#logging-source-generation) — LoggerMessage + +## System.Text.Json Source Generation + +### Creating a JsonSerializerContext +🟡 **DO** | .NET 6+ + +Every type you serialize or deserialize must be registered in a `JsonSerializerContext`. The source generator produces optimized serialization code at compile time. + +**Step 1: Create the context** +```csharp +[JsonSerializable(typeof(WeatherForecast))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(ErrorResponse))] +internal partial class AppJsonContext : JsonSerializerContext { } +``` + +**Step 2: Use the context in serialization calls** +```csharp +// Serialize +string json = JsonSerializer.Serialize(forecast, AppJsonContext.Default.WeatherForecast); + +// Deserialize +var forecast = JsonSerializer.Deserialize(json, AppJsonContext.Default.WeatherForecast); + +// With HttpClient +var response = await httpClient.GetFromJsonAsync("/api/weather", + AppJsonContext.Default.ListWeatherForecast); +``` + +**Step 3: Register context in ASP.NET Core minimal APIs** +```csharp +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default); +}); +``` + +### Handling Polymorphic Types +🟡 **DO** | .NET 7+ + +Use `[JsonDerivedType]` for polymorphic serialization instead of runtime type discovery. + +❌ +```csharp +// Runtime type discovery — not AOT compatible +JsonSerializer.Serialize(derivedObj); +``` +✅ +```csharp +[JsonDerivedType(typeof(Cat), typeDiscriminator: "cat")] +[JsonDerivedType(typeof(Dog), typeDiscriminator: "dog")] +public class Animal { public string Name { get; set; } } + +[JsonSerializable(typeof(Animal))] +internal partial class AppJsonContext : JsonSerializerContext { } +``` + +### Custom Converters with Source Generation +🟡 **DO** | .NET 8+ + +Custom `JsonConverter` implementations work with source generation, but they must be registered on `JsonSerializerOptions` or applied via attributes — not discovered via reflection. + +```csharp +[JsonConverter(typeof(DateOnlyConverter))] +public record Event(string Name, DateOnly Date); + +public class DateOnlyConverter : JsonConverter +{ + public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) => DateOnly.Parse(reader.GetString()!); + + public override void Write(Utf8JsonWriter writer, DateOnly value, + JsonSerializerOptions options) => writer.WriteStringValue(value.ToString("O")); +} +``` + +### Caching JsonSerializerOptions +🔴 **DO** | .NET 5+ + +Always cache `JsonSerializerOptions` — creating a new instance per call re-generates metadata (592x slower in .NET 6). + +❌ +```csharp +JsonSerializer.Serialize(obj, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); +``` +✅ +```csharp +private static readonly JsonSerializerOptions s_options = new() +{ + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + TypeInfoResolver = AppJsonContext.Default +}; +JsonSerializer.Serialize(obj, s_options); +``` + +## Migrating from Newtonsoft.Json + +### Common Migration Patterns + +| Newtonsoft.Json | System.Text.Json Equivalent | +|----------------|----------------------------| +| `JsonConvert.SerializeObject(obj)` | `JsonSerializer.Serialize(obj, AppJsonContext.Default.MyType)` | +| `JsonConvert.DeserializeObject(json)` | `JsonSerializer.Deserialize(json, AppJsonContext.Default.MyType)` | +| `[JsonProperty("name")]` | `[JsonPropertyName("name")]` | +| `[JsonIgnore]` | `[JsonIgnore]` (same attribute name, different namespace) | +| `JsonSerializerSettings` | `JsonSerializerOptions` (cache as static) | +| `JObject.Parse(json)` | `JsonDocument.Parse(json)` or `JsonNode.Parse(json)` | +| `JToken` navigation | `JsonNode` / `JsonElement` navigation | +| `DefaultValueHandling.Ignore` | `DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault` | +| `NullValueHandling.Ignore` | `DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull` | +| `ReferenceLoopHandling.Ignore` | `ReferenceHandler = ReferenceHandler.IgnoreCycles` (.NET 6+) | + +### Key Behavioral Differences + +- STJ is case-sensitive by default (Newtonsoft is case-insensitive). Use `PropertyNameCaseInsensitive = true` if needed. +- STJ does not serialize fields by default. Use `IncludeFields = true` if needed. +- STJ requires `[JsonConstructor]` for parameterized constructors (Newtonsoft auto-detects). + +## Configuration Binding Source Generator + +### Enabling the Generator +🟡 **DO** | .NET 8+ + +```xml + + true + +``` + +The source generator intercepts calls to `Bind()`, `Get()`, and `Configure()` and replaces them with compile-time generated code. Your C# code does not need to change. + +### Options Pattern with Source-Generated Binding + +```csharp +public class SmtpOptions +{ + public string Host { get; set; } = "localhost"; + public int Port { get; set; } = 25; + public bool UseSsl { get; set; } +} + +// Registration — works with source generator +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection("Smtp")); +``` + +### Limitations of the Config Binding Generator + +- Properties must be public with `get`/`set` — `init`-only setters are not supported. +- Types must have a public parameterless constructor. +- Complex converters (`TypeConverter` patterns) may not be source-generated. + +## Options Validation Source Generator + +### Source-Generated Validation +🟡 **DO** | .NET 8+ + +Use `[OptionsValidator]` to generate validation code at compile time instead of reflection-based `ValidateDataAnnotations()`. + +❌ +```csharp +builder.Services.AddOptions() + .Bind(config.GetSection("My")) + .ValidateDataAnnotations(); // uses reflection +``` +✅ +```csharp +[OptionsValidator] +internal sealed partial class MyOptionsValidator : IValidateOptions { } + +builder.Services.AddOptions() + .Bind(config.GetSection("My")); +builder.Services.AddSingleton, MyOptionsValidator>(); +``` + +### Supported Validation Attributes + +The source generator supports `System.ComponentModel.DataAnnotations` attributes: +- `[Required]`, `[Range]`, `[MinLength]`, `[MaxLength]`, `[RegularExpression]` +- `[StringLength]`, `[EmailAddress]`, `[Url]`, `[Phone]` +- Custom `ValidationAttribute` subclasses (if the logic doesn't use reflection) + +## Logging Source Generation + +### LoggerMessage Source Generator +🟡 **DO** | .NET 6+ + +Use `[LoggerMessage]` to generate high-performance, AOT-compatible logging methods. Avoids boxing, string formatting, and reflection. + +❌ +```csharp +_logger.LogInformation("Processing order {OrderId} for {Amount}", orderId, amount); +// boxes value types, parses template at runtime +``` +✅ +```csharp +public static partial class Log +{ + [LoggerMessage(Level = LogLevel.Information, + Message = "Processing order {OrderId} for {Amount}")] + public static partial void ProcessingOrder(ILogger logger, int orderId, decimal amount); +} + +// Usage +Log.ProcessingOrder(_logger, orderId, amount); +``` +**Impact: Zero boxing, compile-time template parsing, AOT-compatible.** diff --git a/tests/diagnosing-dotnet-aot/EVALUATION.md b/tests/diagnosing-dotnet-aot/EVALUATION.md new file mode 100644 index 0000000000..1fdb4632be --- /dev/null +++ b/tests/diagnosing-dotnet-aot/EVALUATION.md @@ -0,0 +1,130 @@ +# Evaluation Test Cases for `diagnosing-dotnet-aot` + +These tests validate that the skill provides value **beyond what an unskilled LLM already knows**. Each test targets a specific knowledge gap where base models consistently give wrong or incomplete answers. + +## How to Run + +For each test case: +1. Start a fresh Claude session with the `diagnosing-dotnet-aot` skill loaded +2. Use the prompt provided +3. Evaluate the response against the success criteria +4. Mark pass/fail for each criterion + +## Test 1: `#pragma warning disable` Doesn't Work for Trim Warnings + +**Asset:** [assets/pragma-warning-suppress.cs](assets/pragma-warning-suppress.cs) + +**Prompt:** +> Review this code for AOT compatibility. The developer has suppressed the AOT warnings — is this correct? + +**Success Criteria:** +- [ ] Identifies that `#pragma warning disable` is **not preserved in IL** and the trimmer/ILC ignores it +- [ ] Recommends `[UnconditionalSuppressMessage]` as the correct suppression mechanism +- [ ] Notes that even with proper suppression, the underlying code (`Type.GetType(string)`, `MakeGenericType`) is still incompatible — suppression is just silencing the warning, not fixing the problem +- [ ] Suggests redesigning to eliminate reflection (e.g., static factory pattern) rather than just suppressing + +**Why This Test Matters:** Base LLMs frequently suggest `#pragma warning disable` for trim warnings, treating them like any other C# warning. This is one of the most common mistakes developers make. + +--- + +## Test 2: MakeGenericType — Reference Types vs Value Types + +**Asset:** [assets/make-generic-type-mixed.cs](assets/make-generic-type-mixed.cs) + +**Prompt:** +> Analyze this code for Native AOT compatibility. Which MakeGenericType calls are safe and which are dangerous? + +**Success Criteria:** +- [ ] Correctly identifies `CreateRepository` as **safe** because `entityType` is always a reference type (classes share canonical code) +- [ ] Correctly identifies `CreateAggregator` as **unsafe** because `numericType` could be a value type (int, float, double each need dedicated compiled code) +- [ ] Correctly identifies `CreateHandler` as **safe** because `T` is constrained to `class` +- [ ] Explains the fundamental AOT rule: reference types share one code path; value types each need their own +- [ ] Does NOT blanket-flag all three methods as dangerous + +**Why This Test Matters:** Base LLMs tend to flag ALL `MakeGenericType` calls as AOT-incompatible. The ref/value type distinction is nuanced and critical — over-flagging creates false positives that erode developer trust. + +--- + +## Test 3: Expression.Compile() Silent Performance Cliff + +**Asset:** [assets/expression-compile-hotpath.cs](assets/expression-compile-hotpath.cs) + +**Prompt:** +> Check this code for AOT issues. Pay attention to both the PropertyAccessorCache and the OrderRepository. + +**Success Criteria:** +- [ ] Flags `lambda.Compile()` in `PropertyAccessorCache.RegisterType` as a **critical issue** — falls back to 10-100x slower interpreter in AOT +- [ ] Notes that **no IL warning is emitted** for this — it's a silent performance degradation +- [ ] Identifies that `GetValue` is called on a hot path, making the perf impact severe +- [ ] Correctly identifies the EF Core LINQ query in `OrderRepository` as **safe** — expression trees are translated to SQL, never compiled to delegates +- [ ] Suggests a concrete alternative (e.g., direct delegate, source generator, or reflection with caching) + +**Why This Test Matters:** This is invisible to the warning system. Base LLMs either miss it entirely or incorrectly flag EF Core LINQ expressions as problematic too. + +--- + +## Test 4: EventSource False Positive + +**Asset:** [assets/eventsource-false-positive.cs](assets/eventsource-false-positive.cs) + +**Prompt:** +> I'm getting IL2026 warnings on my EventSource methods. How do I fix them for AOT? + +**Success Criteria:** +- [ ] Identifies that `WriteEvent` with >3 parameters triggers IL2026 as a **known false positive** +- [ ] Confirms that passing **only primitive types** (string, int, long) to `WriteEvent` is safe in AOT +- [ ] Recommends `[UnconditionalSuppressMessage]` (not `#pragma`) with a justification mentioning primitive types +- [ ] Does NOT suggest rewriting the EventSource methods or removing parameters + +**Why This Test Matters:** Base LLMs treat all IL2026 warnings as real problems and suggest invasive refactoring. Knowing when to suppress is expert-level knowledge. + +--- + +## Test 5: Incomplete Annotation Propagation Chain + +**Asset:** [assets/annotation-chain-incomplete.cs](assets/annotation-chain-incomplete.cs) + +**Prompt:** +> The `CreateInstance` method has the right annotation but I'm still getting warnings. Why? + +**Success Criteria:** +- [ ] Traces the call chain: `Resolve` → `CreateFromConfig` → `CreateInstance` +- [ ] Identifies that `CreateFromConfig` is missing `[DynamicallyAccessedMembers]` on its `serviceType` parameter +- [ ] Identifies that `CreateService` is missing `[DynamicallyAccessedMembers]` on its type parameter `T` +- [ ] Identifies that `Resolve` also needs annotation (or `[RequiresUnreferencedCode]`) since it's the entry point +- [ ] Explains the propagation rule: annotations must flow from the reflection call site **all the way back** to the public API boundary +- [ ] Shows the correctly annotated chain (concrete code fix for each method) + +**Why This Test Matters:** Base LLMs understand `[DynamicallyAccessedMembers]` exists but rarely walk through multi-hop propagation correctly. They often annotate only the immediate caller. + +--- + +## Test 6: IsAotCompatible Property Cascade + +**Asset:** [assets/isaotcompatible-cascade.cs](assets/isaotcompatible-cascade.cs) + +**Prompt:** +> Review my library's project file for AOT setup. Am I missing anything? + +**Success Criteria:** +- [ ] Identifies that `IsTrimmable`, `EnableTrimAnalyzer`, `EnableSingleFileAnalyzer`, `EnableAotAnalyzer` are **redundant** because `IsAotCompatible=true` automatically enables all four +- [ ] Recommends removing the redundant properties to reduce confusion +- [ ] Identifies that `TrimmerSingleWarn` should be set to `false` to see individual warnings instead of one per assembly +- [ ] Does NOT say the redundant properties are wrong or harmful — they're just unnecessary + +**Why This Test Matters:** Base LLMs list these properties independently in their AOT setup advice without knowing the cascade relationship. This leads to cargo-cult project files. + +--- + +## Scoring + +| Test | Target Behavior | Pass Threshold | +|------|----------------|----------------| +| 1. #pragma suppress | Catch the #pragma mistake | All 4 criteria | +| 2. MakeGenericType | Distinguish ref vs value types | All 5 criteria | +| 3. Expression.Compile | Flag silent perf cliff, spare EF Core | All 5 criteria | +| 4. EventSource | Recognize false positive | All 4 criteria | +| 5. Annotation chain | Walk full propagation chain | 5 of 6 criteria | +| 6. IsAotCompatible | Know the cascade | 3 of 4 criteria | + +**Overall pass:** 5 of 6 tests pass at threshold. diff --git a/tests/diagnosing-dotnet-aot/assets/annotation-chain-incomplete.cs b/tests/diagnosing-dotnet-aot/assets/annotation-chain-incomplete.cs new file mode 100644 index 0000000000..c32b931e91 --- /dev/null +++ b/tests/diagnosing-dotnet-aot/assets/annotation-chain-incomplete.cs @@ -0,0 +1,43 @@ +// Test asset: DynamicallyAccessedMembers annotation only on leaf, not propagated +// Expected: Skill should trace the full chain and identify missing annotations + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MyApp; + +public class ServiceFactory +{ + // Has annotation — good + public object CreateInstance( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + Type serviceType) + { + return Activator.CreateInstance(serviceType)!; + } + + // MISSING annotation on serviceType parameter — will produce warning + public object CreateFromConfig(Type serviceType) + { + // Calls annotated method but doesn't propagate the annotation + return CreateInstance(serviceType); + } + + // MISSING annotation on T — will produce warning + public T CreateService() where T : class + { + return (T)CreateInstance(typeof(T)); + } +} + +public class ServiceRegistry +{ + private readonly ServiceFactory _factory = new(); + + // This is the entry point — developer may not realize annotations + // need to propagate all the way here + public object Resolve(Type type) + { + return _factory.CreateFromConfig(type); + } +} diff --git a/tests/diagnosing-dotnet-aot/assets/eventsource-false-positive.cs b/tests/diagnosing-dotnet-aot/assets/eventsource-false-positive.cs new file mode 100644 index 0000000000..149431e73c --- /dev/null +++ b/tests/diagnosing-dotnet-aot/assets/eventsource-false-positive.cs @@ -0,0 +1,26 @@ +// Test asset: EventSource with >3 params triggers IL2026, but is safe for primitives +// Expected: Skill should identify this as a known false positive safe to suppress + +using System.Diagnostics.Tracing; + +namespace MyApp; + +[EventSource(Name = "MyApp-Operations")] +public sealed class AppEventSource : EventSource +{ + public static readonly AppEventSource Log = new(); + + // IL2026 warning on WriteEvent because >3 params — but safe for primitive types + [Event(1, Level = EventLevel.Informational)] + public void OperationCompleted(string operationName, int operationId, long durationMs, string status) + { + WriteEvent(1, operationName, operationId, durationMs, status); + } + + // Also safe — all primitives + [Event(2, Level = EventLevel.Warning)] + public void OperationFailed(string operationName, int operationId, long durationMs, string errorCode, int retryCount) + { + WriteEvent(2, operationName, operationId, durationMs, errorCode, retryCount); + } +} diff --git a/tests/diagnosing-dotnet-aot/assets/expression-compile-hotpath.cs b/tests/diagnosing-dotnet-aot/assets/expression-compile-hotpath.cs new file mode 100644 index 0000000000..af81edf349 --- /dev/null +++ b/tests/diagnosing-dotnet-aot/assets/expression-compile-hotpath.cs @@ -0,0 +1,50 @@ +// Test asset: Expression.Compile() used in a hot path with no warning +// Expected: Skill should flag the silent 10-100x perf degradation + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace MyApp; + +public class PropertyAccessorCache +{ + private readonly Dictionary> _getters = new(); + + // Called once per property — builds compiled expression accessors + public void RegisterType(Type type) + { + foreach (var prop in type.GetProperties()) + { + var param = Expression.Parameter(typeof(object)); + var cast = Expression.Convert(param, type); + var access = Expression.Property(cast, prop); + var box = Expression.Convert(access, typeof(object)); + var lambda = Expression.Lambda>(box, param); + + // In AOT: this uses the interpreter, 10-100x slower + _getters[prop.Name] = lambda.Compile(); + } + } + + // Called thousands of times per request on hot path + public object? GetValue(object obj, string propertyName) + { + return _getters[propertyName](obj); + } +} + +// Meanwhile, EF Core LINQ queries that use expressions are fine: +public class OrderRepository +{ + // This is safe — EF Core translates to SQL, never calls Compile() + public IQueryable GetExpensiveOrders(DbContext db) + { + return db.Set().Where(o => o.Total > 100).OrderBy(o => o.Date); + } +} + +public class Order { public decimal Total { get; set; } public DateTime Date { get; set; } } +public class DbContext { public IQueryable Set() where T : class => throw new NotImplementedException(); } diff --git a/tests/diagnosing-dotnet-aot/assets/isaotcompatible-cascade.cs b/tests/diagnosing-dotnet-aot/assets/isaotcompatible-cascade.cs new file mode 100644 index 0000000000..4a5cc3cd69 --- /dev/null +++ b/tests/diagnosing-dotnet-aot/assets/isaotcompatible-cascade.cs @@ -0,0 +1,19 @@ + + + + + net8.0 + true + + + true + true + true + true + + + + + diff --git a/tests/diagnosing-dotnet-aot/assets/make-generic-type-mixed.cs b/tests/diagnosing-dotnet-aot/assets/make-generic-type-mixed.cs new file mode 100644 index 0000000000..f5b69ee295 --- /dev/null +++ b/tests/diagnosing-dotnet-aot/assets/make-generic-type-mixed.cs @@ -0,0 +1,36 @@ +// Test asset: Mix of safe (reference type) and unsafe (value type) MakeGenericType calls +// Expected: Skill should distinguish between safe and unsafe usages + +using System; +using System.Collections.Generic; + +namespace MyApp; + +public class ConverterFactory +{ + // This is safe — entityType is always a class (reference type) + public object CreateRepository(Type entityType) + { + var repoType = typeof(Repository<>).MakeGenericType(entityType); + return Activator.CreateInstance(repoType)!; + } + + // This is UNSAFE — numericType could be int, float, double (value types) + public object CreateAggregator(Type numericType) + { + var aggType = typeof(Aggregator<>).MakeGenericType(numericType); + return Activator.CreateInstance(aggType)!; + } + + // This is safe — constrained to class + public IHandler CreateHandler() where T : class + { + var handlerType = typeof(DefaultHandler<>).MakeGenericType(typeof(T)); + return (IHandler)Activator.CreateInstance(handlerType)!; + } +} + +public class Repository where T : class { } +public class Aggregator where T : struct { } +public interface IHandler { } +public class DefaultHandler : IHandler where T : class { } diff --git a/tests/diagnosing-dotnet-aot/assets/pragma-warning-suppress.cs b/tests/diagnosing-dotnet-aot/assets/pragma-warning-suppress.cs new file mode 100644 index 0000000000..af9fea1a36 --- /dev/null +++ b/tests/diagnosing-dotnet-aot/assets/pragma-warning-suppress.cs @@ -0,0 +1,27 @@ +// Test asset: Developer attempts to suppress AOT warnings using #pragma +// Expected: Skill should catch that #pragma doesn't work for trim/AOT warnings + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace MyApp; + +public class PluginLoader +{ + #pragma warning disable IL2026 + public object LoadHandler(string typeName) + { + var type = Type.GetType(typeName); + return Activator.CreateInstance(type!); + } + #pragma warning restore IL2026 + + #pragma warning disable IL3050 + public object CreateGeneric(Type elementType) + { + var listType = typeof(List<>).MakeGenericType(elementType); + return Activator.CreateInstance(listType)!; + } + #pragma warning restore IL3050 +}