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

在现代 .NET 开发中,源代码生成器(Source Generators)是一项强大的功能,它允许开发者在编译时自动生成代码,从而减少样板代码的编写,提高开发效率和代码质量。本文主要介绍 Mud 服务代码生成器中的两个重要组件:HttpClientApiSourceGenerator 和 HttpClientApiRegisterSourceGenerator,这两个生成器专门用于简化 HTTP 客户端的开发和配置。

什么是 Mud 服务代码生成器?

Mud 服务代码生成器是一个基于 Roslyn 的源代码生成器,用于自动生成服务层相关代码,提高开发效率。它包含以下主要功能:

  1. 服务类代码生成 - 根据实体类自动生成服务接口和服务实现类
  2. 依赖注入代码生成 - 自动为类生成构造函数注入代码,包括日志、缓存、用户管理等常用服务
  3. 服务注册代码生成 - 自动生成服务注册扩展方法,简化依赖注入配置
  4. HttpClient API 代码生成 - 自动为标记了 HTTP 方法特性的接口生成 HttpClient 实现类

HttpClient API 源生成器详解

核心功能

HttpClientApiSourceGenerator 是一个专门用于生成 HttpClient 实现类的源代码生成器。它基于 Roslyn 技术,能够自动为标记了 [HttpClientApi] 特性的接口生成完整的 HttpClient 实现类,支持 RESTful API 调用。

工作原理

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

  1. 扫描项目中的接口,查找标记了 [HttpClientApi] 特性的接口
  2. 分析接口中定义的方法和参数
  3. 根据 HTTP 方法特性(如 [Get], [Post], [Put] 等)生成相应的实现代码
  4. 处理各种参数特性(如 [Path], [Query], [Body], [Header])
  5. 生成完整的 HttpClient 实现类,包括构造函数、日志记录、错误处理等

使用示例

让我们通过一个具体的示例来了解如何使用这个生成器:

csharp 复制代码
[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 会自动生成一个实现该接口的类,大致如下:

csharp 复制代码
// 自动生成的代码
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 方法:

csharp 复制代码
[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 模板中的路径参数:

csharp 复制代码
[Get("/api/users/{userId}/orders/{orderId}")]
Task<OrderDto> GetOrderAsync([Path] string userId, [Path] string orderId);

Query 参数特性

用于生成查询字符串参数:

csharp 复制代码
[Get("/api/users")]
Task<List<UserDto>> GetUsersAsync(
    [Query] string name, 
    [Query] int? page, 
    [Query] int? pageSize);

Body 参数特性

用于设置请求体内容:

csharp 复制代码
[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 参数特性

用于设置请求头:

csharp 复制代码
[Get("/api/protected")]
Task<ProtectedData> GetProtectedDataAsync([Header] string authorization);

// 自定义头名称
[Get("/api/protected")]
Task<ProtectedData> GetProtectedDataAsync([Header("X-API-Key")] string apiKey);

复杂参数处理

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

复杂查询参数

支持复杂对象作为查询参数,自动展开为键值对:

csharp 复制代码
[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 模板中的路径参数:

csharp 复制代码
[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

错误处理与日志记录

生成的代码包含完整的错误处理和日志记录:

csharp 复制代码
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 服务的配置。

工作原理

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

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

使用示例

首先定义 HTTP API 接口:

csharp 复制代码
[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);
}

生成器会自动生成以下注册代码:

csharp 复制代码
// 自动生成的代码 - 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 特性参数

csharp 复制代码
// 基本配置
[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 { }

使用方式

在应用程序启动时调用

csharp 复制代码
// 在 Program.cs 或 Startup.cs 中
var builder = WebApplication.CreateBuilder(args);

// 自动注册所有 HttpClient API 服务
builder.Services.AddWebApiHttpClient();

// 或者与其他服务注册一起使用
builder.Services
    .AddControllers()
    .AddWebApiHttpClient();

在控制台应用程序中使用

csharp 复制代码
// 在控制台应用程序中
var services = new ServiceCollection();

// 注册 HttpClient API 服务
services.AddWebApiHttpClient();

var serviceProvider = services.BuildServiceProvider();
var dingTalkApi = serviceProvider.GetRequiredService<IDingTalkApi>();

两个生成器的协同工作

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

  1. HttpClientApiSourceGenerator 生成接口的实现类
  2. HttpClientApiRegisterSourceGenerator 生成依赖注入注册代码
  3. 完整的开发体验:定义接口 → 自动生成实现 → 自动注册服务

完整示例

csharp 复制代码
// 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 配置,可以在注册后继续配置:

csharp 复制代码
builder.Services.AddWebApiHttpClient()
    .ConfigureHttpClientDefaults(httpClient =>
    {
        httpClient.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
        {
            UseProxy = false,
            AllowAutoRedirect = false
        });
    });

添加自定义请求头

csharp 复制代码
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");
});

生成的代码结构

bash 复制代码
obj/Debug/net8.0/generated/
├── Mud.ServiceCodeGenerator/
    ├── HttpClientApiSourceGenerator/
    │   └── YourNamespace.DingTalkApi.g.cs
    └── HttpClientApiRegisterSourceGenerator/
        └── HttpClientApiExtensions.g.cs

最佳实践

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

如何查看生成的代码

要查看生成的代码,可以在项目文件中添加以下配置:

xml 复制代码
<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

生成的代码将位于 obj/[Configuration]/[TargetFramework]/generated/ 目录下,文件名以 .g.cs 结尾。

相关推荐
张人玉5 小时前
WPF 数据绑定与转换器详解
c#·wpf·light
主宰者5 小时前
WPF CalcBinding简化判断逻辑
c#·.net·wpf
【D'accumulation】5 小时前
.NET Framework 4.8 + Microsoft.Data.Sqlite 报 Library e_sqlite3 not found
microsoft·sqlite·.net
就是有点傻15 小时前
使用PaddleOCRSharp大模型精选文字识别
c#
LeonDL16815 小时前
【通用视觉框架】基于C#+Winform+OpencvSharp开发的视觉框架软件,全套源码,开箱即用
人工智能·c#·winform·opencvsharp·机器视觉软件框架·通用视觉框架·机器视觉框架
数据的世界0117 小时前
技术变革:为何C#与.NET是未来的开发方向
java·c#·.net
大龄Python青年17 小时前
C#快入教程:Linux安装.NET
linux·c#·.net
向上的车轮17 小时前
Actix Web适合什么类型的Web应用?可以部署 Java 或 .NET 的应用程序?
java·前端·rust·.net