Senparc/NeuChar

当开启Redis缓存,并且设置了JsonConvert.DefaultSettings之后,MessageHandlerMiddleware中的JSON转换会出现错误

thinkif opened this issue · 12 comments

环境:Windows 2016 IIS 10
框架:.net core 3.0
nuget引用的版本:
Senparc.CO2NET Version="1.0.102"
Senparc.CO2NET.Cache.Redis Version="3.6.102"
Senparc.CO2NET.Cache.Redis.RedLock Version="2.2.4"
Senparc.NeuChar Version="1.0.102"
Senparc.NeuChar.App Version="0.6.102"
Senparc.Weixin.Cache.Redis Version="2.7.102"
Senparc.Weixin.Cache.Redis.RedLock Version="1.2.5"
Senparc.Weixin.TenPay Version="1.5.102"
均为发帖时最新版

经过多次反复实验,目前测试的结果是,在setup.cs中开启 Redis 缓存,并且自定义了 JSON 序列化的默认设置 JsonConvert.DefaultSettings (设置成任何内容都一样),向公众号发送消息时,就出现错误,异常信息被保存到了 SenparcTraceLog 的 log 文件中,内容如下:

[[[MessageHandlerware 过程发证异常]]]
[2019/11/11 09:50:36.7535]
[线程:44]
中间件类型:MpMessageHandlerMiddleware1 MessageHandler 类型:尚未生成 异常信息:System.ArgumentNullException: Value cannot be null. (Parameter 'value') at Newtonsoft.Json.Utilities.ValidationUtils.ArgumentNotNull(Object value, String parameterName) at Newtonsoft.Json.Linq.Extensions.Value[T,U](IEnumerable1 value)
at Senparc.NeuChar.Context.MessageContextJsonConverter3.ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.DeserializeConvertable(JsonConverter converter, JsonReader reader, Type objectType, Object existingValue) at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent) at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType) at Newtonsoft.Json.JsonSerializer.Deserialize(JsonReader reader, Type objectType) at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings) at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonConverter[] converters) at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonConverter[] converters) at Senparc.NeuChar.Context.GlobalMessageContext3.GetMessageContextAsync(String userName)
at Senparc.NeuChar.Context.GlobalMessageContext3.GetMessageContextAsync(String userName, Boolean createIfNotExists) at Senparc.NeuChar.Context.GlobalMessageContext3.GetMessageContextAsync(TRequest requestMessage)
at Senparc.NeuChar.MessageHandlers.MessageHandler3.GetCurrentMessageContext() at Senparc.NeuChar.MessageHandlers.MessageHandler3.CommonInitialize(XDocument postDataDocument, Int32 maxRecordCount, IEncryptPostModel postModel, Boolean onlyAllowEcryptMessage)
at Senparc.NeuChar.MessageHandlers.MessageHandler3..ctor(Stream inputStream, IEncryptPostModel postModel, Int32 maxRecordCount, Boolean onlyAllowEcryptMessage) at Senparc.Weixin.MP.MessageHandlers.MessageHandler1..ctor(Stream inputStream, PostModel postModel, Int32 maxRecordCount, Boolean onlyAllowEcryptMessage, DeveloperInfo developerInfo)
at Senparc.NeuChar.Middlewares.MessageHandlerMiddleware`5.Invoke(HttpContext context)

补充一点,关闭 Redis 或者不设置 JsonConvert.DefaultSettings 就不会出问题,也就是这两点同时具备就会出错

@JeffreySu 您好,还在跟进这个问题么?
我这里有了新的进展,现在发现是由于我在项目中设置了 JsonConvert.DefaultSettings 并且包含有
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
两个设置,即JSON全部为 camelCase 式的驼峰命名法,并且忽略 null 的属性

https://github.com/Senparc/NeuChar/blob/master/src/Senparc.NeuChar/Context/JsonConverters/MessageContextJsonConverter.cs 代码中直接写的属性名,如61行起

messageContext.UserName = item["UserName"].Value<string>();
messageContext.LastActiveTime = GetDateTimeOffset(item["LastActiveTime"]);
messageContext.ThisActiveTime = GetDateTimeOffset(item["ThisActiveTime"]);
messageContext.ExpireMinutes = item["ExpireMinutes"].Value<Double?>();
messageContext.AppStoreState = (AppStoreState)(item["AppStoreState"].Value<int>());
messageContext.CurrentAppDataItem = item["CurrentAppDataItem"].ToObject<AppDataItem>();

由此造成了此问题

@thinkif 这个问题还有解决,我这边还没单元测试可以重现,你的意思是,因为设置了驼峰命名法(这个有什么关系吗?),以及可能有些参数为null,但是因为你全局设置,这些null的参数最后都没有出现在JSON中,所以反序列化失败了,是吗?

我加了一行TODO备注,是这个意思吧?

@JeffreySu 你好,根据目前我观察到的现象,由于启用了 redis 缓存,所以微信服务器发来的数据被存储到redis中,以 JSON 格式存储的,本来序列化和反序列化应该是没问题的,但是如我在前一条中提到的,MessageContextJsonConverter.cs 代码中使用了 item["StorageDataTypeName"].Value() 这个写法,如果redis存储的是 { storageDataTypeName: "something" } 则会出现错误。
另外你提到的TODO备注也是出现此问题的另一个原因。

总之就是由于得到的 JSON 字符串的属性与 item[属性名] 使用的属性名不匹配出现的问题。

以上是我在试验得出的结论,仅仅是猜测,也可能是由其他问题造成的。

另外有个地方没想明白,请教一下,messageContext 为何不使用 JSON.NET 的反序列化实现,而要逐个属性赋值呢。

再次感谢你的跟进。真心感谢。

@thinkif 先回答您的问题,之所以不能直接用反序列化是为了处理序列化过程中的一些特殊情况,

public class MessageContextJsonConverter<TMC, TRequest, TResponse> : JsonConverter

这个Converter里面的代码都是为了对Json数据做一些加工然后给到实体里面,比如对 RequestMessagesResponseMessages 的特殊处理。

然后我有个疑问是: { storageDataTypeName: "something" } 这个应该是系统会自动指定的,所以反序列化应该都能获取成功,您是对这些数据会去修改吗?

@thinkif 如果你有“非标”的JSON,欢迎在这个单元测试下面补充一个方法,这样便于我们重现或者确认是否已经解决了这个问题:

新版本也马上会发布。

我项目中的JSON都是标准格式的。

只是在JSON的属性命名式,使用小驼峰规则( camelCase ),并忽略值为null的属性。


然后我有个疑问是: { storageDataTypeName: "something" } 这个应该是系统会自动指定的,所以反序列化应该都能获取成功,您是对这些数据会去修改吗?

我没有修改,但是腾讯传过来的数据是 { StorageDataTypeName: "something" } 这样格式的,属性名为大驼峰规则 (CamelCase) ,而我在项目的Setup.cs 中设置了 JsonConvert.DefaultSettings 即覆盖了JSON序列化/反序列化的模式设置,设置的内容包括了日期时间的处理格式、使用小驼峰规则( camelCase ),并忽略值为null的属性等。

这些设置本来是为了简化项目开发的,但是由于设置在了 JsonConvert.DefaultSettings 中,也就影响到了你的逻辑。

我查询了 JSON.net 的说明,这个设置也会影响到ToObject方法,

Gets or sets a function that creates default JsonSerializerSettings. Default settings are automatically used by serialization methods on JsonConvert, and ToObject () and FromObject(Object) on JToken. To serialize without using any default settings create a JsonSerializer with Create().

https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_JsonConvert_DefaultSettings.htm

之所以能追溯到 MessageContextJsonConverter.cs 这个文件,也是通过异常堆栈中的提示一点点找到的,但愿没错。

如果您已经发布了,我可以我的项目测试看看是否解决了这个问题。

是1.1.100.2-preview2么?

@JeffreySu
我试验了最新preview版本(将所有Senparc的nuget都更新至最新的预览版),仍然出现问题
(截至 2020-02-13 23:11)

测试的方法是在 setup.cs 中加入如下代码
`
JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings
{
DateFormatString = "yyyy-MM-dd HH:mm:ss",
DateParseHandling = Newtonsoft.Json.DateParseHandling.None,
DefaultValueHandling = DefaultValueHandling.Include,
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore,
};

            JsonConvert.DefaultSettings = () => jsonSerializerSettings;

`
此时给微信公众号发送消息,出现错误

将代码改为
`
JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings
{
DateFormatString = "yyyy-MM-dd HH:mm:ss",
DateParseHandling = Newtonsoft.Json.DateParseHandling.None,
DefaultValueHandling = DefaultValueHandling.Include,
};

            JsonConvert.DefaultSettings = () => jsonSerializerSettings;

`

此时给公众号发消息,可正常反馈

经过反复测试,JsonConvert.DefaultSettings 返回的JsonSerializerSettings设置中,只要包含以下任意一项均不好使。

ContractResolver = new CamelCasePropertyNamesContractResolver(), NullValueHandling = NullValueHandling.Ignore,

另外我在想,会不会是再往前一步,接收到微信发送过来的数据写入redis中出现的问题,而 MessageContextJsonConverter.cs 只是问题表现出来的地方,也就是说即便 TryGetValue() 扩展方法 解决了此处,仍会还有其他的,毕竟不可能将所有的地方都用TryGetValue代替。

但是我目前还不太了解 Senparc 接收到微信服务器发来消息后的处理机制。(已启用了UseMessageHandlerForMp中间件)

@thinkif 你能不能直接给一个会出错的Json,这样我们可以直接验证一下。

长时间没有答复,先关闭了。