You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C# HttpClient封装REST API的序列化与通用路径技术问题

REST API基础类序列化/反序列化问题解决方案

1. API名称“contacts”通用化实现

无需依赖JSON特性,推荐通过特性标记+反射或泛型抽象类实现,彻底避免硬编码资源名称:

方案一:自定义资源特性+反射

给实体类标记自定义特性,基础类通过反射读取资源名称:

// 自定义资源特性
[AttributeUsage(AttributeTargets.Class)]
public class ApiResourceAttribute : Attribute
{
    public string ResourceName { get; }
    public ApiResourceAttribute(string resourceName) => ResourceName = resourceName;
}

// 实体类标记对应资源名
[ApiResource("contacts")]
public class Contact 
{
    public int Id { get; set; }
    public string Name { get; set; }
}

// 基础类中获取资源名称
public abstract class BaseApiClient<TEntity>
{
    protected string GetApiEndpoint()
    {
        var resourceAttr = typeof(TEntity).GetCustomAttribute<ApiResourceAttribute>();
        // 未标记特性时,默认用类名小写作为资源名
        return resourceAttr?.ResourceName ?? typeof(TEntity).Name.ToLower();
    }
}

方案二:抽象属性子类重写

在基础类定义抽象属性,子类按需返回对应资源名称:

public abstract class BaseApiClient<TEntity>
{
    protected abstract string ApiEndpoint { get; }
}

public class ContactApiClient : BaseApiClient<Contact>
{
    protected override string ApiEndpoint => "contacts";
}

2. POST隐藏Id、PUT/PATCH保留Id的序列化处理

不能直接用JsonIgnore(会全局生效),需根据请求类型动态控制Id的序列化,不同JSON库的实现方式如下:

针对Newtonsoft.Json(Json.NET)

自定义契约解析器,根据请求方法动态决定是否序列化Id:

public class DynamicIdContractResolver : DefaultContractResolver
{
    private readonly HttpMethod _requestMethod;

    public DynamicIdContractResolver(HttpMethod requestMethod) => _requestMethod = requestMethod;

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        
        // POST请求时跳过Id序列化
        if (member.Name == nameof(Contact.Id) && _requestMethod == HttpMethod.Post)
        {
            property.ShouldSerialize = _ => false;
        }
        
        return property;
    }
}

// 使用示例
// POST序列化(隐藏Id)
var postSettings = new JsonSerializerSettings
{
    ContractResolver = new DynamicIdContractResolver(HttpMethod.Post)
};
var postJson = JsonConvert.SerializeObject(new Contact { Name = "Test" }, postSettings);

// PUT序列化(保留Id)
var putSettings = new JsonSerializerSettings
{
    ContractResolver = new DynamicIdContractResolver(HttpMethod.Put)
};
var putJson = JsonConvert.SerializeObject(new Contact { Id = 1, Name = "Updated" }, putSettings);

针对System.Text.Json

自定义转换器动态过滤Id属性:

public class DynamicIdConverter<TEntity> : JsonConverter<TEntity> where TEntity : class, new()
{
    private readonly HttpMethod _requestMethod;

    public DynamicIdConverter(HttpMethod requestMethod) => _requestMethod = requestMethod;

    public override TEntity Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return JsonSerializer.Deserialize<TEntity>(ref reader, options);
    }

    public override void Write(Utf8JsonWriter writer, TEntity value, JsonSerializerOptions options)
    {
        using var doc = JsonDocument.Parse(JsonSerializer.Serialize(value));
        writer.WriteStartObject();
        
        foreach (var property in doc.RootElement.EnumerateObject())
        {
            // POST请求跳过Id属性
            if (_requestMethod == HttpMethod.Post && property.Name.Equals(nameof(Contact.Id), StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }
            property.WriteTo(writer);
        }
        
        writer.WriteEndObject();
    }
}

// 使用示例
var postOptions = new JsonSerializerOptions();
postOptions.Converters.Add(new DynamicIdConverter<Contact>(HttpMethod.Post));
var postJson = JsonSerializer.Serialize(new Contact { Name = "Test" }, postOptions);

备选方案:DTO分离

如果不想处理动态序列化的复杂度,可创建专用DTO:

  • CreateContactDto:不含Id属性,用于POST请求
  • UpdateContactDto:包含Id属性,用于PUT/PATCH请求
    基础类根据操作类型对应不同DTO,逻辑更直观。

内容的提问来源于stack exchange,提问作者Andre

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 02:25:17