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.
Use this skill when:
Install the required NuGet packages in your project:
bash
# 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+
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 AssemblyMetadata coverageRuntime types onlyComplete metadata (including non-loaded assemblies)Memory usageLowerHigherStartup speedFasterSlowerRecommended forMost scenariosFull metadata needed, memory/size not a concern
Memory assembly initialization (内存程序集预热):
csharp
NatashaManagement
.GetInitializer()
.WithMemoryUsing() // Extract using-code from in-memory assemblies
.WithMemoryReference() // Extract metadata from in-memory assemblies
.Preheating<NatashaDomainCreator>();
Reference assembly initialization (引用程序集预热):
csharp
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:
csharp
NatashaManagement
.GetInitializer()
.WithMemoryUsing()
.WithMemoryReference()
.WithFileUsingCache() // Cache using-code to disk (use when dependencies are stable)
.Preheating<NatashaDomainCreator>();
Usage in smart mode:
csharp
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();
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.
csharp
// 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 objectusing 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)AppendExceptUsings("System.IO", "MyNamespace") to explicitly exclude certain using namespaces from being injected into the syntax treeUse when you provide your own complete metadata set (e.g., from Basic.Reference.Assemblies NuGet package) and handle using-code yourself.
csharp
// 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.
references/initialization-patterns.md for all supported initialization methodsnew AssemblyCSharpBuilder()UseRandomLoadContext() for isolation, UseNewLoadContext("name") for persistenceUseSmartMode() / UseSimpleMode() / customWithReleaseCompile() or WithDebugCompile().Add(csharpCode) to add code strings.GetAssembly() to compile and retrieve the assembly.CompileWithoutAssembly() to compile without injecting into the domainGetTypeFromShortName("ClassName") to retrieve compiled typesGetDelegateFromShortName<T>("ClassName", "MethodName") for delegatescsharp
// 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);
csharp
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.
V9 introduces a cleaner API for private member access. Two key steps:
builder.WithPrivateAccess() to enable private compilation on the builderscript.ToAccessPrivateTree(...) to rewrite the syntax tree with access attributescsharp
// 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");
Different scenarios require different load context management strategies:
UseRandomLoadContext()): Each compilation creates a new isolated load context. Use for most cases where isolation is desired.UseNewLoadContext("name")): Create a persistent named context for reuse across multiple compilations.UseExistLoadContext(context) or UseExistLoadContext(domain)): Compile in an existing context/domain, enabling cross-assembly type references.UseDefaultLoadContext()): Use the default shared context (least isolation).Reference: See references/context-management.md for detailed load context lifecycle patterns.
When reusing a builder for multiple compilations, V9 provides reuse APIs to skip re-creating expensive objects:
csharp
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.
Use CompileWithoutAssembly() when you only need the compilation result (e.g., validation, syntax check) without loading the assembly into the domain:
csharp
builder.Add("public class A {}").CompileWithoutAssembly();
// Assembly is compiled but NOT injected into the load context domain
V9 adds GetException() to retrieve compilation errors outside the compilation lifecycle:
csharp
var assembly = builder.GetAssembly(); // May suppress exceptions internally
var ex = builder.GetException(); // Retrieve if something went wrong
if (ex != null) { /* handle */ }
Prevent certain namespaces from being auto-injected into the syntax tree:
csharp
builder.AppendExceptUsings("System.IO", "System.Net", "MyConflictingNamespace");
csharp
builder.WithForceCleanOutput(); // Delete previous output file before recompiling
// Default: WithoutForceCleanOutput() — renames old file to repeate.{guid}.oldname
csharp
// 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.
Configure compiler behavior through ConfigCompilerOption():
csharp
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.
UseRandomLoadContext(), UseNewLoadContext(), etc.DotNetCore.Natasha.Domain) is used separately for plugin management via new NatashaDomain(key) or DomainManagementUseNewLoadContext() (AssemblyCSharpBuilder method) ≠ new NatashaDomain(key) (plugin system)AssemblyCSharpBuilder with UseRandomLoadContext() / UseNewLoadContext() / UseSmartMode()NatashaManagement.GetInitializer().WithMemoryUsing().WithMemoryReference().Preheating<NatashaDomainCreator>()UseRandomDomain(), UseNewDomain(), UseDefaultDomain() are marked [Obsolete] — use UseRandomLoadContext(), UseDefaultLoadContext() insteadWithFileUsingCache when stable: Only enable once the project's dependencies won't changeUseRandomLoadContext() to avoid load context pollutionGetException() after compilation to catch errors gracefullyWithPreCompilationOptions() + WithPreCompilationReferences() significantly reduce repeated compilation overheadWithFileUsingCache() speeds up application restarts when dependencies are stableThis 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.
DotNetCore.)Version: 3.3 Last Updated: 2026-03-30 Author Note: V3.3: 深入源码学习,揭秘 GetAvailableCompilation() 核心流程、UsingAnalysistor 智能纠错原理、MethodCreator 内部实现、泛型 MethodInfo 缓存技巧、ALC 域加载策略、事件驱动架构等。
Natasha 提供了多个官方扩展包,按需引入:
扩展包用途NuGetDotNetCore.Natasha.CSharp.Extension.MethodCreator动态委托生成,最简洁的 ToFunc<T>() API封装了动态方法创建的简化流程DotNetCore.Natasha.CSharp.Extension.CompileDirector编译"学习"机制,自适应优化 using code适合重复编译相似脚本DotNetCore.Natasha.CSharp.Extension.HotReload热重载支持运行时更新代码DotNetCore.Natasha.CSharp.Extension.Codecov代码覆盖率支持测试场景
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 系列。