Csharp Dotnetcore Natasha

Other

This skill should be used when developers need to create dynamic C# features at runtime, including dynamic class generation, dynamic method creation, accessing private members through dynamic compilation, and managing compilation metadata strategies using the Natasha library.

Install

openclaw skills install csharp-dotnetcore-natasha

Natasha C# Dynamic Compilation Skill

Purpose

This skill enables developers to dynamically generate and compile C# code at runtime using the Natasha library. It supports creating dynamic classes, generating high-performance delegates, accessing private members of existing types, managing isolated compilation contexts, and precisely controlling metadata loading strategies.

When to Use This Skill

Use this skill when:

  • Dynamically creating classes with custom properties and methods at runtime
  • Generating high-performance delegates for computational code without reflection overhead
  • Accessing private fields or methods of existing types from dynamically compiled code
  • Building expression trees or code generation utilities that require runtime C# compilation
  • Creating extensible plugin systems where behavior is determined at runtime
  • Optimizing performance-critical code paths through dynamic method generation
  • Needing fine-grained control over metadata and using-code scope (lean metadata mode)

How to Use This Skill

Prerequisites

Install the required NuGet packages in your project:

# Core compiler package (基础编译单元)
dotnet add package DotNetCore.Natasha.CSharp.Compiler

# Domain implementation package (域实现包)
dotnet add package DotNetCore.Natasha.CSharp.Compiler.Domain

Note: DotNetCore.Natasha.CSharp.Compiler.Domain inherits from DotNetCore.Natasha.Domain and implements the compilation binding interface required by Natasha compiler. All packages are prefixed with DotNetCore.

Framework Support: .NET Core 3.1+, .NET 5.0+, .NET 6.0+, .NET 7.0+, .NET 8.0+


Three Compilation Modes

Mode 1: Smart Mode (智能模式) — Recommended for Most Cases

Smart mode automatically merges metadata and using-code from the default domain + current domain, with semantic checking enabled.

Key decision: Memory Assembly vs Reference Assembly

DimensionMemory AssemblyReference Assembly
Metadata coverageRuntime types onlyComplete metadata (including non-loaded assemblies)
Memory usageLowerHigher
Startup speedFasterSlower
Recommended forMost scenariosFull metadata needed, memory/size not a concern

Memory assembly initialization (内存程序集预热):

NatashaManagement
    .GetInitializer()
    .WithMemoryUsing()       // Extract using-code from in-memory assemblies
    .WithMemoryReference()   // Extract metadata from in-memory assemblies
    .Preheating<NatashaDomainCreator>();

Reference assembly initialization (引用程序集预热):

NatashaManagement
    .GetInitializer()
    .WithRefUsing()          // Extract using-code from reference assembly files
    .WithRefReference()      // Extract metadata from reference assembly files (most complete)
    .Preheating<NatashaDomainCreator>();

Rule of thumb: If the user does not care about program size or memory consumption, use WithRefUsing().WithRefReference() — reference assemblies provide the most complete metadata coverage.

Using file cache (文件缓存优化):

When the project is stable (no more new dependencies being added), enable WithFileUsingCache() to write using-code into a Natasha.Namespace.cache file. On subsequent runs, Natasha reads from the cache instead of scanning assemblies, significantly speeding up startup:

NatashaManagement
    .GetInitializer()
    .WithMemoryUsing()
    .WithMemoryReference()
    .WithFileUsingCache()    // Cache using-code to disk (use when dependencies are stable)
    .Preheating<NatashaDomainCreator>();

Usage in smart mode:

AssemblyCSharpBuilder builder = new();
builder
    .UseRandomLoadContext()
    .UseSmartMode()           // Merges current + default domain refs, enables semantic check
    .Add("public class A { public int Value { get; set; } }");

var assembly = builder.GetAssembly();

Mode 2: Simple Mode (精简模式 / 自管理元数据模式)

Use simple mode when you need to precisely control which metadata participates in compilation. No global preheating metadata is used — you add only what you need via ConfigLoadContext.

// No global Preheating() needed for simple mode
NatashaManagement.RegistDomainCreator<NatashaDomainCreator>();

