From c44275c2987a2889c5b763b68f544d3f8f4c0d2d Mon Sep 17 00:00:00 2001 From: "Mark Miller (CLR)" Date: Thu, 12 Feb 2026 11:49:04 -0800 Subject: [PATCH 1/4] first stab at an appdomain skill --- skills/appdomain-migration/SKILL.md | 227 ++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 skills/appdomain-migration/SKILL.md diff --git a/skills/appdomain-migration/SKILL.md b/skills/appdomain-migration/SKILL.md new file mode 100644 index 0000000000..e2f746d788 --- /dev/null +++ b/skills/appdomain-migration/SKILL.md @@ -0,0 +1,227 @@ +--- +name: appdomain-migration +description: Guides migration of .NET Framework AppDomain usage to modern .NET alternatives. Use when modernizing code that relies on AppDomain for plugin isolation, dynamic assembly loading, sandboxing, or configuration isolation. +--- + +# AppDomain Migration + +This skill helps an agent migrate .NET Framework code that uses `System.AppDomain` to the appropriate modern .NET (6+/8+) replacement. Because there is no single direct replacement for AppDomains, the skill identifies the usage pattern first, then applies the correct migration strategy. + +## When to Use + +- Migrating a .NET Framework project to .NET 6+ that uses `AppDomain.CreateDomain` +- Replacing `AppDomain`-based plugin or add-in hosting with `AssemblyLoadContext` +- Removing `MarshalByRefObject` cross-domain communication patterns +- Converting dynamic assembly loading that depends on AppDomain isolation +- Replacing AppDomain-based sandboxing with process-level isolation +- Resolving build errors related to removed AppDomain APIs after a target framework change + +## When Not to Use + +- The code only uses `AppDomain.CurrentDomain` for event subscriptions like `UnhandledException` or `AssemblyResolve` (these still work in modern .NET; no migration needed) +- The project will remain on .NET Framework indefinitely +- The AppDomain usage is inside a third-party library you do not control + +## Inputs + +| Input | Required | Description | +|-------|----------|-------------| +| Source project or solution | Yes | The .NET Framework project containing AppDomain usage | +| Target framework | Yes | The modern .NET version to target (e.g., `net8.0`) | +| AppDomain usage locations | Recommended | Files or classes that reference `AppDomain.CreateDomain`, `MarshalByRefObject`, or cross-domain delegates | + +## Workflow + +### Step 1: Inventory AppDomain usage + +Search the codebase for all AppDomain-related APIs: + +- `AppDomain.CreateDomain` +- `AppDomain.Unload` +- `AppDomain.CurrentDomain.Load` +- `MarshalByRefObject` subclasses +- `CrossAppDomainDelegate` +- `AppDomainSetup` +- `[Serializable]` types used for cross-domain data transfer +- `DoCallBack` invocations +- `SetData` / `GetData` for cross-domain state + +Record each usage location, the pattern it represents, and any cross-domain types involved. + +### Step 2: Classify each usage pattern + +Categorize every usage into one of the following patterns: + +| Pattern | Description | Modern replacement | +|---------|-------------|--------------------| +| **Plugin isolation** | Loading and unloading third-party assemblies in an isolated domain | `AssemblyLoadContext` with `isCollectible: true` | +| **Dynamic assembly loading** | Loading assemblies by path or name at runtime without isolation requirements | `AssemblyLoadContext.Default.LoadFromAssemblyPath` or `Assembly.LoadFrom` | +| **Sandboxing / partial trust** | Restricting permissions for untrusted code | Separate process with restricted OS-level permissions | +| **Configuration isolation** | Using per-domain config files via `AppDomainSetup.ConfigurationFile` | `IConfiguration` with per-component config sources | +| **Unloadability** | Loading code that must be unloaded to free memory or update in place | Collectible `AssemblyLoadContext` | +| **Cross-domain remoting** | Using `MarshalByRefObject` proxies to call across domains | In-process interfaces across `AssemblyLoadContext` boundaries, or out-of-process communication (named pipes, gRPC) | + +If a single AppDomain serves multiple purposes, list all patterns and address each one. + +### Step 3: Migrate plugin isolation and unloadability + +For code that creates an AppDomain to load and later unload plugins: + +1. Create a custom `AssemblyLoadContext` subclass with `isCollectible: true`: + +```csharp +public class PluginLoadContext : AssemblyLoadContext +{ + private readonly AssemblyDependencyResolver _resolver; + + public PluginLoadContext(string pluginPath) : base(isCollectible: true) + { + _resolver = new AssemblyDependencyResolver(pluginPath); + } + + protected override Assembly? Load(AssemblyName assemblyName) + { + string? assemblyPath = _resolver.ResolveAssemblyPath(assemblyName); + if (assemblyPath != null) + { + return LoadFromAssemblyPath(assemblyPath); + } + return null; + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + string? libraryPath = _resolver.ResolveUnmanagedDllPath(unmanagedDllName); + if (libraryPath != null) + { + return LoadUnmanagedDllFromPath(libraryPath); + } + return IntPtr.Zero; + } +} +``` + +2. Replace `AppDomain.CreateDomain` + `CreateInstanceAndUnwrap` with loading into the custom context: + +```csharp +var context = new PluginLoadContext(pluginPath); +var assembly = context.LoadFromAssemblyPath(pluginPath); +var type = assembly.GetType("MyPlugin.PluginClass"); +var instance = Activator.CreateInstance(type!); +``` + +3. Replace `AppDomain.Unload(domain)` with unloading the context: + +```csharp +context.Unload(); +``` + +4. Ensure no references to types loaded in the context are held after `Unload()`, otherwise the context will not be garbage collected. Use `WeakReference` to verify collectibility during testing. + +### Step 4: Migrate MarshalByRefObject cross-domain communication + +`MarshalByRefObject` has no equivalent across `AssemblyLoadContext` boundaries. Choose a replacement based on isolation needs: + +**If the code stays in-process (same `AssemblyLoadContext` boundary):** + +1. Define a shared interface in an assembly loaded by the default context. +2. Have the plugin implement that interface. +3. Cast the loaded type to the shared interface instead of using `MarshalByRefObject` proxies. + +```csharp +// Shared contract (loaded in default context) +public interface IPlugin +{ + string Execute(string input); +} + +// In plugin (loaded in PluginLoadContext) +public class MyPlugin : IPlugin +{ + public string Execute(string input) => $"Processed: {input}"; +} + +// Host code +var instance = (IPlugin)Activator.CreateInstance(type!); +var result = instance.Execute("hello"); +``` + +**If strong isolation is required (untrusted code):** + +1. Move the plugin to a separate process. +2. Communicate via named pipes, gRPC, or another IPC mechanism. +3. The host process starts and manages the plugin process lifecycle. + +### Step 5: Migrate sandboxing and partial trust + +Modern .NET does not support Code Access Security (CAS) or partial trust. Replace with: + +1. Run untrusted code in a **separate process** with restricted OS permissions. +2. On Windows, use Job Objects or a restricted user account. +3. On Linux, use containers, seccomp, or AppArmor profiles. +4. Communicate with the sandboxed process via IPC (named pipes, gRPC, Unix domain sockets). + +Remove all `PermissionSet`, `SecurityPermission`, and `AppDomain.SetAppDomainPolicy` calls — they have no effect in modern .NET. + +### Step 6: Migrate configuration isolation + +Replace `AppDomainSetup.ConfigurationFile` with the modern configuration system: + +1. Add `Microsoft.Extensions.Configuration` packages if not already present. +2. Create a per-component `IConfiguration` instance: + +```csharp +var config = new ConfigurationBuilder() + .SetBasePath(pluginDirectory) + .AddJsonFile("pluginsettings.json", optional: true) + .Build(); +``` + +3. Remove `AppDomainSetup` configuration and any `ConfigurationManager` calls that relied on per-domain config files. + +### Step 7: Clean up removed APIs + +After migrating all patterns, remove or replace any remaining references: + +| Removed API | Replacement | +|-------------|-------------| +| `AppDomain.CreateDomain` | `new PluginLoadContext(path)` | +| `AppDomain.Unload` | `AssemblyLoadContext.Unload()` | +| `AppDomain.CurrentDomain.Load(byte[])` | `AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(bytes))` | +| `AppDomain.SetData` / `GetData` | Static state, dependency injection, or `AsyncLocal` | +| `AppDomain.DoCallBack` | Direct method call or IPC | +| `MarshalByRefObject` | Shared interface or IPC | +| `[Serializable]` for cross-domain transfer | Shared types in a common assembly, or DTO serialization over IPC | + +### Step 8: Verify the migration + +1. Build the project targeting the new framework. Confirm zero `AppDomain`-related compile errors. +2. Run existing tests. If tests created AppDomains for isolation, update them to use `AssemblyLoadContext` or `[assembly: CollectibleContext]`. +3. For unloadability scenarios, add a test that: + - Loads an assembly into a collectible `AssemblyLoadContext` + - Unloads the context + - Uses a `WeakReference` to confirm the context was garbage collected +4. For plugin scenarios, verify that plugins load, execute, and unload without memory leaks. +5. Search the codebase for any remaining references to `AppDomain.CreateDomain`, `MarshalByRefObject`, or `CrossAppDomainDelegate`. + +## Validation + +- [ ] No references to `AppDomain.CreateDomain` remain in the migrated code +- [ ] No `MarshalByRefObject` subclasses remain (unless still targeting .NET Framework in a multi-target build) +- [ ] Project builds cleanly against the target framework with no AppDomain-related errors +- [ ] Plugin load/unload scenarios work correctly with `AssemblyLoadContext` +- [ ] If collectible contexts are used, a `WeakReference` test confirms unloading works +- [ ] Cross-domain communication replaced with shared interfaces or IPC +- [ ] No CAS or partial trust APIs remain (`PermissionSet`, `SecurityPermission`, etc.) +- [ ] Existing tests pass or have been updated for the new isolation model + +## Common Pitfalls + +| Pitfall | Solution | +|---------|----------| +| Holding references to types from an unloaded context prevents GC | Ensure all references (including event handlers) are released before calling `Unload()`. Use `WeakReference` to verify. | +| Shared types loaded in both default and plugin contexts cause `InvalidCastException` | Load shared interface assemblies only in the default context. Use `AssemblyDependencyResolver` and override `Load` to return `null` for shared assemblies so they fall through to the default context. | +| Assuming `AssemblyLoadContext` provides security isolation | It does not. `AssemblyLoadContext` provides assembly isolation, not permission isolation. Use process boundaries for security. | +| Removing `[Serializable]` from types still used by other serializers | Only remove `[Serializable]` if it was solely for cross-AppDomain marshaling. Check for `BinaryFormatter`, remoting, or other serialization usage first. | +| Using `Assembly.LoadFrom` instead of `AssemblyLoadContext.LoadFromAssemblyPath` | `LoadFrom` loads into the default context and does not provide isolation. Always use a custom `AssemblyLoadContext` when isolation is needed. | +| Forgetting to handle unmanaged (native) DLL loading in the plugin context | Override `LoadUnmanagedDll` in your custom `AssemblyLoadContext` to resolve native dependencies from the plugin directory. | From ba8cf59da9ca9f8f8095d722acf14091dacba738 Mon Sep 17 00:00:00 2001 From: "Mark Miller (CLR)" Date: Thu, 12 Feb 2026 11:51:25 -0800 Subject: [PATCH 2/4] Fixing hallucination --- skills/appdomain-migration/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/appdomain-migration/SKILL.md b/skills/appdomain-migration/SKILL.md index e2f746d788..6b2f1cee36 100644 --- a/skills/appdomain-migration/SKILL.md +++ b/skills/appdomain-migration/SKILL.md @@ -196,7 +196,7 @@ After migrating all patterns, remove or replace any remaining references: ### Step 8: Verify the migration 1. Build the project targeting the new framework. Confirm zero `AppDomain`-related compile errors. -2. Run existing tests. If tests created AppDomains for isolation, update them to use `AssemblyLoadContext` or `[assembly: CollectibleContext]`. +2. Run existing tests. If tests created AppDomains for isolation, update them to use a custom `AssemblyLoadContext` with `isCollectible: true`. 3. For unloadability scenarios, add a test that: - Loads an assembly into a collectible `AssemblyLoadContext` - Unloads the context From f4dce29937c7954b4ab4b7400753f256cb0a7d3f Mon Sep 17 00:00:00 2001 From: "Mark Miller (CLR)" Date: Thu, 12 Feb 2026 15:00:25 -0800 Subject: [PATCH 3/4] Changes after some value evaluation experiments. --- skills/appdomain-migration/SKILL.md | 127 +++------------------------- 1 file changed, 10 insertions(+), 117 deletions(-) diff --git a/skills/appdomain-migration/SKILL.md b/skills/appdomain-migration/SKILL.md index 6b2f1cee36..c199ffba2f 100644 --- a/skills/appdomain-migration/SKILL.md +++ b/skills/appdomain-migration/SKILL.md @@ -18,7 +18,7 @@ This skill helps an agent migrate .NET Framework code that uses `System.AppDomai ## When Not to Use -- The code only uses `AppDomain.CurrentDomain` for event subscriptions like `UnhandledException` or `AssemblyResolve` (these still work in modern .NET; no migration needed) +- The code only uses `AppDomain.CurrentDomain` for event subscriptions like `UnhandledException`, `AssemblyResolve`, or `ProcessExit`. These events still work in modern .NET and do not require migration. Do not refactor them to use `AssemblyLoadContext` or other replacements. - The project will remain on .NET Framework indefinitely - The AppDomain usage is inside a third-party library you do not control @@ -61,125 +61,18 @@ Categorize every usage into one of the following patterns: | **Unloadability** | Loading code that must be unloaded to free memory or update in place | Collectible `AssemblyLoadContext` | | **Cross-domain remoting** | Using `MarshalByRefObject` proxies to call across domains | In-process interfaces across `AssemblyLoadContext` boundaries, or out-of-process communication (named pipes, gRPC) | -If a single AppDomain serves multiple purposes, list all patterns and address each one. +**Critical:** If a single AppDomain serves multiple purposes, list every pattern separately and address each one. Do not pick a single replacement strategy for a multi-pattern AppDomain. -### Step 3: Migrate plugin isolation and unloadability +### Step 3: Apply the replacement for each pattern -For code that creates an AppDomain to load and later unload plugins: +Use the modern replacement from the table above. Key implementation notes per pattern: -1. Create a custom `AssemblyLoadContext` subclass with `isCollectible: true`: +- **Plugin isolation / unloadability**: Create a custom `AssemblyLoadContext` subclass with `isCollectible: true`. Use `AssemblyDependencyResolver` in the `Load` override. Override `LoadUnmanagedDll` to resolve native dependencies from the plugin directory. After calling `Unload()`, release all references to types from that context and use `WeakReference` to verify the context is garbage collected. +- **MarshalByRefObject / cross-domain remoting**: Define a shared interface in an assembly loaded by the default context. The plugin implements that interface and the host casts to it. If strong security isolation is needed, use a separate process with IPC instead. +- **Sandboxing / partial trust**: Modern .NET does not support CAS or partial trust. Remove all `PermissionSet`, `SecurityPermission`, and `AppDomain.SetAppDomainPolicy` calls. Replace with a separate process running under restricted OS permissions (Windows: Job Objects or restricted user account; Linux: containers, seccomp, or AppArmor). +- **Configuration isolation**: Replace `AppDomainSetup.ConfigurationFile` with `Microsoft.Extensions.Configuration`. Create a per-component `IConfiguration` instance using `ConfigurationBuilder`. Remove `AppDomainSetup` and any `ConfigurationManager` calls that relied on per-domain config files. -```csharp -public class PluginLoadContext : AssemblyLoadContext -{ - private readonly AssemblyDependencyResolver _resolver; - - public PluginLoadContext(string pluginPath) : base(isCollectible: true) - { - _resolver = new AssemblyDependencyResolver(pluginPath); - } - - protected override Assembly? Load(AssemblyName assemblyName) - { - string? assemblyPath = _resolver.ResolveAssemblyPath(assemblyName); - if (assemblyPath != null) - { - return LoadFromAssemblyPath(assemblyPath); - } - return null; - } - - protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) - { - string? libraryPath = _resolver.ResolveUnmanagedDllPath(unmanagedDllName); - if (libraryPath != null) - { - return LoadUnmanagedDllFromPath(libraryPath); - } - return IntPtr.Zero; - } -} -``` - -2. Replace `AppDomain.CreateDomain` + `CreateInstanceAndUnwrap` with loading into the custom context: - -```csharp -var context = new PluginLoadContext(pluginPath); -var assembly = context.LoadFromAssemblyPath(pluginPath); -var type = assembly.GetType("MyPlugin.PluginClass"); -var instance = Activator.CreateInstance(type!); -``` - -3. Replace `AppDomain.Unload(domain)` with unloading the context: - -```csharp -context.Unload(); -``` - -4. Ensure no references to types loaded in the context are held after `Unload()`, otherwise the context will not be garbage collected. Use `WeakReference` to verify collectibility during testing. - -### Step 4: Migrate MarshalByRefObject cross-domain communication - -`MarshalByRefObject` has no equivalent across `AssemblyLoadContext` boundaries. Choose a replacement based on isolation needs: - -**If the code stays in-process (same `AssemblyLoadContext` boundary):** - -1. Define a shared interface in an assembly loaded by the default context. -2. Have the plugin implement that interface. -3. Cast the loaded type to the shared interface instead of using `MarshalByRefObject` proxies. - -```csharp -// Shared contract (loaded in default context) -public interface IPlugin -{ - string Execute(string input); -} - -// In plugin (loaded in PluginLoadContext) -public class MyPlugin : IPlugin -{ - public string Execute(string input) => $"Processed: {input}"; -} - -// Host code -var instance = (IPlugin)Activator.CreateInstance(type!); -var result = instance.Execute("hello"); -``` - -**If strong isolation is required (untrusted code):** - -1. Move the plugin to a separate process. -2. Communicate via named pipes, gRPC, or another IPC mechanism. -3. The host process starts and manages the plugin process lifecycle. - -### Step 5: Migrate sandboxing and partial trust - -Modern .NET does not support Code Access Security (CAS) or partial trust. Replace with: - -1. Run untrusted code in a **separate process** with restricted OS permissions. -2. On Windows, use Job Objects or a restricted user account. -3. On Linux, use containers, seccomp, or AppArmor profiles. -4. Communicate with the sandboxed process via IPC (named pipes, gRPC, Unix domain sockets). - -Remove all `PermissionSet`, `SecurityPermission`, and `AppDomain.SetAppDomainPolicy` calls — they have no effect in modern .NET. - -### Step 6: Migrate configuration isolation - -Replace `AppDomainSetup.ConfigurationFile` with the modern configuration system: - -1. Add `Microsoft.Extensions.Configuration` packages if not already present. -2. Create a per-component `IConfiguration` instance: - -```csharp -var config = new ConfigurationBuilder() - .SetBasePath(pluginDirectory) - .AddJsonFile("pluginsettings.json", optional: true) - .Build(); -``` - -3. Remove `AppDomainSetup` configuration and any `ConfigurationManager` calls that relied on per-domain config files. - -### Step 7: Clean up removed APIs +### Step 4: Clean up removed APIs After migrating all patterns, remove or replace any remaining references: @@ -193,7 +86,7 @@ After migrating all patterns, remove or replace any remaining references: | `MarshalByRefObject` | Shared interface or IPC | | `[Serializable]` for cross-domain transfer | Shared types in a common assembly, or DTO serialization over IPC | -### Step 8: Verify the migration +### Step 5: Verify the migration 1. Build the project targeting the new framework. Confirm zero `AppDomain`-related compile errors. 2. Run existing tests. If tests created AppDomains for isolation, update them to use a custom `AssemblyLoadContext` with `isCollectible: true`. From 0e0ed0e4acf11e76a502e4bafe64d57530069f84 Mon Sep 17 00:00:00 2001 From: "Mark Miller (CLR)" Date: Fri, 13 Feb 2026 16:29:23 -0800 Subject: [PATCH 4/4] Some improvements to the skill after some iterations with the skill validator --- skills/appdomain-migration/SKILL.md | 4 +- skills/appdomain-migration/tests/eval.yaml | 186 +++++++++++++++++++++ 2 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 skills/appdomain-migration/tests/eval.yaml diff --git a/skills/appdomain-migration/SKILL.md b/skills/appdomain-migration/SKILL.md index c199ffba2f..4729322d3f 100644 --- a/skills/appdomain-migration/SKILL.md +++ b/skills/appdomain-migration/SKILL.md @@ -67,10 +67,12 @@ Categorize every usage into one of the following patterns: Use the modern replacement from the table above. Key implementation notes per pattern: -- **Plugin isolation / unloadability**: Create a custom `AssemblyLoadContext` subclass with `isCollectible: true`. Use `AssemblyDependencyResolver` in the `Load` override. Override `LoadUnmanagedDll` to resolve native dependencies from the plugin directory. After calling `Unload()`, release all references to types from that context and use `WeakReference` to verify the context is garbage collected. +- **Plugin isolation / unloadability**: Create a custom `AssemblyLoadContext` subclass with `isCollectible: true`. Use `AssemblyDependencyResolver` in the `Load` override. Override `LoadUnmanagedDll` to resolve native dependencies from the plugin directory. Do not use `Assembly.LoadFrom` — it loads into the default context and bypasses isolation. Always use `AssemblyLoadContext.LoadFromAssemblyPath` on the custom context. After calling `Unload()`, release all references to types from that context and use `WeakReference` to verify the context is garbage collected. - **MarshalByRefObject / cross-domain remoting**: Define a shared interface in an assembly loaded by the default context. The plugin implements that interface and the host casts to it. If strong security isolation is needed, use a separate process with IPC instead. - **Sandboxing / partial trust**: Modern .NET does not support CAS or partial trust. Remove all `PermissionSet`, `SecurityPermission`, and `AppDomain.SetAppDomainPolicy` calls. Replace with a separate process running under restricted OS permissions (Windows: Job Objects or restricted user account; Linux: containers, seccomp, or AppArmor). - **Configuration isolation**: Replace `AppDomainSetup.ConfigurationFile` with `Microsoft.Extensions.Configuration`. Create a per-component `IConfiguration` instance using `ConfigurationBuilder`. Remove `AppDomainSetup` and any `ConfigurationManager` calls that relied on per-domain config files. +- **Cross-domain state (`SetData` / `GetData`)**: Replace with explicit parameter passing, dependency injection, or `AsyncLocal` for ambient state. Do not rely on implicit shared state. +- **`DoCallBack`**: Replace with a direct method call on the loaded type. Since `AssemblyLoadContext` does not create a remoting boundary, there is no need for a callback delegate — load the assembly, instantiate the type, and invoke methods directly. ### Step 4: Clean up removed APIs diff --git a/skills/appdomain-migration/tests/eval.yaml b/skills/appdomain-migration/tests/eval.yaml new file mode 100644 index 0000000000..092c37f1ed --- /dev/null +++ b/skills/appdomain-migration/tests/eval.yaml @@ -0,0 +1,186 @@ +scenarios: + - name: "Plugin host with AppDomain isolation" + prompt: | + Migrate this .NET Framework code to .NET 8. Provide complete migration guidance with code and pitfalls. + + ```csharp + public class PluginHost + { + private AppDomain _pluginDomain; + public IPluginResult LoadAndExecute(string pluginDllPath, string inputData) + { + var setup = new AppDomainSetup + { + ApplicationBase = Path.GetDirectoryName(pluginDllPath), + ConfigurationFile = Path.Combine(Path.GetDirectoryName(pluginDllPath), "plugin.config") + }; + _pluginDomain = AppDomain.CreateDomain("PluginDomain", null, setup); + var proxy = (PluginProxy)_pluginDomain.CreateInstanceAndUnwrap( + typeof(PluginProxy).Assembly.FullName, typeof(PluginProxy).FullName); + var result = proxy.Execute(pluginDllPath, inputData); + AppDomain.Unload(_pluginDomain); + return result; + } + } + + [Serializable] + public class PluginResult : IPluginResult + { + public string Output { get; set; } + public bool Success { get; set; } + } + + public class PluginProxy : MarshalByRefObject + { + public PluginResult Execute(string dllPath, string input) + { + var assembly = Assembly.LoadFrom(dllPath); + var type = assembly.GetTypes().First(t => typeof(IPlugin).IsAssignableFrom(t)); + var plugin = (IPlugin)Activator.CreateInstance(type); + return plugin.Run(input); + } + } + ``` + assertions: + - type: output_contains + value: "AssemblyLoadContext" + - type: output_contains + value: "isCollectible" + - type: output_contains + value: "LoadUnmanagedDll" + - type: exit_success + rubric: + - "Warns about InvalidCastException from shared types loaded in both default and plugin AssemblyLoadContext" + - "Mentions WeakReference to verify unloading works" + - "Cautions about removing [Serializable] only after checking for BinaryFormatter or other serializer usage" + - "Recommends using AssemblyLoadContext.LoadFromAssemblyPath instead of Assembly.LoadFrom for isolation" + - "Addresses the ConfigurationFile replacement with IConfiguration" + timeout: 120 + + - name: "Sandboxing with CAS and partial trust" + prompt: | + Migrate this .NET Framework sandboxing code to .NET 8. Provide complete migration guidance with code and pitfalls. + + ```csharp + public class SandboxRunner + { + public object RunUntrusted(string assemblyPath, string typeName, string methodName) + { + var permissions = new PermissionSet(PermissionState.None); + permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution)); + permissions.AddPermission(new FileIOPermission(FileIOPermissionAccess.Read, @"C:\SafeData")); + var setup = new AppDomainSetup { ApplicationBase = Path.GetDirectoryName(assemblyPath) }; + var domain = AppDomain.CreateDomain("Sandbox", null, setup, permissions); + domain.SetData("SharedConfig", new Dictionary { ["MaxItems"] = "100" }); + try + { + var instance = domain.CreateInstanceAndUnwrap( + Path.GetFileNameWithoutExtension(assemblyPath), typeName); + var result = instance.GetType().GetMethod(methodName).Invoke(instance, null); + return result; + } + finally { AppDomain.Unload(domain); } + } + } + ``` + assertions: + - type: output_contains + value: "process" + - type: output_not_contains + value: "AssemblyLoadContext provides security" + - type: exit_success + rubric: + - "Correctly identifies this as a sandboxing pattern, not a plugin isolation pattern" + - "States that CAS and partial trust are not supported in modern .NET" + - "Recommends separate process isolation rather than AssemblyLoadContext for security" + - "Addresses the SetData/GetData replacement with dependency injection, arguments, or AsyncLocal" + - "Does not suggest AssemblyLoadContext as a security boundary" + timeout: 120 + + - name: "Multi-pattern AppDomain decomposition" + prompt: | + Migrate this .NET Framework code to .NET 8. This code uses AppDomain for MULTIPLE purposes simultaneously. Provide complete migration guidance. + + ```csharp + public class WorkerManager + { + public void RunWorker(string workerDllPath, Dictionary config) + { + var setup = new AppDomainSetup + { + ApplicationBase = Path.GetDirectoryName(workerDllPath), + ConfigurationFile = Path.Combine(Path.GetDirectoryName(workerDllPath), "worker.exe.config") + }; + var domain = AppDomain.CreateDomain("Worker_" + Guid.NewGuid(), null, setup); + foreach (var kvp in config) + domain.SetData(kvp.Key, kvp.Value); + domain.DoCallBack(() => + { + var workerConfig = AppDomain.CurrentDomain.GetData("ConnectionString") as string; + var maxRetries = AppDomain.CurrentDomain.GetData("MaxRetries") as string; + var assembly = Assembly.LoadFrom(workerDllPath); + var workerType = assembly.GetTypes().First(t => t.Name == "Worker"); + var worker = Activator.CreateInstance(workerType); + workerType.GetMethod("Start").Invoke(worker, new object[] { workerConfig, int.Parse(maxRetries) }); + }); + AppDomain.Unload(domain); + } + } + ``` + assertions: + - type: output_contains + value: "AssemblyLoadContext" + - type: output_contains + value: "IConfiguration" + - type: exit_success + rubric: + - "Explicitly decomposes the code into multiple patterns before migrating (plugin loading, config isolation, cross-domain state, DoCallBack)" + - "Addresses DoCallBack replacement with direct method invocation" + - "Addresses SetData/GetData replacement with dependency injection or parameters" + - "Addresses ConfigurationFile replacement with IConfiguration or ConfigurationBuilder" + - "Warns about Assembly.LoadFrom loading into the default context without isolation" + timeout: 120 + + - name: "CurrentDomain event subscriptions only - should not migrate" + prompt: | + Migrate this .NET Framework code to .NET 8. Provide your migration guidance. + + ```csharp + public class AppStartup + { + public static void Initialize() + { + AppDomain.CurrentDomain.UnhandledException += (sender, args) => + { + var ex = (Exception)args.ExceptionObject; + Logger.Fatal($"Unhandled exception: {ex}"); + Environment.Exit(1); + }; + AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => + { + var name = new AssemblyName(args.Name); + var probePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "libs", name.Name + ".dll"); + if (File.Exists(probePath)) + return Assembly.LoadFrom(probePath); + return null; + }; + AppDomain.CurrentDomain.ProcessExit += (sender, args) => + { + Logger.Info("Application shutting down"); + CleanupResources(); + }; + } + } + ``` + assertions: + - type: output_contains + value: "no migration" + - type: output_not_contains + value: "AssemblyLoadContext" + - type: exit_success + rubric: + - "Correctly identifies that this code only uses AppDomain.CurrentDomain event subscriptions" + - "States that these events still work in modern .NET and do not require migration" + - "Does NOT recommend replacing AssemblyResolve with AssemblyLoadContext" + - "Does NOT over-migrate working code" + timeout: 120