Cập nhật: Xem thêm bài viết khác về Isolator phân tán.
Mục lục
Giới Thiệu
Cô lập code, hay còn gọi là sandbox, là một chủ đề quan trọng trong bất kỳ ngôn ngữ lập trình cấp doanh nghiệp nào: khả năng chạy code một cách an toàn mà không ảnh hưởng đến chương trình chính. Điều này từng tương đối dễ đạt được với .NET Framework, nhưng không còn như vậy kể từ khi .NET Core được giới thiệu. Tôi sẽ nói một chút về vấn đề này và cách chúng ta có thể triển khai điều gì đó tương tự.
Cô Lập Code Trong .NET
App domain tùy chỉnh đã biến mất khỏi .NET kể từ .NET Core. Chính thức, chúng bị loại bỏ vì nặng và khó bảo trì. Vẫn có lớp AppDomain và chúng ta luôn có một app domain hiện tại, có thể được truy cập từ AppDomain.CurrentDomain, nhưng phương thức AppDomain.CreateDomain giờ đây trả về một exception trên tất cả các nền tảng.
App domain rất tuyệt từ góc độ lập trình vì chúng cho phép chúng ta tạo các domain mới với quyền tùy chỉnh, sau đó có thể được sử dụng để tải và thực thi code, rồi được xóa khi không còn cần thiết.
Điều này đặt ra một câu hỏi thú vị: chúng ta có thể sử dụng gì thay thế? Có ít nhất hai lựa chọn:
- Processes: Chúng ta có thể tạo một process mới, có thể với code được sinh ra, và tương tác với nó, có thể thông qua dòng lệnh, và đầu vào/đầu ra chuẩn
- Assembly Load Contexts: Đây là cách được khuyến nghị để tải các assembly với một mức độ cô lập nào đó
Cả hai đều có một số hạn chế, cụ thể là không thể điều chỉnh chi tiết các quyền mà code của chúng ta sẽ chạy với. Đối với processes, chúng ta có thể chỉ định một tài khoản có thể có quyền giảm trên hệ thống đang chạy. Đối với assembly load contexts, điều này không dễ dàng như vậy.
Giới Thiệu Isolator
Vì vậy, để gộp tất cả lại, tôi đã tạo ra Isolator: một khung framework để chạy code .NET cô lập. Nó đang trong quá trình phát triển – và có lẽ sẽ luôn như vậy – nhưng, tôi quyết định công bố nó để có thể nhận phản hồi và hiểu liệu có sự quan tâm đến chủ đề này hay không.
Tóm lại, Isolator sẽ cho phép chạy các plugin bên trong một host. Nó sẽ có thể truyền tham số, ngoài code thực tế, đến host và trả về kết quả từ nó. Cả luồng đầu ra chuẩn và luồng lỗi chuẩn cũng sẽ được cô lập.
Các Khái Niệm
Các khái niệm chính được giới thiệu bởi Isolator là plugin, được đại diện bởi interface IPlugin:
public interface IPlugin{
object? Execute(IsolationContext context);
}
Sau đó là isolation host, interface IIsolationHost:
public interface IIsolationHost : IDisposable{
Task<PluginExecutionResult> ExecutePluginAsync<TPlugin>(TPlugin plugin, IsolationContext context, CancellationToken cancellationToken = default) where TPlugin : IPlugin, new();
}
Lớp PluginExecutionResult, chứa kết quả thực thi của plugin và các thông báo đầu ra chuẩn và lỗi chuẩn được ghi lại:
public record PluginExecutionResult(string StandardOutput, string StandardError, object? Result);
Cuối cùng là execution context, IsolationContext:
public class IsolationContext{
public Dictionary<string, object> Properties { get; set; } = [];
public string[] Arguments { get; set; } = [];
}
Context cho phép truyền các giá trị tùy ý trong một từ điển (Properties) cũng như một mảng tham số chuỗi (Arguments). Bất kỳ thay đổi nào đối với bộ sưu tập Properties được thực hiện bên trong phương thức Execute của plugin sẽ được trả về cho người gọi.
Một plugin mẫu có thể như thế này:
public class HelloWorldPlugin : IPlugin
{
public object? Execute(IsolationContext ctx)
{
System.Console.WriteLine(ctx.Properties["Greeting"]);
System.Console.WriteLine(string.Join(", ", ctx.Arguments));
ctx.Properties["ExecutedAt"] = DateTime.UtcNow;
return DateTime.UtcNow;
}
}
Các Triển Khai
Tôi đã triển khai hai isolation host cụ thể:
- ProcessIsolationHost: sử dụng một process làm host cho code plugin
- AssemblyLoadContextIsolationHost: sử dụng một assembly load context để cô lập thực thi của plugin
Chúng ta có thể sử dụng chúng như thế này:
using var host = new ProcessIsolationHost();
var plugin = new HelloWorldPlugin();
var context = new IsolationContext
{
Properties = new Dictionary<string, object>
{
["Greeting"] = "Hello, World!"
},
Arguments = ["This", "is", "a", "test"]
};
var result = await host.ExecutePluginAsync(plugin, context);
Đối với ProcessIsolationHost chúng ta cũng có thể truyền các tham số liên quan đến bảo mật:
using var host = new ProcessIsolationHost(
userName: "Joe",
password: "Black",
domain: "Movies",
loadUserProfile: true);
Tất nhiên, bạn cũng có thể sử dụng AssemblyLoadContextIsolationHost:
using var host = new AssemblyLoadContextIsolationHost();
Serialization
Tất cả các tham số trên lớp cụ thể IPlugin, tất cả những thứ được truyền trong từ điển Properties, cũng như kết quả trả về từ Execute, phải có thể được serialized bằng System.Text.Json.
Các Lựa Chọn Thay Thế
Một số lựa chọn thay thế để thực thi code .NET trong sandbox tồn tại. Tôi sẽ đề cập đến hai.
DotNetIsolator
DotNetIsolator là một dự án thử nghiệm của Steve Sanderson của Microsoft (người tạo ra Knockout.js và Blazor, trong số những thứ khác), sử dụng sandbox WebAssembly để chạy code.
AppDomainAlternative
AppDomainAlternative là một dự án của Cy Scott sử dụng cách tiếp cận tương tự remoting để thực thi code.
Lộ Trình Phát Triển
Một số tính năng nảy ra trong đầu tôi và có thể sẽ được triển khai dưới bất kỳ hình thức nào vào một ngày nào đó:
- Có thêm các tùy chọn liên quan đến bảo mật cho cả hai triển khai
- Làm cho serializer và deserializer có thể cắm được
- Có một triển khai cho WebAssembly, có thể sử dụng DotNetIsolator làm cơ sở của nó
- Thêm nhiều tùy chọn và kiểm tra hơn
Kết Luận
Như mọi khi, tôi hy vọng bạn thấy điều này hữu ích và mong muốn nghe ý kiến của bạn. Code này có sẵn trên GitHub và dưới dạng gói Nuget.