AssemblyCSharpBuilder builder = new();
builder
    .UseRandomLoadContext()
    .UseSimpleMode()          // No combined references, no semantic check
    .ConfigLoadContext(ldc => ldc
        .AddReferenceAndUsingCode(typeof(Math).Assembly)    // Adds Math + all its deps
        .AddReferenceAndUsingCode(typeof(MathF))            // Adds MathF's assembly
        .AddReferenceAndUsingCode(typeof(object)))          // Adds core runtime
    .Add("public static class A { public static double Calc(double v) { return Math.Floor(v/0.3); } }");

var assembly = builder.GetAssembly();

Key points for simple mode:

  • AddReferenceAndUsingCode(typeof(T)) — loads T's assembly AND its dependency assemblies, plus extracts all using-code from them
  • AddReferenceAndUsingCode(typeof(T).Assembly) — same but takes an Assembly object
  • The using directives are automatically collected from the added assemblies in simple mode, so you don't need to write them explicitly in the script (unless using WithoutCombineUsingCode)
  • Use AppendExceptUsings("System.IO", "MyNamespace") to explicitly exclude certain using namespaces from being injected into the syntax tree

Mode 3: Custom Compilation Mode (自定义编译模式)

Use when you provide your own complete metadata set (e.g., from Basic.Reference.Assemblies NuGet package) and handle using-code yourself.

// You prepare the metadata collection yourself, e.g. from Basic.Reference.Assemblies
IEnumerable<MetadataReference> myRefs = Basic.Reference.Assemblies.Net80.References.All;

AssemblyCSharpBuilder builder = new();
builder
    .UseRandomDomain()
    .WithSpecifiedReferences(myRefs)          // Use ONLY these references (no domain refs)
    .WithoutCombineUsingCode()                // Do NOT auto-inject using-code
    .WithReleaseCompile()
    .Add("using System; using static System.Math; public static class A { public static int Test(int a, int b) { return a + b; } }");
// Note: when WithoutCombineUsingCode() is used, include using directives manually in your script

var assembly = builder.GetAssembly();

WithSpecifiedReferences makes the builder ignore both the default domain and current domain metadata, using only the explicitly provided references.


Core Workflow

  1. Initialize Natasha (once per application startup)

    • Choose smart or simple mode based on your needs
    • Reference: See references/initialization-patterns.md for all supported initialization methods
  2. Create AssemblyCSharpBuilder

    • Instantiate new AssemblyCSharpBuilder()
    • Configure load context: UseRandomLoadContext() for isolation, UseNewLoadContext("name") for persistence
    • Select compilation mode: UseSmartMode() / UseSimpleMode() / custom
    • Set compilation level: WithReleaseCompile() or WithDebugCompile()
  3. Add and Compile Code

    • Use .Add(csharpCode) to add code strings
    • Call .GetAssembly() to compile and retrieve the assembly
    • Or call .CompileWithoutAssembly() to compile without injecting into the domain
  4. Extract and Use Generated Types

    • Use GetTypeFromShortName("ClassName") to retrieve compiled types
    • Use GetDelegateFromShortName<T>("ClassName", "MethodName") for delegates

Three Core Usage Patterns

Pattern 1: Dynamic Class Generation

// Initialize (once at application startup)
NatashaManagement
    .GetInitializer()
    .WithMemoryUsing()
    .WithMemoryReference()
    .Preheating<NatashaDomainCreator>();

AssemblyCSharpBuilder builder = new();
builder
    .UseRandomLoadContext()
    .UseSmartMode();

