| name | csharp-dotnetcore-natasha |
| description | 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. |
| description_zh | Natasha C# 动态编译框架 - 运行时动态生成类、委托,访问私有成员 |
| description_en | Runtime C# dynamic compilation with Natasha framework |
| license | MIT |
| version | 3.3.0 |
| category | dotnet |
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:
dotnet add package DotNetCore.Natasha.CSharp.Compiler
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
| Dimension | Memory Assembly | Reference Assembly |
|---|
| Metadata coverage | Runtime types only | Complete metadata (including non-loaded assemblies) |
| Memory usage | Lower | Higher |
| Startup speed | Faster | Slower |
| Recommended for | Most scenarios | Full metadata needed, memory/size not a concern |
Memory assembly initialization (内存程序集预热):
NatashaManagement
.GetInitializer()
.WithMemoryUsing()
.WithMemoryReference()
.Preheating<NatashaDomainCreator>();
Reference assembly initialization (引用程序集预热):
NatashaManagement
.GetInitializer()
.WithRefUsing()
.WithRefReference()
.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()
.Preheating<NatashaDomainCreator>();
Usage in smart mode:
AssemblyCSharpBuilder builder = new();
builder
.UseRandomLoadContext()
.UseSmartMode()
.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.
NatashaManagement.RegistDomainCreator<NatashaDomainCreator>();
AssemblyCSharpBuilder builder = new();
builder
.UseRandomLoadContext()
.UseSimpleMode()
.ConfigLoadContext(ldc => ldc
.AddReferenceAndUsingCode(typeof(Math).Assembly)
.AddReferenceAndUsingCode(typeof(MathF))
.AddReferenceAndUsingCode(typeof(object)))
.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.
IEnumerable<MetadataReference> myRefs = Basic.Reference.Assemblies.Net80.References.All;
AssemblyCSharpBuilder builder = new();
builder
.UseRandomDomain()
.WithSpecifiedReferences(myRefs)
.WithoutCombineUsingCode()
.WithReleaseCompile()
.Add("using System; using static System.Math; public static class A { public static int Test(int a, int b) { return a + b; } }");
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
-
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
-
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()
-
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
-
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
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);
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:
- Call
builder.WithPrivateAccess() to enable private compilation on the builder
- Call
script.ToAccessPrivateTree(...) to rewrite the syntax tree with access attributes
AssemblyCSharpBuilder builder = new();
builder
.UseRandomLoadContext()
.UseSmartMode()
.WithPrivateAccess();
string script = @"
public class Accessor {
public static int GetSecret(UserModel model) {
return model._secret; // private field
}
}
";
builder.Add(script.ToAccessPrivateTree(typeof(UserModel)));
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()
.WithPreCompilationReferences()
.WithRandomAssenblyName()
.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();
External Exception Retrieval
V9 adds GetException() to retrieve compilation errors outside the compilation lifecycle:
var assembly = builder.GetAssembly();
var ex = builder.GetException();
if (ex != null) { }
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();
Debug Compilation Levels
builder.WithDebugCompile(opt => opt.ForCore());
builder.WithDebugPlusCompile(opt => opt.ForCore());
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)
.WithAllMetadata()
.AppendNullableFlag(NullableContextOptions.Enable)
);
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
- Initialize once at application startup: Do not reinitialize on every compilation
- Cache delegates: Store compiled delegates for frequently used functions to avoid recompilation
- 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
- Use
WithFileUsingCache when stable: Only enable once the project's dependencies won't change
- Isolate contexts: Use
UseRandomLoadContext() to avoid load context pollution
- 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 系列。