專案簡介
自 Natasha v9.0 釋出起,我將基於 Natasha 的推出熱執行方案,這項技術允許基於 控制檯(Console) 和新版 Asp.net Core 架構的專案在執行中動態重編譯,在不停止工程的情況下獲取最新結果,以幫助技術初學者、專案初期開發人員等,進行快速實驗以及試錯。
為了更形象的說明 [熱執行] 請看下圖:
熱執行
以下為了更加簡潔,稱熱執行為 [HE].
圖中是 Asp.net Core 一個介面開發的案例,我更改了一個實體類的結構,並儲存,可以看到介面返回了最新的實體類結構。藉此簡單闡述一些熱執行的工作原理,檔案發生變化會觸發 [HE] 對專案進行熱編譯,開發者無論是大改還是小改,只要你的專案檔案(cs) 、依賴專案、csproj 發生了變化,[HE] 就會代理整個專案並自動編譯輸出。對於有些老機器較慢,可能 [HE] 熱編譯要比 [按下F5-程式跑起來] 要快的多。
熱過載與熱執行
也許有人會覺得這更像一個完全體的熱過載,並不是,這是與熱過載完全不同的技術,[HE] 的核心技術是語法樹重寫與動態編譯。而熱過載是對 Runtime 的程式集進行熱更新,熱過載嚴重依賴 Debugger 元件,且目前從 ENC 錯誤程式碼 來看這項技術的限制還是很大的。
起初我也是悶頭鑽研熱過載技術,但實驗效果很不理想,熱過載技術是一項前沿的,邊界明確的技術,並不適合敞開手腳快刀闊斧的幹,由於不是面對開發者,(截至2024年8月)資料也不是很多。與其死磕它,不如另闢蹊徑,藉助 Natasha 動態代理將專案管理起來。
指令簡介
註釋指令
HE 使用註釋作為熱代理指令,這些指令會影響語法樹重建以及熱編譯選項,但不影響程式的釋出和使用。目前具體如下:
-
最佳化級別
使用 //HE:Release 指令允許在 HE 重編譯時,使用 Release 模式進行編譯。
-
非同步代理
當 Main 方法中有物件 A, A 需要延遲解除安裝,A 不干擾 new A (即全域性可以不只有一個 A), 此時使用 //HE:Async 允許 HE代理 在上一次 A 物件未完全銷燬時非同步執行新 Main 方法。
-
Using 排除
由於開發可能會開啟隱式 using, 若開啟,則 HE 在代理期間,會載入所有記憶體中存在的名稱空間,因此有機率會出現 using 二義性引用問題,使用 //HE:CS0104 可以排除干擾 using,例如 //HE:CS0104 using1;using2...
-
動態表示式
如果您需要在 HE 代理期間動態的除錯輸出一些結果,且不影響程式釋出,您可以使用 //DS 或 //RS 指令輸出其後的表示式。例如 //DS 1+1
在 Debug 模式下輸出 2. //RS a.age+b.age
在 Release 輸出兩個物件年齡相加。
-
引數傳遞
void ProxyMainArguments()
方法將在代理執行之前執行,該方法允許開發者在動態開發中,在 HE 代理期間模擬 控制檯向 main 方法中傳遞引數。
虛擬碼類似於:
public static void ProxyMainArguments()
{
HEProxy.AppendArgs("123");
HEProxy.AppendArgs("引數2");
HEProxy.AppendArgs("abc");
}
main("123","引數2","abc");
注意:HE 每次建立新的代理都是一次全新的 main 執行過程,因此將清空 Args, 避免上一次代理干擾本次執行。
-
僅在程式第一次執行
使用 //Once 命令允許程式僅在程式第一次開啟時執行被其註釋的程式碼,在後續的 HE 代理期間,被註釋的語法節點將被剔除。
使用
目前該專案支援 .NET3.0 即以上版本,且 .NET5.0 版本以上有 Source Generator 技術加持。
無 SG 加持的版本
- 引入熱執行包:
DotNetCore.Natasha.CSharp.HotExecutor
class Program
{
public static void Main(string[] args)
{
//設定當前程式的型別 ,預設為 Console
HEProxy.SetProjectKind(HEProjectKind.Console);
//HE 代理週期日誌(如果不需要 HE 寫入日誌,這句就不用寫了)
string debugFilePath = Path.Combine(VSCSProjectInfoHelper.HEOutputPath, "Debug.txt");
HEFileLogger logger = new HEFileLogger(debugFilePath);
//設定資訊輸出方式,該方法影響 DS/RS 指令的輸出方式
//預設是 Console.WriteLine 方式輸出
HEProxy.ShowMessage = async msg => {
//一些專案可能禁用控制檯,那就用日誌輸出 HE 資訊
await logger.WriteUtf8FileAsync(msg);
};
//編譯初始化選項,主要是 Natasha 的初始化操作.
//Once (熱編譯時使用 Once 剔除被註釋的語句)
HEProxy.SetCompileInitAction(() => {
{
NatashaManagement.RegistDomainCreator<NatashaDomainCreator>();
NatashaManagement.Preheating((asmName, @namespace) =>
!string.IsNullOrWhiteSpace(@namespace) &&
(HEProxy.IsExcluded(@namespace)),
true,
true);
}
});
//開始執行動態代理.
//Once (熱編譯時使用 Once 剔除被註釋的語句)
HEProxy.Run();
for (int i = 0; i < args.Length; i++)
{
Console.WriteLine(args[i]);
//在 HE 代理期間輸出 args 值
//DS args[i]
}
//while 阻塞時需要指定 CancelToken ,熱執行時 HE 將取消迴圈操作。
CancellationTokenSource source = new CancellationTokenSource();
//新增到 HE 中,以便下個編譯時釋放
source.ToHotExecutor();
while (!source.IsCancellationRequested)
{
Thread.Sleep(1000);
//在 HE 代理期間輸出 "In while loop!"
//DS "In while loop!"
}
for (int i = 0; i < args.Length; i++)
{
Console.WriteLine(args[i]);
}
//防止 while 退出後直接關閉主執行緒
//Once (這句 `//Once` 可以不寫,HE 有針對 “Console.Read” 的末尾阻塞檢測)
Console.ReadKey();
}
//方法體中的引數操作對應 Main(string[] args) 中的 args,
//熱執行時,Main 將接收到 "引數11",“引數2”,“引數23”
//非必要,可以不寫
public static void ProxyMainArguments()
{
HEProxy.AppendArgs("引數11");
HEProxy.AppendArgs("引數2");
HEProxy.AppendArgs("引數23");
}
}
這段程式碼是 HE 最原始的程式碼。
SG 加持(.NET5.0及以上版本)
SG 主要是減少了 HE 初始化的一些操作。而一些需要手動傳遞的 cancel/dispose 例項仍然需要手動傳遞給 HE。
簡單案例
- 引入 SG 包:
DotNetCore.Natasha.CSharp.HotExecutor.Wrapper
internal class Program
{
static void Main(string[] args)
{
for (int i = 0; i < args.Length; i++)
{
//DS args[i]
}
//這裡仍然需要手動將 canceltoken 傳遞給 HE
CancellationTokenSource source = new();
source.ToHotExecutor();
while (!source.IsCancellationRequested)
{
Thread.Sleep(1000);
//DS "In while loop!"
}
//防止 while 退出後直接關閉主執行緒
Console.ReadKey();
}
public static void ProxyMainArguments()
{
HEProxy.AppendArgs("引數1");
HEProxy.AppendArgs("引數2");
HEProxy.AppendArgs("引數3");
HEProxy.AppendArgs("引數4");
}
}
代理新 Asp.net Core
HE 目前不能代理 MVC 專案和老版的 API 專案。
public class Program
{
public static void Main(string[] args)
{
//HE:Async
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
//將 APP 新增到 HE 中,以便在下一次編譯中釋放該物件。
app.AsyncToHotExecutor();
//更改以下的值,儲存檔案,會觸發 HE 建立新的 WebApplicationBuilder
var summaries = new[]
{
"Freezing441", "Bracing"
};
app.MapGet("/weatherforecast", (HttpContext httpContext) =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
})
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.Run();
}
}
其他專案支援
截至目前而言, HE 對 Winform 的支援不是很好,WPF 的很難代理,時間和精力有限,不會深入去研究了。
鳴謝
感謝 九哥 的支援。
結尾
遇到問題可以到 Natasha Issue 區 提出反饋。