朋友電腦未安裝 Visual Studio,需快速驗證幾十行 C# 程式碼。靈感來自線上編譯器:瀏覽器輸入代碼 → 伺服器動態編譯執行 → 回傳結果。本文實作一個**最小可行原型**,展示核心技術:CSharpCodeProvider、CompilerParameters、Console.WriteLine 重導向。
瀏覽器 (前端)
│ HTTP POST { code, id }
▼
ASP.NET / Web API (後端)
│ 1. 字串替換 Console.Write → Consoler.Write
│ 2. CSharpCodeProvider 編譯成記憶體組件
│ 3. 反射呼叫 Program.Main()
│ 4. 從 Consoler.Outputs[id] 取回輸出
▼
JSON 回傳 { output: [...] }
Consolerpublic static class Consoler
{
// 以 Session/Request ID 隔離多用戶輸出
public static Dictionary> Outputs { get; } = new Dictionary>();
public static void Write(string id, object value)
{
if (!Outputs.ContainsKey(id)) Outputs[id] = new List();
Outputs[id].Add(value?.ToString() ?? "");
}
public static void WriteLine(string id, object value) => Write(id, (value?.ToString() ?? "") + "\n");
public static List TakeOutput(string id)
{
if (Outputs.TryGetValue(id, out var list))
{
Outputs.Remove(id);
return list;
}
return new List();
}
}
CodeRunnerpublic class CodeRunner
{
private readonly string _frameworkVersion; // "v2.0", "v3.5", "v4.0"...
public CodeRunner(string frameworkVersion = "v4.0") => _frameworkVersion = frameworkVersion;
public List Run(string code, string requestId, params string[] extraAssemblies)
{
// 1. 註冊輸出容器
Consoler.Outputs[requestId] = new List();
// 2. 編譯參數
var cp = new CompilerParameters
{
GenerateInMemory = true, // 直接在記憶體產生組件
TreatWarningsAsErrors = false,
CompilerOptions = "/target:library /optimize",
IncludeDebugInformation = false
};
cp.ReferencedAssemblies.AddRange(new[] { "mscorlib.dll", "System.dll", "System.Core.dll" });
foreach (var asm in extraAssemblies) cp.ReferencedAssemblies.Add(asm);
// 參考自身組件 (包含 Consoler)
cp.ReferencedAssemblies.Add(typeof(Consoler).Assembly.Location);
// 3. 代碼預處理:重導向 Console 輸出
code = code.Replace("Console.WriteLine", $"Consoler.WriteLine(\"{requestId}\", ")
.Replace("Console.Write", $"Consoler.Write(\"{requestId}\", ")
.Replace("};", "});"); // 修正 Lambda 結尾 (簡易處理)
// 4. 編譯
var provider = new CSharpCodeProvider(new Dictionary { { "CompilerVersion", _frameworkVersion } });
var results = provider.CompileAssemblyFromSource(cp, code);
if (results.Errors.HasErrors)
{
foreach (CompilerError err in results.Errors)
Consoler.WriteLine(requestId, $"錯誤 {err.Line}: {err.ErrorText}");
return Consoler.TakeOutput(requestId);
}
// 5. 執行 Program.Main()
try
{
var asm = results.CompiledAssembly;
var entry = asm.GetType("Program")?.GetMethod("Main", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);
entry?.Invoke(null, null);
}
catch (Exception ex)
{
Consoler.WriteLine(requestId, $"執行期錯誤: {ex.Message}");
}
return Consoler.TakeOutput(requestId);
}
}
[ApiController]
[Route("api/[controller]")]
public class CompileController : ControllerBase
{
private readonly CodeRunner _runner = new CodeRunner("v4.0");
[HttpPost("run")]
public IActionResult Run([FromBody] CompileRequest req)
{
if (string.IsNullOrWhiteSpace(req.Code)) return BadRequest("代碼不可為空");
var id = Guid.NewGuid().ToString("N");
var output = _runner.Run(req.Code, id, req.References);
return Ok(new { output });
}
}
public class CompileRequest
{
public string Code { get; set; }
public string[] References { get; set; } = Array.Empty();
}
| 風險 | 對策 |
|---|---|
| 任意程式碼執行 (RCE) | 沙箱隔離 (Docker / gVisor / AppContainer)、限制 CPU/記憶體/網路/檔案系統 |
| 惡意無限迴圈 | 執行緒超時強制中止 (需獨立 Process 而非 AppDomain) |
| 敏感資訊洩露 | 禁止引用系統組件、過濾 Process/File/Registry 等危險 API |
| 編譯器版本相容 | Roslyn (Microsoft.CodeAnalysis.CSharp.Scripting) 取代舊版 CodeDom |
// .NET Core / .NET 5+ 推薦用法
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
var options = ScriptOptions.Default
.AddReferences("System.dll", "System.Core.dll")
.AddImports("System", "System.Linq", "System.Collections.Generic");
var result = await CSharpScript.RunAsync(@"
var list = Enumerable.Range(1, 10).ToList();
Console.WriteLine(string.Join(',', list));
", options);
Roslyn 提供**更快的編譯**、**更豐富的診斷**、**支援 C# 9/10/11 語法**,且可輕鬆實作 REPL 互動式環境。