环境:
- asp.net core3.1
一、先说下总结:
如果想让一个实体模型同时应用在webapi(使用newtonsoft.json序列化)和grpc环境中,那么可以参照如下形式:
[JsonObject(MemberSerialization.OptOut)] [DataContract] public class PageReq { [DataMember(Order = 1)] public int PageIndex { get; set; } [DataMember(Order = 2)] public int PageSize { get; set; } }
二、问题剖析
一般web框架都是用NewtonsoftJson序列化库,在asp.netcore
中引用如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddNewtonsoftJson(options =>
{
//忽略循环引用
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
//使用驼峰 首字母小写
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
//设置时间格式
options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
//将枚举序列化为字符串
//options.SerializerSettings.Converters.Add(new StringEnumConverter());
});
}
这么用一般没什么问题,然而在grpc中的序列化是需要借助[DataContract]
和[DataMember]
的,所以定义一个公共dto模型的时候就不得不考虑冲突的问题,
比如说分页请求模型:
public class PageReq
{
public int PageIndex { get; set; }
public int PageSize { get; set; }
}
public class GetUserListReq : PageReq
{
public string Name { get; set; }
}
这个模型在web开发中是没有问题的,但是到了Grpc中就出现了问题:因为PageReq未标记为[DataContract],[DataMember] 所以不会被序列化
。
于是,在PageReq上增加标记如下:
public class PageReq
{
[DataMember(Order = 1)]
public int PageIndex { get; set; }
[DataMember(Order = 2)]
public int PageSize { get; set; }
}
public class GetUserListReq : PageReq
{
[DataMember(Order = 3)]
public string Name { get; set; }
}
当打上标记后,针对grpc
的数据传输是没有问题了,但当这个模型应用在webapi
开发中时,我们发现所有继承自 PageReq
的模型属性都必须打上DataMember
标记,否则不被序列化和反序列化,如下所示:
/*模型*/
[DataContract]
public class PageReq
{
[DataMember(Order = 1)]
public int PageIndex { get; set; }
[DataMember(Order = 2)]
public int PageSize { get; set; }
}
public class GetUserListReq : PageReq
{
public string Name { get; set; }
}
/*startup.cs引入newtonsoft*/
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(config =>
{
config.Filters.Add<GlobalExceptionFilter>();
}).AddNewtonsoftJson(options =>
{
//忽略循环引用
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
//使用驼峰 首字母小写
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
//设置时间格式
options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
//将枚举序列化为字符串
options.SerializerSettings.Converters.Add(new StringEnumConverter());
});
}
/*Controller定义*/
[Route("api/[controller]/[action]")]
public class DemoController : ControllerBase
{
[HttpPost]
public string TestJson([FromBody] GetUserListReq req)
{
if (string.IsNullOrWhiteSpace(req.Name)) req.Name = "xxxx";
var res = Newtonsoft.Json.JsonConvert.SerializeObject(req);
return $"res={res}\r\nname={req.Name},pageIndex={req.PageIndex},pageSize={req.PageSize}";
}
}
postman测试情况:
这就尴尬了,先说下原因:
默认NewtonsoftJson序列化的规则是对所有公共属性都进行序列化,除非手动指定忽略哪些属性,如下:
而当我们在标记为[DataContract]
特性后,就把序列化模式从OptOut
改为 OptIn
,在OptIn
模式下,NewtonsoftJson只会序列化标记为JsonProperty
或DataMember
特性的属性,其他的则忽略。
让我们来看一下,NewtonsoftJson的说明:
知道了其中的原因,那么将公共模型PageReq
定义如下:
[JsonObject(MemberSerialization.OptOut)]
[DataContract]
public class PageReq
{
[DataMember(Order = 1)]
public int PageIndex { get; set; }
[DataMember(Order = 2)]
public int PageSize { get; set; }
}
public class GetUserListReq : PageReq
{
public string Name { get; set; }
}
这样,再看一下postman的情况: