作为一名长期深耕.NET生态的技术架构师,我最近主导了一个颇具挑战性的项目——将Codex官方提供的TypeScript SDK完整移植到C#平台。这不是简单的语法转换,而是一次涉及语言特性、生态差异和工程化规范的系统性重构。
最初接到这个任务时,团队里有人提议:"为什么不直接用NodeServices在.NET里调用TS代码?"这个方案很快被否决了,因为实际测试发现跨语言调用存在三个致命问题:
TypeScript的结构化类型系统与C#的名义类型系统差异,就像两种不同的思维模式。在TS中,只要形状匹配就是兼容的:
typescript复制interface Point { x: number; y: number }
function printPoint(p: Point) {...}
const obj = { x: 1, y: 2, z: 3 };
printPoint(obj); // 合法,因为obj包含Point所需属性
而C#需要显式声明类型关系:
csharp复制interface IPoint { int X { get; } int Y { get; } }
void PrintPoint(IPoint point) {...}
var obj = new { X = 1, Y = 2, Z = 3 };
PrintPoint(obj); // 编译错误,匿名类型未实现IPoint
我们通过以下策略解决这个问题:
dynamic+运行时检查TS的Promise链在C#中需要转换为async/await模式。特别要注意的是取消机制——TS通常用第三方库实现,而C#有原生支持:
csharp复制public async Task<Response> GetDataAsync(
Request request,
CancellationToken ct = default)
{
// 模拟TS的Promise.race([fetch(), timeout()])
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token);
try {
return await _httpClient.PostAsync(request, linkedCts.Token);
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested) {
throw new TimeoutException("Request timed out");
}
}
TS的事件监听是松散的函数回调,而C#需要强类型委托。我们设计了双重方案:
csharp复制// 强类型方案
public event EventHandler<DataReceivedEventArgs> DataReceived;
// 兼容TS风格的方案
public IDisposable On(string eventName, Action<JObject> handler)
{
return _eventAggregator.Subscribe(eventName, handler);
}
我们开发了一个代码生成工具,可以解析TS的.d.ts类型定义文件,自动生成C#接口和类。核心转换规则包括:
| TS类型 | C#对应 | 特殊处理 |
|---|---|---|
| string | string | 编码处理 |
| number | double | 精度检查 |
| boolean | bool | - |
| any | dynamic | 添加运行时检查 |
| T[] | List |
初始化容量 |
| Record<K,V> | Dictionary<K,V> | 键类型转换 |
在基准测试中,我们发现JSON序列化是性能瓶颈。通过对比测试选择了最优方案:
csharp复制// System.Text.Json vs Newtonsoft.Json 性能对比
| 操作 | 数据量 | STJ(ms) | Newtonsoft(ms) |
|-----------------|--------|---------|----------------|
| 序列化 | 1MB | 12 | 28 |
| 反序列化 | 1MB | 15 | 34 |
| 内存分配 | 1MB | 2.1MB | 4.7MB |
// 最终采用的配置
var options = new JsonSerializerOptions {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
code复制CodexSDK/
├── Core/ // 核心类型和接口
├── Clients/ // 具体客户端实现
├── Serialization/ // 序列化适配器
├── Extensions/ // DI扩展等
└── Testing/
├── Unit/ // 单元测试
└── Integration/ // 集成测试
我们配置了GitHub Actions实现自动化:
TS中常见的动态属性模式:
typescript复制const config = { timeout: 1000 };
config.retry = 3; // 运行时添加属性
在C#中的解决方案:
csharp复制// 方案1:ExpandoObject
dynamic config = new ExpandoObject();
config.Timeout = 1000;
config.Retry = 3; // 合法
// 方案2:字典包装
var config = new ConfigDictionary {
["Timeout"] = 1000
};
config["Retry"] = 3; // 通过字符串键访问
TS中的事件监听可能被多次添加:
typescript复制client.on('data', handler);
client.on('data', handler); // 重复添加
我们在C#中实现了自动去重:
csharp复制private readonly HashSet<EventHandler<DataEventArgs>> _handlers
= new HashSet<EventHandler<DataEventArgs>>();
public event EventHandler<DataEventArgs> DataReceived {
add {
if (!_handlers.Contains(value)) {
_handlers.Add(value);
_nativeClient.OnData += value;
}
}
remove {
if (_handlers.Remove(value)) {
_nativeClient.OnData -= value;
}
}
}
初始实现每次请求都新建HttpClient,导致TCP端口耗尽。优化方案:
csharp复制// 旧方案 - 反模式
public Task<Response> CallApi() {
using var client = new HttpClient(); // 每次新建
return client.GetAsync(...);
}
// 新方案 - 使用IHttpClientFactory
public class CodexHttpClient {
private readonly HttpClient _client;
public CodexHttpClient(IHttpClientFactory factory) {
_client = factory.CreateClient("codex");
}
}
对高频创建的请求对象使用对象池:
csharp复制private readonly ObjectPool<Request> _requestPool =
ObjectPool.Create<Request>();
public async Task<Response> SendRequestAsync() {
var request = _requestPool.Get();
try {
// 使用request对象...
return await SendCoreAsync(request);
}
finally {
_requestPool.Return(request);
}
}
优化后性能提升对比:
| 场景 | 原方案(ops/sec) | 对象池方案(ops/sec) | 提升 |
|---|---|---|---|
| 单请求 | 1,200 | 1,250 | 4% |
| 高并发 | 8,500 | 15,300 | 80% |
我们开发了专门的测试工具,可以并行执行TS和C#版本的相同操作,然后对比结果:
csharp复制[Fact]
public async Task GetUser_ShouldReturnSameAsTsSdk()
{
// 相同输入
var userId = Guid.NewGuid().ToString();
// 并行执行
var tsTask = TsSdk.GetUserAsync(userId);
var csTask = CsSdk.GetUserAsync(userId);
await Task.WhenAll(tsTask, csTask);
// 结果对比
var tsResult = await tsTask;
var csResult = await csTask;
tsResult.Should().BeEquivalentTo(csResult, opts => opts
.Excluding(x => x.Metadata["timestamp"]) // 忽略动态字段
.Excluding(x => x.Headers["request-id"]));
}
TS中的错误通常只有message属性,而C#异常包含丰富信息。我们实现了双向转换:
csharp复制public class CodexException : Exception
{
public string Code { get; }
public object Details { get; }
public CodexException(string code, string message, object details = null)
: base(message)
{
Code = code;
Details = details;
}
public JObject ToTsStyleError()
{
return new JObject {
["code"] = Code,
["message"] = Message,
["details"] = JToken.FromObject(Details ?? new object())
};
}
public static CodexException FromTsError(JObject error)
{
return new CodexException(
error["code"]?.ToString() ?? "unknown",
error["message"]?.ToString() ?? "Unknown error",
error["details"]
);
}
}
经过这次项目,我们提炼出跨语言SDK移植的六步法:
关键经验:
我们通过扩展方法提供TS风格的链式调用:
csharp复制// 传统C#风格
var result = await client
.WithTimeout(TimeSpan.FromSeconds(30))
.WithRetry(3)
.GetAsync("/users");
// 提供的TS风格扩展
var result = await client
.Timeout(30_000)
.Retry(3)
.Get("/users");
支持多种配置方式满足不同场景:
csharp复制// 方式1:流畅接口
var client = new CodexClient()
.UseProduction()
.WithLogger(logger);
// 方式2:配置对象
var client = new CodexClient(new CodexOptions {
Environment = Environment.Production,
Timeout = TimeSpan.FromSeconds(30)
});
// 方式3:依赖注入
services.AddCodexClient(options => {
options.Serializer = new CustomSerializer();
});
当前成果只是起点,我们规划了以下演进路径:
移植过程中最大的体会是:技术决策要服务于开发者体验。一个好的SDK应该让使用者感觉"这就是为我的平台原生设计的",而不是一个外来移植品。