找回密码
 立即注册
首页 业界区 业界 解放双手!使用Roslyn生成代码让你的 HTTP 客户端开发变 ...

解放双手!使用Roslyn生成代码让你的 HTTP 客户端开发变得如此简单

毡轩 昨天 19:50
在现代 .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 实现类,包括构造函数、日志记录、错误处理等
使用示例

让我们通过一个具体的示例来了解如何使用这个生成器:
  1. [HttpClientApi]
  2. public interface IDingTalkApi
  3. {
  4.     [Get("/api/v1/user/{id}")]
  5.     Task<UserDto> GetUserAsync([Query] string id);
  6.    
  7.     [Post("/api/v1/user")]
  8.     Task<UserDto> CreateUserAsync([Body] UserDto user);
  9.    
  10.     [Put("/api/v1/user/{id}")]
  11.     Task<UserDto> UpdateUserAsync([Path] string id, [Body] UserDto user);
  12.    
  13.     [Delete("/api/v1/user/{id}")]
  14.     Task<bool> DeleteUserAsync([Path] string id);
  15. }
复制代码
当项目编译时,HttpClientApiSourceGenerator 会自动生成一个实现该接口的类,大致如下:
  1. // 自动生成的代码
  2. public partial class DingTalkApi : IDingTalkApi
  3. {
  4.     private readonly HttpClient _httpClient;
  5.     private readonly ILogger<DingTalkApi> _logger;
  6.     private readonly JsonSerializerOptions _jsonSerializerOptions;
  7.    
  8.     public DingTalkApi(HttpClient httpClient, ILogger<DingTalkApi> logger)
  9.     {
  10.         _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
  11.         _logger = logger ?? throw new ArgumentNullException(nameof(logger));
  12.         _jsonSerializerOptions = new JsonSerializerOptions
  13.         {
  14.             PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
  15.             WriteIndented = false,
  16.             PropertyNameCaseInsensitive = true
  17.         };
  18.     }
  19.    
  20.     public async Task<UserDto> GetUserAsync(string id)
  21.     {
  22.         // 自动生成的 HTTP GET 请求逻辑
  23.         _logger.LogDebug("开始HTTP GET请求: {Url}", "/api/v1/user/{id}");
  24.         
  25.         var url = $"/api/v1/user/{id}";
  26.         using var request = new HttpRequestMessage(HttpMethod.Get, url);
  27.         
  28.         // 处理查询参数
  29.         var queryParams = new List<string>();
  30.         if (id != null)
  31.             queryParams.Add($"id={id}");
  32.         
  33.         if (queryParams.Any())
  34.             url += "?" + string.Join("&", queryParams);
  35.         
  36.         // 发送请求并处理响应
  37.         // ... 完整的请求处理逻辑
  38.     }
  39. }
复制代码
支持的 HTTP 方法

该生成器支持所有标准的 HTTP 方法:
  1. [HttpClientApi]
  2. public interface IExampleApi
  3. {
  4.     [Get("/api/resource/{id}")]
  5.     Task<ResourceDto> GetResourceAsync([Path] string id);
  6.    
  7.     [Post("/api/resource")]
  8.     Task<ResourceDto> CreateResourceAsync([Body] ResourceDto resource);
  9.    
  10.     [Put("/api/resource/{id}")]
  11.     Task<ResourceDto> UpdateResourceAsync([Path] string id, [Body] ResourceDto resource);
  12.    
  13.     [Delete("/api/resource/{id}")]
  14.     Task<bool> DeleteResourceAsync([Path] string id);
  15.    
  16.     [Patch("/api/resource/{id}")]
  17.     Task<ResourceDto> PatchResourceAsync([Path] string id, [Body] object patchData);
  18.    
  19.     [Head("/api/resource/{id}")]
  20.     Task<bool> CheckResourceExistsAsync([Path] string id);
  21.    
  22.     [Options("/api/resource")]
  23.     Task<HttpResponseMessage> GetResourceOptionsAsync();
  24. }
复制代码
参数特性详解

生成器支持多种参数特性,以处理不同的 HTTP 请求参数:
Path 参数特性

用于替换 URL 模板中的路径参数:
  1. [Get("/api/users/{userId}/orders/{orderId}")]
  2. Task<OrderDto> GetOrderAsync([Path] string userId, [Path] string orderId);
复制代码
Query 参数特性

用于生成查询字符串参数:
  1. [Get("/api/users")]
  2. Task<List<UserDto>> GetUsersAsync(
  3.     [Query] string name,
  4.     [Query] int? page,
  5.     [Query] int? pageSize);
复制代码
Body 参数特性

用于设置请求体内容:
  1. [Post("/api/users")]
  2. Task<UserDto> CreateUserAsync([Body] UserDto user);
  3. // 支持自定义内容类型
  4. [Post("/api/users")]
  5. Task<UserDto> CreateUserAsync([Body(ContentType = "application/xml")] UserDto user);
  6. // 支持字符串内容
  7. [Post("/api/logs")]
  8. Task LogMessageAsync([Body(UseStringContent = true)] string message);
复制代码
Header 参数特性

