- 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:
# Core compiler package (基础编译单元)
dotnet add package DotNetCore.Natasha.CSharp.Compiler
# Domain implementation package (域实现包)
dotnet add package DotNetCore.Natasha.CSharp.Compiler.DomainNote: 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() // 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 themAddReferenceAndUsingCode(typeof(T).Assembly)— same but takes an Assembly object- The
usingdirectives are automatically collected from the added assemblies in simple mode, so you don't need to write them explicitly in the script (unless usingWithoutCombineUsingCode) - 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
- 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
// 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: 8Important: 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
// 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)orUseExistLoadContext(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 domainExternal 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}.oldnameDebug 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 vianew NatashaDomain(key)orDomainManagement - Do NOT confuse:
UseNewLoadContext()(AssemblyCSharpBuilder method) ≠new NatashaDomain(key)(plugin system)
Modern API (Recommended)
- Always use:
AssemblyCSharpBuilderwithUseRandomLoadContext()/UseNewLoadContext()/UseSmartMode() - Initialize once:
NatashaManagement.GetInitializer().WithMemoryUsing().WithMemoryReference().Preheating<NatashaDomainCreator>() - Deprecated: Old methods like
UseRandomDomain(),UseNewDomain(),UseDefaultDomain()are marked[Obsolete]— useUseRandomLoadContext(),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
WithFileUsingCachewhen 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 overheadWithFileUsingCache()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 variationsreferences/context-management.md- Load context lifecycle and management patternsreferences/compiler-options.md- Compiler configuration options and flagsreferences/migration-guide.md- Migration from deprecated Template APIreferences/common-patterns.md- Real-world usage patterns and recipesreferences/troubleshooting.md- Common errors and solutionsCOMPILATION_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
- Natasha GitHub: https://github.com/dotnetcore/Natasha
- Official Documentation: https://natasha.dotnetcore.xyz/zh-Hans/docs
- NuGet Package (Compiler): https://www.nuget.org/packages/DotNetCore.Natasha.CSharp.Compiler
- NuGet Package (Domain): https://www.nuget.org/packages/DotNetCore.Natasha.CSharp.Compiler.Domain
- Source Code: G:\Project\OpenSource\Natasha (all packages prefixed with
DotNetCore.)
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 系列。