builder.Add(@"
    public class DynamicPerson {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Greet() => $""Hello, I'm {Name}, age {Age}"";
    }
");

var assembly = builder.GetAssembly();
var personType = assembly.GetTypeFromShortName("DynamicPerson");
var instance = Activator.CreateInstance(personType);
personType.GetProperty("Name")!.SetValue(instance, "Alice");
var greeting = personType.GetMethod("Greet")!.Invoke(instance, null);

Pattern 2: Dynamic Delegate Generation

AssemblyCSharpBuilder builder = new();
builder
    .UseRandomLoadContext()
    .UseSmartMode()
    .Add(@"
        public class MathHelper {
            public static int Add(int a, int b) => a + b;
        }
    ");

var assembly = builder.GetAssembly();
var addFunc = assembly.GetDelegateFromShortName<Func<int, int, int>>("MathHelper", "Add");
int result = addFunc(3, 5);  // result: 8

Important: The delegate type parameter T must exactly match the method signature.

Pattern 3: Accessing Private Members (V9 API)

V9 introduces a cleaner API for private member access. Two key steps:

  1. Call builder.WithPrivateAccess() to enable private compilation on the builder
  2. Call script.ToAccessPrivateTree(...) to rewrite the syntax tree with access attributes
// Add IgnoresAccessChecksToAttribute.cs to your project (see references/troubleshooting.md)

AssemblyCSharpBuilder builder = new();
builder
    .UseRandomLoadContext()
    .UseSmartMode()
    .WithPrivateAccess();   // V9: enable private member access on the builder

string script = @"
    public class Accessor {
        public static int GetSecret(UserModel model) {
            return model._secret;  // private field
        }
    }
";

// Pass the target type (or namespace string, or instance) to ToAccessPrivateTree
builder.Add(script.ToAccessPrivateTree(typeof(UserModel)));
// OR: builder.Add(script.ToAccessPrivateTree("MyNamespace.Assembly", "OtherNamespace"));
// OR: builder.Add(script.ToAccessPrivateTree(instance1, instance2));

var assembly = builder.GetAssembly();
var getSecret = assembly.GetDelegateFromShortName<Func<UserModel, int>>("Accessor", "GetSecret");

Load Context Management

Different scenarios require different load context management strategies:

  • Random context (UseRandomLoadContext()): Each compilation creates a new isolated load context. Use for most cases where isolation is desired.
  • Named context (UseNewLoadContext("name")): Create a persistent named context for reuse across multiple compilations.
  • Existing context (UseExistLoadContext(context) or UseExistLoadContext(domain)): Compile in an existing context/domain, enabling cross-assembly type references.
  • Default context (UseDefaultLoadContext()): Use the default shared context (least isolation).

Reference: See references/context-management.md for detailed load context lifecycle patterns.


Advanced & V9 Features

Reuse Optimization for Repeated Compilations

When reusing a builder for multiple compilations, V9 provides reuse APIs to skip re-creating expensive objects:

builder
    .WithPreCompilationOptions()     // Reuse previous CSharpCompilationOptions (debug/release flags)
    .WithPreCompilationReferences()  // Reuse previous metadata reference set
    .WithRandomAssenblyName()        // Generate new GUID assembly name for each run
    .Add(newCode)
    .GetAssembly();

Note: Use WithoutPreCompilationOptions() (default) if you need to switch debug/release or unsafe/nullable between compilations.

Compile Without Injecting Assembly

Use CompileWithoutAssembly() when you only need the compilation result (e.g., validation, syntax check) without loading the assembly into the domain:

builder.Add("public class A {}").CompileWithoutAssembly();
// Assembly is compiled but NOT injected into the load context domain

External Exception Retrieval

V9 adds GetException() to retrieve compilation errors outside the compilation lifecycle:

var assembly = builder.GetAssembly();  // May suppress exceptions internally
var ex = builder.GetException();       // Retrieve if something went wrong
if (ex != null) { /* handle */ }

Excluding Specific Using Namespaces

Prevent certain namespaces from being auto-injected into the syntax tree:

builder.AppendExceptUsings("System.IO", "System.Net", "MyConflictingNamespace");

Forced Output Cleanup on Repeated Compilation

builder.WithForceCleanOutput();    // Delete previous output file before recompiling
// Default: WithoutForceCleanOutput() — renames old file to repeate.{guid}.oldname

Debug Compilation Levels

// Standard debug (sufficient for most debugging needs)
builder.WithDebugCompile(opt => opt.ForCore());

// Enhanced debug — more granular output including implicit conversions
builder.WithDebugPlusCompile(opt => opt.ForCore());

// Release with debug info embedded (production tracing)
builder.WithReleasePlusCompile();

Note: Before using dynamic debugging, disable [Address-level debugging] in Tools → Options → Debugging.

Compiler Options

Configure compiler behavior through ConfigCompilerOption():

builder.ConfigCompilerOption(opt => opt
    .AppendCompilerFlag(CompilerBinderFlags.IgnoreAccessibility)   // Bypass access checks
    .WithAllMetadata()                                              // Access all metadata levels
    .AppendNullableFlag(NullableContextOptions.Enable)             // Enable nullable annotations
);

Reference: See references/compiler-options.md for complete compiler configuration options.


Important Notes

Core Concepts

  • AssemblyCSharpBuilder is the main API for dynamic compilation in Natasha
  • NatashaManagement handles global initialization and compiler setup
  • Load Context is what AssemblyCSharpBuilder uses internally via UseRandomLoadContext(), UseNewLoadContext(), etc.
  • Domain (from DotNetCore.Natasha.Domain) is used separately for plugin management via new NatashaDomain(key) or DomainManagement
  • Do NOT confuse: UseNewLoadContext() (AssemblyCSharpBuilder method) ≠ new NatashaDomain(key) (plugin system)

Modern API (Recommended)

  • Always use: AssemblyCSharpBuilder with UseRandomLoadContext() / UseNewLoadContext() / UseSmartMode()
  • Initialize once: NatashaManagement.GetInitializer().WithMemoryUsing().WithMemoryReference().Preheating<NatashaDomainCreator>()
  • Deprecated: Old methods like UseRandomDomain(), UseNewDomain(), UseDefaultDomain() are marked [Obsolete] — use UseRandomLoadContext(), UseDefaultLoadContext() instead

Best Practices

  1. Initialize once at application startup: Do not reinitialize on every compilation
  2. Cache delegates: Store compiled delegates for frequently used functions to avoid recompilation
  3. Choose the right mode:
    • Smart mode + memory refs → fast startup, adequate metadata
    • Smart mode + ref assembly refs → slowest startup, most complete metadata
    • Simple mode → precise control, minimal footprint
    • Custom mode → you own everything, maximum control
  4. Use WithFileUsingCache when stable: Only enable once the project's dependencies won't change
  5. Isolate contexts: Use UseRandomLoadContext() to avoid load context pollution
  6. Handle errors: Use GetException() after compilation to catch errors gracefully

Performance Considerations

  • Compiled delegates have zero reflection overhead after compilation
  • First compilation has overhead for initializing the compilation service
  • WithPreCompilationOptions() + WithPreCompilationReferences() significantly reduce repeated compilation overhead
  • WithFileUsingCache() speeds up application restarts when dependencies are stable
  • Subsequent compilations in the same named context are faster than random context compilations

Reference Files

This skill includes the following reference materials:

  • references/initialization-patterns.md - Complete initialization method variations
  • references/context-management.md - Load context lifecycle and management patterns
  • references/compiler-options.md - Compiler configuration options and flags
  • references/migration-guide.md - Migration from deprecated Template API
  • references/common-patterns.md - Real-world usage patterns and recipes
  • references/troubleshooting.md - Common errors and solutions
  • COMPILATION_ERROR_HANDLING.md - 完整错误处理指南(推荐)
  • PRIVATE_MEMBER_ACCESS.md - 私有成员访问最佳实践
  • REPEAT_COMPILE_OPTIMIZATION.md - 重复编译优化分析

Reference these files when encountering specific scenarios or needing detailed configuration guidance.

Additional Resources


Version: 3.3 Last Updated: 2026-03-30 Author Note: V3.3: 深入源码学习,揭秘 GetAvailableCompilation() 核心流程、UsingAnalysistor 智能纠错原理、MethodCreator 内部实现、泛型 MethodInfo 缓存技巧、ALC 域加载策略、事件驱动架构等。

扩展包说明

Natasha 提供了多个官方扩展包,按需引入:

扩展包用途NuGet
DotNetCore.Natasha.CSharp.Extension.MethodCreator动态委托生成,最简洁的 ToFunc<T>() API封装了动态方法创建的简化流程
DotNetCore.Natasha.CSharp.Extension.CompileDirector编译"学习"机制,自适应优化 using code适合重复编译相似脚本
DotNetCore.Natasha.CSharp.Extension.HotReload热重载支持运行时更新代码
DotNetCore.Natasha.CSharp.Extension.Codecov代码覆盖率支持测试场景

API 设计规范

Natasha 的 API 遵循严格的命名规范,理解它们能帮助你快速找到需要的 API:

系列语义示例
With条件开关/附加值WithSmartMode(), WithPrivateAccess(), WithDebugCompile()
Set单向赋值SetAssemblyName(), SetDllFilePath()
Config组件深入配置ConfigCompilerOption(opt=>...), ConfigSyntaxOptions(opt=>...)
Use核心行为选择UseRandomLoadContext(), UseSmartMode(), UseSimpleMode()
Add添加内容Add(code), AddReferenceAndUsingCode(type)
Get获取结果GetAssembly(), GetTypeFromShortName()

提示: 如果你找不到 API,先确定你要做什么(开关?赋值?配置?),然后去找对应的 With/Set/Config 系列。