用于设置请求头:
  1. [Get("/api/protected")]
  2. Task<ProtectedData> GetProtectedDataAsync([Header] string authorization);
  3. // 自定义头名称
  4. [Get("/api/protected")]
  5. Task<ProtectedData> GetProtectedDataAsync([Header("X-API-Key")] string apiKey);
复制代码
复杂参数处理

生成器还能处理复杂的参数类型:
复杂查询参数

支持复杂对象作为查询参数,自动展开为键值对:
  1. [Get("/api/search")]
  2. Task<List<UserDto>> SearchUsersAsync([Query] UserSearchCriteria criteria);
  3. public class UserSearchCriteria
  4. {
  5.     public string Name { get; set; }
  6.     public int? Age { get; set; }
  7.     public string Department { get; set; }
  8. }
  9. // 生成的查询字符串:?Name=John&Age=30&Department=IT
复制代码
路径参数自动替换

自动处理 URL 模板中的路径参数:
  1. [Get("/api/users/{userId}/orders/{orderId}/items/{itemId}")]
  2. Task<OrderItemDto> GetOrderItemAsync(
  3.     [Path] string userId,
  4.     [Path] string orderId,
  5.     [Path] string itemId);
  6. // 自动替换:/api/users/123/orders/456/items/789
复制代码
错误处理与日志记录

生成的代码包含完整的错误处理和日志记录:
  1. public async Task<UserDto> GetUserAsync(string id)
  2. {
  3.     try
  4.     {
  5.         _logger.LogDebug("开始HTTP GET请求: {Url}", "/api/v1/user/{id}");
  6.         
  7.         // 请求处理逻辑
  8.         
  9.         using var response = await _httpClient.SendAsync(request);
  10.         var responseContent = await response.Content.ReadAsStringAsync();
  11.         
  12.         _logger.LogDebug("HTTP请求完成: {StatusCode}, 响应长度: {ContentLength}",
  13.             (int)response.StatusCode, responseContent?.Length ?? 0);
  14.         
  15.         if (!response.IsSuccessStatusCode)
  16.         {
  17.             _logger.LogError("HTTP请求失败: {StatusCode}, 响应: {Response}",
  18.                 (int)response.StatusCode, responseContent);
  19.             throw new HttpRequestException($"HTTP请求失败: {(int)response.StatusCode} - {response.ReasonPhrase}");
  20.         }
  21.         
  22.         // 响应处理逻辑
  23.     }
  24.     catch (Exception ex)
  25.     {
  26.         _logger.LogError(ex, "HTTP请求异常: {Url}", url);
  27.         throw;
  28.     }
  29. }
复制代码
HttpClient API 注册源生成器详解

核心功能

HttpClientApiRegisterSourceGenerator 是另一个重要的组件,它自动为标记了 [HttpClientApi] 特性的接口生成依赖注入注册代码,简化 HttpClient 服务的配置。
工作原理

该生成器的工作流程如下:

  • 扫描项目中的接口,查找标记了 [HttpClientApi] 特性的接口
  • 提取特性中的配置参数(如 BaseUrl、Timeout 等)
  • 生成用于依赖注入的扩展方法
  • 自动注册接口和实现类到服务容器中
使用示例

首先定义 HTTP API 接口:
  1. [HttpClientApi("https://api.dingtalk.com", Timeout = 30)]
  2. public interface IDingTalkApi
  3. {
  4.     [Get("/api/v1/user/{id}")]
  5.     Task<UserDto> GetUserAsync([Query] string id);
  6.    
  7.     [Post("/api/v1/user")]
  8.     Task<UserDto> CreateUserAsync([Body] UserDto user);
  9. }
  10. [HttpClientApi("https://api.wechat.com", Timeout = 60)]
  11. public interface IWeChatApi
  12. {
  13.     [Get("/api/v1/user/{id}")]
  14.     Task<UserDto> GetUserAsync([Query] string id);
  15. }
复制代码
生成器会自动生成以下注册代码:
  1. // 自动生成的代码 - HttpClientApiExtensions.g.cs
  2. using System;
  3. using System.Net.Http;
  4. using Microsoft.Extensions.DependencyInjection;
  5. namespace Microsoft.Extensions.DependencyInjection
  6. {
  7.     public static class HttpClientApiExtensions
  8.     {
  9.         public static IServiceCollection AddWebApiHttpClient(this IServiceCollection services)
  10.         {
  11.             services.AddHttpClient<global::YourNamespace.IDingTalkApi, global::YourNamespace.DingTalkApi>(client =>
  12.             {
  13.                 client.BaseAddress = new Uri("https://api.dingtalk.com");
  14.                 client.Timeout = TimeSpan.FromSeconds(30);
  15.             });
  16.             
  17.             services.AddHttpClient<global::YourNamespace.IWeChatApi, global::YourNamespace.WeChatApi>(client =>
  18.             {
  19.                 client.BaseAddress = new Uri("https://api.wechat.com");
  20.                 client.Timeout = TimeSpan.FromSeconds(60);
  21.             });
  22.             
  23.             return services;
  24.         }
  25.     }
  26. }
