如何用Roslyn解放双手,让HTTP客户端开发变得如此简单?

摘要:在现代 .NET 开发中,源代码生成器(Source Generators)是一项强大的功能,它允许开发者在编译时自动生成代码,从而减少样板代码的编写,提高开发效率和代码质量。本文主要介绍使用Roslyn实现两个代码生成器:HttpClie
在现代 .NET 开发中,源代码生成器(Source Generators)是一项强大的功能,它允许开发者在编译时自动生成代码,从而减少样板代码的编写,提高开发效率和代码质量。本文主要介绍使用Roslyn实现两个代码生成器:HttpClientApiSourceGenerator 和 HttpClientApiRegisterSourceGenerator,这两个生成器专门用于简化 HTTP 客户端的开发和配置。 什么是 Mud 代码生成器? Mud 代码生成器是一个基于 Roslyn 的源代码生成器,用于自动生成数据实体、服务层相关代码,提高开发效率。服务层代码生成包含以下主要功能: 服务类代码生成 - 根据实体类自动生成服务接口和服务实现类 依赖注入代码生成 - 自动为类生成构造函数注入代码,包括日志、缓存、用户管理等常用服务 服务注册代码生成 - 自动生成服务注册扩展方法,简化依赖注入配置 HttpClient API 代码生成 - 自动为标记了 HTTP 方法特性的接口生成 HttpClient 实现类 HttpClient API 源生成器详解 核心功能 HttpClientApiSourceGenerator 是一个专门用于生成 HttpClient 实现类的源代码生成器。它基于 Roslyn 技术,能够自动为标记了 [HttpClientApi] 特性的接口生成完整的 HttpClient 实现类,支持 RESTful API 调用。 工作原理 源生成器的工作流程如下: 扫描项目中的接口,查找标记了 [HttpClientApi] 特性的接口 分析接口中定义的方法和参数 根据 HTTP 方法特性(如 [Get], [Post], [Put] 等)生成相应的实现代码 处理各种参数特性(如 [Path], [Query], [Body], [Header]) 生成完整的 HttpClient 实现类,包括构造函数、日志记录、错误处理等 使用示例 让我们通过一个具体的示例来了解如何使用这个生成器: [HttpClientApi] public interface IDingTalkApi { [Get("/api/v1/user/{id}")] Task<UserDto> GetUserAsync([Query] string id); [Post("/api/v1/user")] Task<UserDto> CreateUserAsync([Body] UserDto user); [Put("/api/v1/user/{id}")] Task<UserDto> UpdateUserAsync([Path] string id, [Body] UserDto user); [Delete("/api/v1/user/{id}")] Task<bool> DeleteUserAsync([Path] string id); } 当项目编译时,HttpClientApiSourceGenerator 会自动生成一个实现该接口的类,大致如下: // 自动生成的代码 public partial class DingTalkApi : IDingTalkApi { private readonly HttpClient _httpClient; private readonly ILogger<DingTalkApi> _logger; private readonly JsonSerializerOptions _jsonSerializerOptions; public DingTalkApi(HttpClient httpClient, ILogger<DingTalkApi> logger) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false, PropertyNameCaseInsensitive = true }; } public async Task<UserDto> GetUserAsync(string id) { // 自动生成的 HTTP GET 请求逻辑 _logger.LogDebug("开始HTTP GET请求: {Url}", "/api/v1/user/{id}"); var url = $"/api/v1/user/{id}"; using var request = new HttpRequestMessage(HttpMethod.Get, url); // 处理查询参数 var queryParams = new List<string>(); if (id != null) queryParams.Add($"id={id}"); if (queryParams.Any()) url += "?" + string.Join("&", queryParams); // 发送请求并处理响应 // ... 完整的请求处理逻辑 } } 支持的 HTTP 方法 该生成器支持所有标准的 HTTP 方法: [HttpClientApi] public interface IExampleApi { [Get("/api/resource/{id}")] Task<ResourceDto> GetResourceAsync([Path] string id); [Post("/api/resource")] Task<ResourceDto> CreateResourceAsync([Body] ResourceDto resource); [Put("/api/resource/{id}")] Task<ResourceDto> UpdateResourceAsync([Path] string id, [Body] ResourceDto resource); [Delete("/api/resource/{id}")] Task<bool> DeleteResourceAsync([Path] string id); [Patch("/api/resource/{id}")] Task<ResourceDto> PatchResourceAsync([Path] string id, [Body] object patchData); [Head("/api/resource/{id}")] Task<bool> CheckResourceExistsAsync([Path] string id); [Options("/api/resource")] Task<HttpResponseMessage> GetResourceOptionsAsync(); } 参数特性详解 生成器支持多种参数特性,以处理不同的 HTTP 请求参数: Path 参数特性 用于替换 URL 模板中的路径参数: [Get("/api/users/{userId}/orders/{orderId}")] Task<OrderDto> GetOrderAsync([Path] string userId, [Path] string orderId); Query 参数特性 用于生成查询字符串参数: [Get("/api/users")] Task<List<UserDto>> GetUsersAsync( [Query] string name, [Query] int? page, [Query] int? pageSize); Body 参数特性 用于设置请求体内容: [Post("/api/users")] Task<UserDto> CreateUserAsync([Body] UserDto user); // 支持自定义内容类型 [Post("/api/users")] Task<UserDto> CreateUserAsync([Body(ContentType = "application/xml")] UserDto user); // 支持字符串内容 [Post("/api/logs")] Task LogMessageAsync([Body(UseStringContent = true)] string message); Header 参数特性 用于设置请求头: [Get("/api/protected")] Task<ProtectedData> GetProtectedDataAsync([Header] string authorization); // 自定义头名称 [Get("/api/protected")] Task<ProtectedData> GetProtectedDataAsync([Header("X-API-Key")] string apiKey); 复杂参数处理 生成器还能处理复杂的参数类型: 复杂查询参数 支持复杂对象作为查询参数,自动展开为键值对: [Get("/api/search")] Task<List<UserDto>> SearchUsersAsync([Query] UserSearchCriteria criteria); public class UserSearchCriteria { public string Name { get; set; } public int? Age { get; set; } public string Department { get; set; } } // 生成的查询字符串:?Name=John&Age=30&Department=IT 路径参数自动替换 自动处理 URL 模板中的路径参数: [Get("/api/users/{userId}/orders/{orderId}/items/{itemId}")] Task<OrderItemDto> GetOrderItemAsync( [Path] string userId, [Path] string orderId, [Path] string itemId); // 自动替换:/api/users/123/orders/456/items/789 错误处理与日志记录 生成的代码包含完整的错误处理和日志记录: public async Task<UserDto> GetUserAsync(string id) { try { _logger.LogDebug("开始HTTP GET请求: {Url}", "/api/v1/user/{id}"); // 请求处理逻辑 using var response = await _httpClient.SendAsync(request); var responseContent = await response.Content.ReadAsStringAsync(); _logger.LogDebug("HTTP请求完成: {StatusCode}, 响应长度: {ContentLength}", (int)response.StatusCode, responseContent?.Length ?? 0); if (!response.IsSuccessStatusCode) { _logger.LogError("HTTP请求失败: {StatusCode}, 响应: {Response}", (int)response.StatusCode, responseContent); throw new HttpRequestException($"HTTP请求失败: {(int)response.StatusCode} - {response.ReasonPhrase}"); } // 响应处理逻辑 } catch (Exception ex) { _logger.LogError(ex, "HTTP请求异常: {Url}", url); throw; } } HttpClient API 注册源生成器详解 核心功能 HttpClientApiRegisterSourceGenerator 是另一个重要的组件,它自动为标记了 [HttpClientApi] 特性的接口生成依赖注入注册代码,简化 HttpClient 服务的配置。 工作原理 该生成器的工作流程如下: 扫描项目中的接口,查找标记了 [HttpClientApi] 特性的接口 提取特性中的配置参数(如 BaseUrl、Timeout 等) 生成用于依赖注入的扩展方法 自动注册接口和实现类到服务容器中 使用示例 首先定义 HTTP API 接口: [HttpClientApi("https://api.dingtalk.com", Timeout = 30)] public interface IDingTalkApi { [Get("/api/v1/user/{id}")] Task<UserDto> GetUserAsync([Query] string id); [Post("/api/v1/user")] Task<UserDto> CreateUserAsync([Body] UserDto user); } [HttpClientApi("https://api.wechat.com", Timeout = 60)] public interface IWeChatApi { [Get("/api/v1/user/{id}")] Task<UserDto> GetUserAsync([Query] string id); } 生成器会自动生成以下注册代码: // 自动生成的代码 - HttpClientApiExtensions.g.cs using System; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.DependencyInjection { public static class HttpClientApiExtensions { public static IServiceCollection AddWebApiHttpClient(this IServiceCollection services) { services.AddHttpClient<global::YourNamespace.IDingTalkApi, global::YourNamespace.DingTalkApi>(client => { client.BaseAddress = new Uri("https://api.dingtalk.com"); client.Timeout = TimeSpan.FromSeconds(30); }); services.AddHttpClient<global::YourNamespace.IWeChatApi, global::YourNamespace.WeChatApi>(client => { client.BaseAddress = new Uri("https://api.wechat.com"); client.Timeout = TimeSpan.FromSeconds(60); }); return services; } } } 配置选项 HttpClientApi 特性参数 // 基本配置 [HttpClientApi("https://api.example.com")] public interface IExampleApi { } // 配置超时时间 [HttpClientApi("https://api.example.com", Timeout = 120)] public interface IExampleApi { } // 使用命名参数 [HttpClientApi(BaseUrl = "https://api.example.com", Timeout = 60)] public interface IExampleApi { } 使用方式 在应用程序启动时调用 // 在 Program.cs 或 Startup.cs 中 var builder = WebApplication.CreateBuilder(args); // 自动注册所有 HttpClient API 服务 builder.Services.AddWebApiHttpClient(); // 或者与其他服务注册一起使用 builder.Services .AddControllers() .AddWebApiHttpClient(); 在控制台应用程序中使用 // 在控制台应用程序中 var services = new ServiceCollection(); // 注册 HttpClient API 服务 services.AddWebApiHttpClient(); var serviceProvider = services.BuildServiceProvider(); var dingTalkApi = serviceProvider.GetRequiredService<IDingTalkApi>(); 两个生成器的协同工作 HttpClientApiRegisterSourceGenerator 与 HttpClientApiSourceGenerator 完美配合,提供完整的开发体验: HttpClientApiSourceGenerator 生成接口的实现类 HttpClientApiRegisterSourceGenerator 生成依赖注入注册代码 完整的开发体验:定义接口 → 自动生成实现 → 自动注册服务 完整示例 // 1. 定义接口 [HttpClientApi("https://api.dingtalk.com", Timeout = 30)] public interface IDingTalkApi { [Get("/api/v1/user/{id}")] Task<UserDto> GetUserAsync([Query] string id); } // 2. 自动生成实现类 (由 HttpClientApiSourceGenerator 生成) // public partial class DingTalkApi : IDingTalkApi { ... } // 3. 自动生成注册代码 (由 HttpClientApiRegisterSourceGenerator 生成) // public static class HttpClientApiExtensions { ... } // 4. 在应用程序中使用 var builder = WebApplication.CreateBuilder(args); builder.Services.AddWebApiHttpClient(); // 自动注册 var app = builder.Build(); // 5. 在服务中注入使用 public class UserService { private readonly IDingTalkApi _dingTalkApi; public UserService(IDingTalkApi dingTalkApi) { _dingTalkApi = dingTalkApi; } public async Task<UserDto> GetUserAsync(string userId) { return await _dingTalkApi.GetUserAsync(userId); } } 高级配置 自定义 HttpClient 配置 如果需要更复杂的 HttpClient 配置,可以在注册后继续配置: builder.Services.AddWebApiHttpClient() .ConfigureHttpClientDefaults(httpClient => { httpClient.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { UseProxy = false, AllowAutoRedirect = false }); }); 添加自定义请求头 builder.Services.AddHttpClient<IDingTalkApi, DingTalkApi>(client => { client.BaseAddress = new Uri("https://api.dingtalk.com"); client.Timeout = TimeSpan.FromSeconds(30); client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0"); client.DefaultRequestHeaders.Add("X-API-Key", "your-api-key"); }); 生成的代码结构 obj/Debug/net8.0/generated/ ├── Mud.ServiceCodeGenerator/ ├── HttpClientApiSourceGenerator/ │ └── YourNamespace.DingTalkApi.g.cs └── HttpClientApiRegisterSourceGenerator/ └── HttpClientApiExtensions.g.cs 最佳实践 统一配置:在 [HttpClientApi] 特性中统一配置所有 API 的基础设置 合理超时:根据 API 的响应时间设置合理的超时时间 命名规范:遵循接口命名规范(I{ServiceName}Api) 错误处理:在服务层处理 API 调用异常 日志记录:利用生成的日志记录功能监控 API 调用 如何查看生成的代码 要查看生成的代码,可以在项目文件中添加以下配置: <PropertyGroup> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> </PropertyGroup> 生成的代码将位于 obj/[Configuration]/[TargetFramework]/generated/ 目录下,文件名以 .g.cs 结尾。