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
相关产品推荐
相关产品推荐