复制代码
配置选项

HttpClientApi 特性参数
  1. // 基本配置
  2. [HttpClientApi("https://api.example.com")]
  3. public interface IExampleApi { }
  4. // 配置超时时间
  5. [HttpClientApi("https://api.example.com", Timeout = 120)]
  6. public interface IExampleApi { }
  7. // 使用命名参数
  8. [HttpClientApi(BaseUrl = "https://api.example.com", Timeout = 60)]
  9. public interface IExampleApi { }
复制代码
使用方式

在应用程序启动时调用
  1. // 在 Program.cs 或 Startup.cs 中
  2. var builder = WebApplication.CreateBuilder(args);
  3. // 自动注册所有 HttpClient API 服务
  4. builder.Services.AddWebApiHttpClient();
  5. // 或者与其他服务注册一起使用
  6. builder.Services
  7.     .AddControllers()
  8.     .AddWebApiHttpClient();
复制代码
在控制台应用程序中使用
  1. // 在控制台应用程序中
  2. var services = new ServiceCollection();
  3. // 注册 HttpClient API 服务
  4. services.AddWebApiHttpClient();
  5. var serviceProvider = services.BuildServiceProvider();
  6. var dingTalkApi = serviceProvider.GetRequiredService<IDingTalkApi>();
复制代码
两个生成器的协同工作

HttpClientApiRegisterSourceGenerator 与 HttpClientApiSourceGenerator 完美配合,提供完整的开发体验:

  • HttpClientApiSourceGenerator 生成接口的实现类
  • HttpClientApiRegisterSourceGenerator 生成依赖注入注册代码
  • 完整的开发体验:定义接口 → 自动生成实现 → 自动注册服务
完整示例
  1. // 1. 定义接口
  2. [HttpClientApi("https://api.dingtalk.com", Timeout = 30)]
  3. public interface IDingTalkApi
  4. {
  5.     [Get("/api/v1/user/{id}")]
  6.     Task<UserDto> GetUserAsync([Query] string id);
  7. }
  8. // 2. 自动生成实现类 (由 HttpClientApiSourceGenerator 生成)
  9. // public partial class DingTalkApi : IDingTalkApi { ... }
  10. // 3. 自动生成注册代码 (由 HttpClientApiRegisterSourceGenerator 生成)
  11. // public static class HttpClientApiExtensions { ... }
  12. // 4. 在应用程序中使用
  13. var builder = WebApplication.CreateBuilder(args);
  14. builder.Services.AddWebApiHttpClient(); // 自动注册
  15. var app = builder.Build();
  16. // 5. 在服务中注入使用
  17. public class UserService
  18. {
  19.     private readonly IDingTalkApi _dingTalkApi;
  20.    
  21.     public UserService(IDingTalkApi dingTalkApi)
  22.     {
  23.         _dingTalkApi = dingTalkApi;
  24.     }
  25.    
  26.     public async Task<UserDto> GetUserAsync(string userId)
  27.     {
  28.         return await _dingTalkApi.GetUserAsync(userId);
  29.     }
  30. }
复制代码
高级配置

自定义 HttpClient 配置

如果需要更复杂的 HttpClient 配置,可以在注册后继续配置:
  1. builder.Services.AddWebApiHttpClient()
  2.     .ConfigureHttpClientDefaults(httpClient =>
  3.     {
  4.         httpClient.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
  5.         {
  6.             UseProxy = false,
  7.             AllowAutoRedirect = false
  8.         });
  9.     });
复制代码
添加自定义请求头
  1. builder.Services.AddHttpClient<IDingTalkApi, DingTalkApi>(client =>
  2. {
  3.     client.BaseAddress = new Uri("https://api.dingtalk.com");
  4.     client.Timeout = TimeSpan.FromSeconds(30);
  5.     client.DefaultRequestHeaders.Add("User-Agent", "MyApp/1.0");
  6.     client.DefaultRequestHeaders.Add("X-API-Key", "your-api-key");
  7. });
复制代码
生成的代码结构
  1. obj/Debug/net8.0/generated/
  2. ├── Mud.ServiceCodeGenerator/
  3.     ├── HttpClientApiSourceGenerator/
  4.     │   └── YourNamespace.DingTalkApi.g.cs
  5.     └── HttpClientApiRegisterSourceGenerator/
  6.         └── HttpClientApiExtensions.g.cs
复制代码
最佳实践


  • 统一配置:在 [HttpClientApi] 特性中统一配置所有 API 的基础设置
  • 合理超时:根据 API 的响应时间设置合理的超时时间
  • 命名规范:遵循接口命名规范(I{ServiceName}Api)
  • 错误处理:在服务层处理 API 调用异常
  • 日志记录:利用生成的日志记录功能监控 API 调用
如何查看生成的代码

要查看生成的代码,可以在项目文件中添加以下配置:
  1. <PropertyGroup>
  2.   <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  3. </PropertyGroup>
复制代码
生成的代码将位于 obj/[Configuration]/[TargetFramework]/generated/ 目录下,文件名以 .g.cs 结尾。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册