diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/__Internal/Newtonsoft.Json/Object/FlattenNArrayObjectConverterBase.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/__Internal/Newtonsoft.Json/Object/FlattenNArrayObjectConverterBase.cs index 32dfa040..d94a81bc 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/__Internal/Newtonsoft.Json/Object/FlattenNArrayObjectConverterBase.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/__Internal/Newtonsoft.Json/Object/FlattenNArrayObjectConverterBase.cs @@ -1,54 +1,44 @@ using System; -using System.Collections; -using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Converters.Common; using Newtonsoft.Json.Serialization; -namespace Newtonsoft.Json.Converters +namespace Newtonsoft.Json.Converters.Internal { using SKIT.FlurlHttpClient.Wechat.TenpayV2.Utilities; - internal static class FlattenNArrayObjectConverterBase + internal abstract class FlattenNArrayObjectConverterBase : JsonConverter + where TModelContainer : class, new() + where TFlattenElement: class, new() { - public const string PROPERTY_WILDCARD_NARRAY_ELEMENT = "$n"; - public const string PROPERTY_NAME_NARRAY = "#n"; - } + public const string FLATTEN_PROPERTY_JSON_NAME = "#n"; - internal abstract partial class FlattenNArrayObjectConverterBase : JsonConverter - where T : class, new() - { - private sealed class InnerTypedJsonPropertyInfo + private static bool TryMatchNArrayIndex(string key, out int index) { - public string PropertyName { get; } - - public PropertyInfo PropertyInfo { get; } - - public Type PropertyType { get { return PropertyInfo.PropertyType; } } - - public bool PropertyIsNArray { get; } - - public JsonConverter? JsonConverterOnRead { get; } - - public JsonConverter? JsonConverterOnWrite { get; } - - public InnerTypedJsonPropertyInfo(string propertyName, PropertyInfo propertyInfo, bool propertyIsNArray, JsonConverter? jsonReadConverter, JsonConverter? jsonWriteConverter) + Regex regex = new Regex(@"_(\d+)$", RegexOptions.Compiled); + if (regex.IsMatch(key)) { - PropertyName = propertyName; - PropertyInfo = propertyInfo; - PropertyIsNArray = propertyIsNArray; - JsonConverterOnRead = jsonReadConverter; - JsonConverterOnWrite = jsonWriteConverter; + string str = regex.Match(key).Groups[1].Value; + index = int.Parse(str); + return true; } + + index = -1; + return false; } - private const string PROPERTY_WILDCARD_NARRAY_ELEMENT = FlattenNArrayObjectConverterBase.PROPERTY_WILDCARD_NARRAY_ELEMENT; - private const string PROPERTY_NAME_NARRAY = FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY; + private static JsonObjectContract ResolveJsonType(Type type, JsonSerializer serializer) + { + IContractResolver resolver = serializer.ContractResolver ?? JsonSerializer.CreateDefault().ContractResolver; + return (JsonObjectContract)resolver.ResolveContract(type); + } - private static readonly Hashtable _mappedTypeJsonProperties = new Hashtable(); + /// + /// 获取 `` 类下需要扁平展开的属性,该属性需是一个数组类型、其中每个元素是一个 ``。 + /// + protected abstract PropertyInfo FlattenProperty { get; } public override bool CanRead { @@ -62,7 +52,11 @@ namespace Newtonsoft.Json.Converters public override bool CanConvert(Type objectType) { - return objectType.IsClass && !objectType.IsAbstract && !objectType.IsInterface; + if (!FlattenProperty.PropertyType.IsArray) + throw new NotSupportedException(); + + return objectType.IsClass && !objectType.IsAbstract && !objectType.IsInterface + && typeof(TModelContainer).IsAssignableFrom(objectType); } public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) @@ -73,72 +67,92 @@ namespace Newtonsoft.Json.Converters } else if (reader.TokenType == JsonToken.StartObject) { + TModelContainer outputObj = existingValue as TModelContainer ?? ReflectionHelper.CreateInstance(); + + // 读取为 JObject JObject jObject = JObject.Load(reader); - T tObject = existingValue as T ?? new T(); + if (!jObject.Properties().Any()) + return outputObj; - JsonObjectContract jsonContract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(typeof(T)); - foreach (JProperty jProperty in jObject.Properties()) + // 取得下标最大值(-1 表示无需扁平展开) + int maxIndex = jObject.Properties().Select(e => TryMatchNArrayIndex(e.Name!, out int index) ? index : -1).Max(); + + // 解析 JSON 结构 + JsonObjectContract jsonContract = ResolveJsonType(objectType, serializer); + + // 遍历 JObject 并反序列化 + foreach (JProperty jProp in jObject.Properties()) { - string propertyName = jProperty.Name!; + string propertyName = jProp.Name!; - if (TryMatchNArrayIndex(propertyName, out int index)) + if (maxIndex != -1 && TryMatchNArrayIndex(propertyName, out int index)) { - InnerTypedJsonPropertyInfo[] typedJsonProperties = GetTypedJsonProperties(objectType); - if (typedJsonProperties.Count(p => p.PropertyIsNArray) != 1) - throw new JsonSerializationException("The count of `$n` properties must be only one."); - - foreach (InnerTypedJsonPropertyInfo propertyJsonInfo in typedJsonProperties.Where(e => e.PropertyIsNArray)) + // 反序列化需扁平展开的字段 + JsonObjectContract flattenJsonContract = ResolveJsonType(FlattenProperty.PropertyType.GetElementType()!, serializer); + JsonProperty? jsonProperty = flattenJsonContract.Properties.FirstOrDefault(e => propertyName == e.PropertyName?.Replace("$n", index.ToString())); + if (jsonProperty is null) { - Array? propertyValue = propertyJsonInfo.PropertyInfo.GetValue(tObject) as Array; - ReflectionHelper.CreateOrExpandArray(ref propertyValue, propertyJsonInfo.PropertyType.GetElementType()!, index + 1); - ReflectionHelper.CreateOrExpandArrayElement(propertyValue!, index, (object element) => + if (serializer.MissingMemberHandling == MissingMemberHandling.Error) + throw new JsonException($"Could not find member '{propertyName}' on object of type '{objectType.FullName}'"); + } + else + { + Array? array = ReflectionHelper.GetPropertyValue(outputObj, FlattenProperty); + if (array is null) { - InnerTypedJsonPropertyInfo? elementPropertyJsonInfo = GetTypedJsonProperties(element.GetType()) - .SingleOrDefault(p => string.Equals(p.PropertyName.Replace(PROPERTY_WILDCARD_NARRAY_ELEMENT, index.ToString()), jProperty.Name)); - if (elementPropertyJsonInfo is not null) - { - JsonSerializer serializerCopy = JsonSerializer.Create(serializer.ExtractSerializerSettings()); - serializerCopy.Converters.Remove(this); + array = new TFlattenElement[maxIndex + 1]; + ReflectionHelper.SetPropertyValue(outputObj, FlattenProperty, array); + } - if (elementPropertyJsonInfo.JsonConverterOnRead is not null) - serializerCopy.Converters.Add(elementPropertyJsonInfo.JsonConverterOnRead); + TFlattenElement? element = ReflectionHelper.GetElementValue(array, index); + if (element is null) + { + element = ReflectionHelper.CreateInstance(); + ReflectionHelper.SetElementValue(array, index, element); + } - object? elementPropertyValue = jProperty.Value?.ToObject(elementPropertyJsonInfo.PropertyType, serializerCopy); - elementPropertyJsonInfo.PropertyInfo.SetValue(element, elementPropertyValue); - } + if (jsonProperty.ShouldDeserialize is not null && !jsonProperty.ShouldDeserialize(array)) + continue; + if (!jsonProperty.Writable) + continue; - return element; - }); - - propertyJsonInfo.PropertyInfo.SetValue(tObject, propertyValue); + object? elementPropertyValue = jProp.Value?.ToObject(jsonProperty.PropertyType, CopyJsonSerializer(serializer, jsonProperty.Converter)); + jsonProperty.ValueProvider!.SetValue(element, elementPropertyValue); } } else { - JsonProperty? jsonContractProperty = jsonContract.Properties[propertyName]; - if (jsonContractProperty is not null && jsonContractProperty.Writable) + JsonProperty? jsonProperty = jsonContract.Properties.GetClosestMatchProperty(propertyName); + // 反序列化普通字段 + if (jsonProperty is not null) { - JsonSerializer serializerCopy = JsonSerializer.Create(serializer.ExtractSerializerSettings()); - serializerCopy.Converters.Remove(this); + if (jsonProperty.Ignored && jsonProperty.PropertyName is null) + continue; + if (jsonProperty.ShouldDeserialize is not null && !jsonProperty.ShouldDeserialize(outputObj)) + continue; + if (!jsonProperty.Writable) + continue; - if (jsonContractProperty.Converter is not null) - serializerCopy.Converters.Add(jsonContractProperty.Converter); - - object? propertyValue = jProperty.Value.ToObject(jsonContractProperty.PropertyType, serializerCopy); - jsonContractProperty.ValueProvider!.SetValue(tObject, propertyValue); + object? propertyValue = jProp.Value?.ToObject(jsonProperty.PropertyType, CopyJsonSerializer(serializer, jsonProperty.Converter)); + jsonProperty.ValueProvider!.SetValue(outputObj, propertyValue); + } + // 反序列化扩展字段 + else if (jsonContract.ExtensionDataSetter is not null) + { + throw new NotSupportedException($"'{GetType().FullName}' does not support JsonExtensionData."); } } } - return tObject; + return outputObj; } throw new JsonException($"Unexpected JSON token type '{reader.TokenType}' when reading."); } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, object? inputObj, JsonSerializer serializer) { - if (value is null) + if (inputObj is null) { writer.WriteNull(); return; @@ -146,16 +160,29 @@ namespace Newtonsoft.Json.Converters writer.WriteStartObject(); - JsonObjectContract jsonContract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType()); - foreach (JsonProperty jsonContractProperty in jsonContract.Properties.OrderBy(e => e.Order)) + // 解析 JSON 结构 + JsonObjectContract jsonContract = ResolveJsonType(inputObj.GetType(), serializer); + + // 序列化普通字段 + foreach (JsonProperty jsonProperty in jsonContract.Properties.OrderBy(e => e.Order)) { - if (jsonContractProperty.Ignored) + if (jsonProperty.Ignored && jsonProperty.PropertyName is null) continue; - if (!(jsonContractProperty.ShouldSerialize is null || jsonContractProperty.ShouldSerialize(value))) + if (jsonProperty.ShouldSerialize is not null && !jsonProperty.ShouldSerialize(inputObj)) + continue; + if (!jsonProperty.Readable) continue; - string propertyName = jsonContractProperty.PropertyName!; - object? propertyValue = jsonContractProperty.ValueProvider?.GetValue(value); + string propertyName = jsonProperty.PropertyName ?? jsonProperty.UnderlyingName!; + if (FLATTEN_PROPERTY_JSON_NAME.Equals(propertyName)) + { + if (jsonProperty.PropertyType != FlattenProperty.PropertyType) + throw new JsonException("Ambiguous flatten property found."); + + continue; + } + + object? propertyValue = jsonProperty.ValueProvider?.GetValue(inputObj); if (propertyValue is null) { if (serializer.NullValueHandling == NullValueHandling.Include) @@ -163,100 +190,71 @@ namespace Newtonsoft.Json.Converters writer.WritePropertyName(propertyName); writer.WriteNull(); } - - continue; - } - - if (!PROPERTY_NAME_NARRAY.Equals(propertyName)) - { - writer.WritePropertyName(propertyName); - - if (jsonContractProperty.Converter is not null && jsonContractProperty.Converter.CanWrite) - { - jsonContractProperty.Converter.WriteJson(writer, propertyValue, serializer); - } - else - { - serializer.Serialize(writer, propertyValue); - } } else { - JArray jArray = JArray.FromObject(propertyValue); - for (int i = 0, len = jArray.Count; i < len; i++) - { - JToken? jSubToken = jArray[i]; - if (jSubToken is null) - continue; + writer.WritePropertyName(propertyName); - foreach (JProperty jSubProperty in jSubToken) + if (jsonProperty.Converter is not null && jsonProperty.Converter.CanWrite) + jsonProperty.Converter.WriteJson(writer, propertyValue, serializer); + else + serializer.Serialize(writer, propertyValue); + } + } + + // 序列化扩展字段 + if (jsonContract.ExtensionDataGetter is not null) + { + throw new NotSupportedException($"'{GetType().FullName}' does not support JsonExtensionData."); + } + + // 序列化需扁平展开的字段 + Array? flattenArray = ReflectionHelper.GetPropertyValue(inputObj, FlattenProperty); + if (flattenArray is not null) + { + int index = 0; + foreach (JToken jToken in JArray.FromObject(flattenArray)) + { + if (jToken.Type != JTokenType.Null) + { + foreach (JProperty jProp in jToken) { - writer.WritePropertyName(jSubProperty.Name.Replace(PROPERTY_WILDCARD_NARRAY_ELEMENT, i.ToString())); - serializer.Serialize(writer, jSubProperty.Value); + string propertyName = jProp.Name.Replace("$n", index.ToString()); + if (jProp.Value is null || jProp.Value.Type == JTokenType.Null) + { + if (serializer.NullValueHandling == NullValueHandling.Include) + { + writer.WritePropertyName(propertyName); + writer.WriteNull(); + } + } + else + { + writer.WritePropertyName(propertyName); + serializer.Serialize(writer, jProp.Value); + } } } + + index++; } } writer.WriteEndObject(); } - private static InnerTypedJsonPropertyInfo[] GetTypedJsonProperties(Type type) + private JsonSerializer CopyJsonSerializer(JsonSerializer serializer, params JsonConverter?[] customConverters) { - if (type is null) throw new ArgumentNullException(nameof(type)); + JsonSerializer serializerCopy = JsonSerializer.Create(serializer.ExtractSerializerSettings()); + serializerCopy.Converters.Remove(this); - string mappedKey = type.AssemblyQualifiedName ?? type.GetHashCode().ToString(); - InnerTypedJsonPropertyInfo[]? mappedValue = (InnerTypedJsonPropertyInfo[]?)_mappedTypeJsonProperties[mappedKey]; - - if (mappedValue is null) + foreach (JsonConverter? converter in customConverters) { - mappedValue = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(p => - { - if (p.CanWrite || p.GetCustomAttributes(inherit: true).Any()) - { - return !p.GetCustomAttributes(inherit: false).Any(); - } - - return false; - }) - .Select(p => - { - string name = p.GetCustomAttribute(inherit: true)?.PropertyName ?? p.Name; - IEnumerable converters = p.GetCustomAttributes(inherit: true) - .OrderByDescending(attr => attr.IsDefaultAttribute()) - .Select(attr => (JsonConverter)Activator.CreateInstance(attr.ConverterType, attr.ConverterParameters)!); - JsonConverter? readConverter = converters.FirstOrDefault(c => c.CanRead); - JsonConverter? writeConverter = converters.FirstOrDefault(c => c.CanWrite); - return new InnerTypedJsonPropertyInfo - ( - propertyName: name, - propertyInfo: p, - propertyIsNArray: PROPERTY_NAME_NARRAY.Equals(name) && p.PropertyType.IsArray && p.PropertyType.GetElementType()!.IsClass, - jsonReadConverter: readConverter, - jsonWriteConverter: writeConverter - ); - }) - .OrderBy(e => e.PropertyInfo.GetCustomAttribute(inherit: true)?.Order) - .ToArray(); - _mappedTypeJsonProperties[mappedKey] = mappedValue; + if (converter is not null) + serializerCopy.Converters.Add(converter); } - return mappedValue; - } - - private static bool TryMatchNArrayIndex(string key, out int index) - { - Regex regex = new Regex(@"(_)(\d+)$", RegexOptions.Compiled); - if (regex.IsMatch(key)) - { - string str = regex.Match(key).Groups[2].Value; - index = int.Parse(str); - return true; - } - - index = -1; - return false; + return serializerCopy; } } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/__Internal/System.Text.Json/Object/FlattenNArrayObjectConverterBase.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/__Internal/System.Text.Json/Object/FlattenNArrayObjectConverterBase.cs index 5ea510d9..804f3ddf 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/__Internal/System.Text.Json/Object/FlattenNArrayObjectConverterBase.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Converters/__Internal/System.Text.Json/Object/FlattenNArrayObjectConverterBase.cs @@ -3,265 +3,24 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Common; using System.Text.RegularExpressions; -namespace System.Text.Json.Converters +namespace System.Text.Json.Serialization.Internal { using SKIT.FlurlHttpClient.Wechat.TenpayV2.Utilities; - internal static class FlattenNArrayObjectConverterBase + internal abstract partial class FlattenNArrayObjectConverterBase : JsonConverter + where TModelContainer : class, new() + where TFlattenElement : class, new() { - public const string PROPERTY_WILDCARD_NARRAY_ELEMENT = Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_WILDCARD_NARRAY_ELEMENT; - public const string PROPERTY_NAME_NARRAY = Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY; - } - - internal class FlattenNArrayObjectConverterBase : JsonConverter - where T : class, new() - { - private sealed class InnerTypedJsonPropertyInfo - { - public string PropertyName { get; } - - public PropertyInfo PropertyInfo { get; } - - public Type PropertyType { get { return PropertyInfo.PropertyType; } } - - public bool PropertyIsNArray { get; } - - public JsonConverter? JsonConverter { get; } - - public InnerTypedJsonPropertyInfo(string propertyName, PropertyInfo propertyInfo, bool propertyIsNArray, JsonConverter? jsonConverter) - { - PropertyName = propertyName; - PropertyInfo = propertyInfo; - PropertyIsNArray = propertyIsNArray; - JsonConverter = jsonConverter; - } - } - - private const string PROPERTY_WILDCARD_NARRAY_ELEMENT = FlattenNArrayObjectConverterBase.PROPERTY_WILDCARD_NARRAY_ELEMENT; - private const string PROPERTY_NAME_NARRAY = FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY; - - private static readonly Hashtable _mappedTypeJsonProperties = new Hashtable(); - - public override bool CanConvert(Type typeToConvert) - { - return typeToConvert.IsClass && !typeToConvert.IsAbstract && !typeToConvert.IsInterface; - } - - public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Null) - { - return default; - } - else if (reader.TokenType == JsonTokenType.StartObject) - { - InnerTypedJsonPropertyInfo[] typedJsonProperties = GetTypedJsonProperties(typeToConvert); - //if (typedJsonProperties.Count(p => p.PropertyIsNArray) != 1) - // throw new JsonException("The count of `$n` properties must be only one."); - - JsonElement jElement = JsonDocument.ParseValue(ref reader).RootElement.Clone(); - T tObject = new T(); - - foreach (JsonProperty jProperty in jElement.EnumerateObject()) - { - InnerTypedJsonPropertyInfo? typedJsonPropertyInfo = typedJsonProperties.SingleOrDefault(e => e.PropertyName == jProperty.Name); - if (typedJsonPropertyInfo is not null) - { - JsonSerializerOptions tmpOptions = GetClonedJsonSerializerOptions(options, typedJsonPropertyInfo.JsonConverter); - object? propertyValue = jProperty.Value.Deserialize(typedJsonPropertyInfo.PropertyType, tmpOptions); - typedJsonPropertyInfo.PropertyInfo.SetValue(tObject, propertyValue); - } - else if (TryMatchNArrayIndex(jProperty.Name, out int index)) - { - foreach (var _ in typedJsonProperties.Where(e => e.PropertyIsNArray)) - { - typedJsonPropertyInfo = _; - - Array? propertyValue = typedJsonPropertyInfo.PropertyInfo.GetValue(tObject) as Array; - ReflectionHelper.CreateOrExpandArray(ref propertyValue, typedJsonPropertyInfo.PropertyType.GetElementType()!, index + 1); - ReflectionHelper.CreateOrExpandArrayElement(propertyValue!, index, (object element) => - { - InnerTypedJsonPropertyInfo? insider = GetTypedJsonProperties(element.GetType()) - .SingleOrDefault(p => string.Equals(p.PropertyName.Replace(PROPERTY_WILDCARD_NARRAY_ELEMENT, index.ToString()), jProperty.Name)); - if (insider is not null) - { - JsonSerializerOptions tmpOptions = GetClonedJsonSerializerOptions(options, insider.JsonConverter); - object? elementPropertyValue = JsonSerializer.Deserialize(jProperty.Value, insider.PropertyType, tmpOptions)!; - insider.PropertyInfo.SetValue(element, elementPropertyValue); - } - - return element; - }); - - typedJsonPropertyInfo.PropertyInfo.SetValue(tObject, propertyValue); - } - } - } - return tObject; - } - - throw new JsonException(); - } - - public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) - { - if (value is null) - { - writer.WriteNullValue(); - return; - } - - writer.WriteStartObject(); - - foreach (InnerTypedJsonPropertyInfo typedJsonProperty in GetTypedJsonProperties(value.GetType())) - { - if (!typedJsonProperty.PropertyIsNArray) - { - string propertyName = typedJsonProperty.PropertyName; - object? propertyValue = typedJsonProperty.PropertyInfo.GetValue(value); - if (propertyValue is null) - { - if (options.DefaultIgnoreCondition == JsonIgnoreCondition.Always || options.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull) - continue; - } - if (propertyValue == (typedJsonProperty.PropertyType.IsValueType ? Activator.CreateInstance(typedJsonProperty.PropertyType) : null)) - { - if (options.DefaultIgnoreCondition == JsonIgnoreCondition.Always || options.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingDefault) - continue; - } - - JsonSerializerOptions tmpOptions = GetClonedJsonSerializerOptions(options, typedJsonProperty.JsonConverter); - if (typedJsonProperty.JsonConverter is not null && ReflectionHelper.CheckTypeIsSubclassOf(typedJsonProperty.JsonConverter.GetType(), typeof(StringifiedObjectInJsonFormatConverter))) - { - // TODO: 优化 - tmpOptions.Converters.Remove(typedJsonProperty.JsonConverter); - writer.WritePropertyName(tmpOptions.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName); - writer.WriteStringValue(JsonSerializer.Serialize(propertyValue, tmpOptions)); - } - else - { - writer.WritePropertyName(tmpOptions.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName); - writer.WriteRawValue(JsonSerializer.Serialize(propertyValue, tmpOptions), skipInputValidation: true); - } - } - else - { - Array? array = (Array?)typedJsonProperty.PropertyInfo.GetValue(value); - if (array is null) - continue; - - for (int i = 0, len = array.Length; i < len; i++) - { - object? element = array.GetValue(i); - if (element is null) - continue; - - JsonSerializerOptions tmpOptions = GetClonedJsonSerializerOptions(options, GetTypedJsonConverter(element.GetType())); - JsonObject jSubObject = JsonSerializer.SerializeToNode(element, tmpOptions)!.AsObject(); - foreach (KeyValuePair jSubProperty in jSubObject) - { - string subPropertyName = jSubProperty.Key.Replace(PROPERTY_WILDCARD_NARRAY_ELEMENT, i.ToString()); - JsonNode? subPropertyValue = jSubProperty.Value; - writer.WritePropertyName(tmpOptions.PropertyNamingPolicy?.ConvertName(subPropertyName) ?? subPropertyName); - writer.WriteRawValue(subPropertyValue?.ToJsonString(tmpOptions)!, skipInputValidation: true); - } - } - } - } - writer.WriteEndObject(); - } - - private static InnerTypedJsonPropertyInfo[] GetTypedJsonProperties(Type type) - { - if (type is null) throw new ArgumentNullException(nameof(type)); - - string mappedKey = type.AssemblyQualifiedName ?? type.GetHashCode().ToString(); - InnerTypedJsonPropertyInfo[]? mappedValue = (InnerTypedJsonPropertyInfo[]?)_mappedTypeJsonProperties[mappedKey]; - - if (mappedValue is null) - { - mappedValue = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(p => - { - if (p.CanWrite || p.GetCustomAttributes(inherit: true).Any()) - { - return !p.GetCustomAttributes(inherit: false).Any(); - } - - return false; - }) - .Select(p => - { - string name = p.GetCustomAttribute(inherit: true)?.Name ?? p.Name; - return new InnerTypedJsonPropertyInfo - ( - propertyName: name, - propertyInfo: p, - propertyIsNArray: PROPERTY_NAME_NARRAY.Equals(name) && p.PropertyType.IsArray && p.PropertyType.GetElementType()!.IsClass, - jsonConverter: GetTypedJsonConverter(p) - ); - }) - .OrderBy(e => e.PropertyInfo.GetCustomAttribute(inherit: true)?.Order) - .ToArray(); - _mappedTypeJsonProperties[mappedKey] = mappedValue; - } - - return mappedValue; - } - - private static JsonConverter? GetTypedJsonConverter(MemberInfo? memberInfo) - { - if (memberInfo is null) - return null; - - return memberInfo.GetCustomAttributes(inherit: true) - .OrderByDescending(attr => attr.IsDefaultAttribute()) - .Select(attr => - { - JsonConverter? converter = null; - - if (memberInfo is Type type) - { - converter = attr.CreateConverter(type); - } - else if (memberInfo is PropertyInfo propertyInfo) - { - converter = attr.CreateConverter(propertyInfo.PropertyType); - } - - if (converter is null && attr.ConverterType is not null) - { - converter = (JsonConverter)Activator.CreateInstance(attr.ConverterType)!; - } - - return converter; - }) - .FirstOrDefault(converter => converter is not null); - } - - private static JsonSerializerOptions GetClonedJsonSerializerOptions(JsonSerializerOptions options, JsonConverter? converter) - { - JsonSerializerOptions optionsCopy = options; - - if (converter is not null) - { - optionsCopy = new JsonSerializerOptions(options); - optionsCopy.Converters.Add(converter); - } - - return optionsCopy; - } - + public const string FLATTEN_PROPERTY_JSON_NAME = "#n"; + private static bool TryMatchNArrayIndex(string key, out int index) { - Regex regex = new Regex(@"(_)(\d+)", RegexOptions.Compiled); + Regex regex = new Regex(@"_(\d+)$", RegexOptions.Compiled); if (regex.IsMatch(key)) { - string str = regex.Match(key).Groups[2].Value; + string str = regex.Match(key).Groups[1].Value; index = int.Parse(str); return true; } @@ -269,5 +28,356 @@ namespace System.Text.Json.Converters index = -1; return false; } + + /// + /// 获取 `` 类下需要扁平展开的属性,该属性需是一个数组类型、其中每个元素是一个 ``。 + /// + protected abstract PropertyInfo FlattenProperty { get; } + + public override bool CanConvert(Type typeToConvert) + { + if (!FlattenProperty.PropertyType.IsArray) + throw new NotSupportedException(); + + return typeToConvert.IsClass && !typeToConvert.IsAbstract && !typeToConvert.IsInterface + && typeof(TModelContainer).IsAssignableFrom(typeToConvert); + } + + public override TModelContainer? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return default; + } + else if (reader.TokenType == JsonTokenType.StartObject) + { + TModelContainer outputObj = ReflectionHelper.CreateInstance(); + + // 读取为 JsonObject + JsonObject jObject = JsonObject.Parse(ref reader, new JsonNodeOptions() { PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive })!.AsObject(); + if (!jObject.Any()) + return outputObj; + + // 取得下标最大值(-1 表示无需扁平展开) + int maxIndex = jObject.Select(e => TryMatchNArrayIndex(e.Key, out int index) ? index : -1).Max(); + + // 解析 JSON 结构 + JsonPropertyTypeInfo[] jsonPropertyTypes = JsonPropertyTypeInfoCache.GetOrAddJsonPropertyTypes(typeToConvert, options); + + // 遍历 JsonObject 并反序列化 + foreach (KeyValuePair jProp in jObject) + { + string propertyName = jProp.Key; + + if (maxIndex != -1 && TryMatchNArrayIndex(propertyName, out int index)) + { + // 反序列化需扁平展开的字段 + JsonPropertyTypeInfo[] flattenJsonPropertyTypes = JsonPropertyTypeInfoCache.GetOrAddJsonPropertyTypes(FlattenProperty.PropertyType.GetElementType()!, options); + JsonPropertyTypeInfo? jsonPropertyType = flattenJsonPropertyTypes.FirstOrDefault(e => propertyName == e.JsonPropertyName.Replace("$n", index.ToString())); + if (jsonPropertyType is null) + { + if (options.UnmappedMemberHandling == JsonUnmappedMemberHandling.Disallow) + throw new JsonException($"The JSON property '{propertyName}' could not be mapped to any .NET member contained in type '{typeToConvert.FullName}'"); + } + else + { + Array? array = ReflectionHelper.GetPropertyValue(outputObj, FlattenProperty); + if (array is null) + { + array = new TFlattenElement[maxIndex + 1]; + ReflectionHelper.SetPropertyValue(outputObj, FlattenProperty, array); + } + + TFlattenElement? element = ReflectionHelper.GetElementValue(array, index); + if (element is null) + { + element = ReflectionHelper.CreateInstance(); + ReflectionHelper.SetElementValue(array, index, element); + } + + object? elementPropertyValue = jProp.Value?.Deserialize(jsonPropertyType.PropertyType, CopyJsonSerializerOptions(options, jsonPropertyType.JsonConverter)); + ReflectionHelper.SetPropertyValue(element, jsonPropertyType.Property, elementPropertyValue); + } + } + else + { + JsonPropertyTypeInfo? jsonPropertyType = jsonPropertyTypes.FirstOrDefault(e => propertyName == e.JsonPropertyName); + if (jsonPropertyType is null && options.PropertyNameCaseInsensitive) + jsonPropertyType = jsonPropertyTypes.FirstOrDefault(e => string.Equals(propertyName, e.JsonPropertyName, StringComparison.OrdinalIgnoreCase)); + + // 反序列化普通字段 + if (jsonPropertyType is not null) + { + object? propertyValue = jProp.Value?.Deserialize(jsonPropertyType.PropertyType, CopyJsonSerializerOptions(options, jsonPropertyType.JsonConverter)); + ReflectionHelper.SetPropertyValue(outputObj, jsonPropertyType.Property, propertyValue); + } + // 反序列化扩展字段 + else if (jsonPropertyTypes.Any(e => e.IsExtensionData)) + { + throw new NotSupportedException($"'{GetType().FullName}' does not support JsonExtensionData."); + } + } + } + + return outputObj; + } + + throw new JsonException($"Unexpected JSON token type '{reader.TokenType}' when reading."); + } + + public override void Write(Utf8JsonWriter writer, TModelContainer? inputObj, JsonSerializerOptions options) + { + if (inputObj is null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartObject(); + + // 解析 JSON 结构 + JsonPropertyTypeInfo[] jsonPropertyTypes = JsonPropertyTypeInfoCache.GetOrAddJsonPropertyTypes(inputObj.GetType(), options); + + // 序列化普通字段 + foreach (JsonPropertyTypeInfo jsonPropertyType in jsonPropertyTypes) + { + if (jsonPropertyType.IsExtensionData) + continue; + + string propertyName = jsonPropertyType.JsonPropertyName; + if (FLATTEN_PROPERTY_JSON_NAME.Equals(propertyName)) + { + if (jsonPropertyType.Property != FlattenProperty) + throw new JsonException("Ambiguous flatten property found."); + + continue; + } + + object? propertyValue = ReflectionHelper.GetPropertyValue(inputObj, jsonPropertyType.Property); + if (propertyValue is null) + { + if (options.DefaultIgnoreCondition == JsonIgnoreCondition.Always || options.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingNull) + continue; + } + else if (propertyValue == (jsonPropertyType.PropertyType.IsValueType ? ReflectionHelper.CreateInstance() : null)) + { + if (options.DefaultIgnoreCondition == JsonIgnoreCondition.Always || options.DefaultIgnoreCondition == JsonIgnoreCondition.WhenWritingDefault) + continue; + } + + writer.WritePropertyName(options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName); + writer.WriteRawValue(JsonSerializer.Serialize(propertyValue, CopyJsonSerializerOptions(options, jsonPropertyType.JsonConverter)), skipInputValidation: true); + } + + // 序列化扩展字段 + if (jsonPropertyTypes.Any(e => e.IsExtensionData)) + { + throw new NotSupportedException($"'{GetType().FullName}' does not support JsonExtensionData."); + } + + // 序列化需扁平展开的字段 + Array? flattenArray = ReflectionHelper.GetPropertyValue(inputObj, FlattenProperty); + if (flattenArray is not null) + { + for (int index = 0; index < flattenArray.Length; index++) + { + object? element = ReflectionHelper.GetElementValue(flattenArray, index); + if (element is null) + continue; + + JsonObject jObject = JsonSerializer.SerializeToNode(element, CopyJsonSerializerOptions(options))!.AsObject(); + foreach (KeyValuePair jProp in jObject) + { + string propertyName = jProp.Key.Replace("$n", index.ToString()); + JsonNode? propertyValueNode = jProp.Value; + if (propertyValueNode is null || propertyValueNode.GetValueKind() == JsonValueKind.Null) + { + if (options.DefaultIgnoreCondition == JsonIgnoreCondition.Never) + { + writer.WritePropertyName(options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName); + writer.WriteNullValue(); + } + } + else + { + writer.WritePropertyName(options.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName); + writer.WriteRawValue(propertyValueNode?.ToJsonString(options)!, skipInputValidation: true); + } + } + } + } + + writer.WriteEndObject(); + } + + private JsonSerializerOptions CopyJsonSerializerOptions(JsonSerializerOptions options, params JsonConverter?[] customConverters) + { + JsonSerializerOptions optionsCopy = new JsonSerializerOptions(options); + optionsCopy.Converters.Remove(this); + + foreach (JsonConverter? converter in customConverters) + { + if (converter is not null) + optionsCopy.Converters.Add(converter); + } + + return optionsCopy; + } + } + + partial class FlattenNArrayObjectConverterBase + { + private class JsonPropertyTypeInfo + { + public PropertyInfo Property { get; } + + public Type PropertyType { get { return Property.PropertyType; } } + + public JsonIgnoreCondition JsonIgnoreCondition { get; } + + public JsonConverter? JsonConverter { get; } + + public string JsonPropertyName { get; } + + public int? JsonPropertyOrder { get; } + + public bool IsExtensionData { get; } + + public JsonPropertyTypeInfo(PropertyInfo property, JsonIgnoreCondition jsonIgnoreCondition, JsonConverter? jsonConverter, string jsonPropertyName, int? jsonPropertyOrder) + { + Property = property ?? throw new ArgumentNullException(nameof(property)); + JsonIgnoreCondition = jsonIgnoreCondition; + JsonConverter = jsonConverter; + JsonPropertyName = jsonPropertyName; + JsonPropertyOrder = jsonPropertyOrder; + IsExtensionData = property.IsDefined(typeof(JsonExtensionDataAttribute), inherit: true); + } + } + + private static class JsonPropertyTypeInfoCache + { + private static readonly Hashtable _cache = new Hashtable(capacity: 32); + + private static Type[] GetSortedTypeHierarchy(Type type) + { + if (!type.IsInterface) + { + IList results = new List(); + for (Type? current = type; current is not null; current = current.BaseType) + { + results.Add(current); + } + + return results.ToArray(); + } + else + { + throw new NotSupportedException(); + } + } + + private static PropertyInfo[] GetSerializableProperties(Type typeToConvert) + { + IList results = new List(); + + foreach (PropertyInfo propertyInfo in typeToConvert.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + if (propertyInfo.GetIndexParameters().Length > 0 || + (propertyInfo.GetMethod?.IsAbstract == true || propertyInfo.SetMethod?.IsAbstract == true)) + { + continue; + } + + bool hasJsonIncludeAttribute = propertyInfo.GetCustomAttribute(inherit: false) is not null; + if (propertyInfo.GetMethod?.IsPublic == true || + propertyInfo.SetMethod?.IsPublic == true || + hasJsonIncludeAttribute) + { + results.Add(propertyInfo); + } + } + + return results.ToArray(); + } + + private static JsonIgnoreCondition GetPropertyJsonIgnoreCondition(PropertyInfo propertyInfo, JsonSerializerOptions jsonSerializerOptions) + { + JsonIgnoreAttribute? ignoreAttribute = propertyInfo.GetCustomAttribute(inherit: false); + if (ignoreAttribute is not null) + return ignoreAttribute.Condition; + + return jsonSerializerOptions.DefaultIgnoreCondition; + } + + private static JsonConverter? GetPropertyJsonConverter(PropertyInfo propertyInfo, JsonSerializerOptions jsonSerializerOptions) + { + JsonConverterAttribute? converterAttribute = propertyInfo.GetCustomAttribute(inherit: false); + if (converterAttribute is null) + return null; + + Type? converterType = converterAttribute.ConverterType; + if (converterType is null) + return converterAttribute.CreateConverter(propertyInfo.PropertyType) + ?? converterAttribute.CreateConverter(propertyInfo.ReflectedType ?? propertyInfo.DeclaringType!) + ?? jsonSerializerOptions.GetConverter(propertyInfo.PropertyType); + else + return (JsonConverter)Activator.CreateInstance(converterType)!; + } + + private static int? GetPropertyJsonOrder(PropertyInfo propertyInfo, JsonSerializerOptions jsonSerializerOptions) + { + JsonPropertyOrderAttribute? orderAttribute = propertyInfo.GetCustomAttribute(inherit: true); + if (orderAttribute is not null) + return orderAttribute.Order; + + return null; + } + + private static string GetPropertyJsonName(PropertyInfo propertyInfo, JsonSerializerOptions jsonSerializerOptions) + { + JsonPropertyNameAttribute? nameAttribute = propertyInfo.GetCustomAttribute(inherit: true); + if (nameAttribute is not null) + return nameAttribute.Name; + else if (jsonSerializerOptions.PropertyNamingPolicy is not null) + return jsonSerializerOptions.PropertyNamingPolicy.ConvertName(propertyInfo.Name); + else + return propertyInfo.Name; + } + + public static JsonPropertyTypeInfo[] GetOrAddJsonPropertyTypes(Type typeToConvert, JsonSerializerOptions options) + { + JsonPropertyTypeInfo[]? results = _cache[typeToConvert] as JsonPropertyTypeInfo[]; + + if (results is null) + { + IList temp = new List(); + + foreach (Type currentType in GetSortedTypeHierarchy(typeToConvert)) + { + if (currentType == typeof(Object) || + currentType == typeof(ValueType)) + { + break; + } + + foreach (PropertyInfo propertyInfo in GetSerializableProperties(typeToConvert)) + { + JsonIgnoreCondition jsonIgnoreCondition = GetPropertyJsonIgnoreCondition(propertyInfo, options); + JsonConverter? jsonConverter = GetPropertyJsonConverter(propertyInfo, options); + string jsonPropertyName = GetPropertyJsonName(propertyInfo, options); + int? jsonPropertyOrder = GetPropertyJsonOrder(propertyInfo, options); + + if (!temp.Any(e => e.JsonPropertyName == jsonPropertyName)) + temp.Add(new JsonPropertyTypeInfo(property: propertyInfo, jsonIgnoreCondition: jsonIgnoreCondition, jsonConverter: jsonConverter, jsonPropertyName: jsonPropertyName, jsonPropertyOrder: jsonPropertyOrder)); + } + } + + results = temp.OrderBy(e => e.JsonPropertyOrder).ToArray(); + _cache[typeToConvert] = results; + } + + return results; + } + } } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Events/OrderEvent.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Events/OrderEvent.cs index a390a92e..881150e5 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Events/OrderEvent.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Events/OrderEvent.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Events { @@ -38,15 +39,19 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Events internal static class Converters { - internal class EventClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase + internal class EventClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => OrderEvent._flattenProperty; } - internal class EventClassSystemTextJsonConverter : System.Text.Json.Converters.FlattenNArrayObjectConverterBase + internal class EventClassSystemTextJsonConverter : System.Text.Json.Serialization.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => OrderEvent._flattenProperty; } } + private readonly static PropertyInfo _flattenProperty = typeof(OrderEvent).GetProperty(nameof(CouponList), BindingFlags.Instance | BindingFlags.Public)!; + /// /// 获取或设置子商户号。 /// @@ -161,8 +166,8 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Events /// /// 获取或设置代金券使用列表。 /// - [Newtonsoft.Json.JsonProperty(Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] - [System.Text.Json.Serialization.JsonPropertyName(System.Text.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] + [Newtonsoft.Json.JsonProperty(Converters.EventClassNewtonsoftJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] + [System.Text.Json.Serialization.JsonPropertyName(Converters.EventClassSystemTextJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] public Types.Coupon[]? CouponList { get; set; } /// diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Events/PAPOrderEvent.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Events/PAPOrderEvent.cs index d62f0b1f..1b6085bb 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Events/PAPOrderEvent.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Events/PAPOrderEvent.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Reflection; namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Events { @@ -18,15 +19,19 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Events internal static class Converters { - internal class EventClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase + internal class EventClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => PAPOrderEvent._flattenProperty; } - internal class EventClassSystemTextJsonConverter : System.Text.Json.Converters.FlattenNArrayObjectConverterBase + internal class EventClassSystemTextJsonConverter : System.Text.Json.Serialization.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => PAPOrderEvent._flattenProperty; } } + private readonly static PropertyInfo _flattenProperty = typeof(PAPOrderEvent).GetProperty(nameof(CouponList), BindingFlags.Instance | BindingFlags.Public)!; + /// /// 获取或设置子商户号。 /// @@ -119,8 +124,8 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Events /// /// 获取或设置代金券使用列表。 /// - [Newtonsoft.Json.JsonProperty(Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] - [System.Text.Json.Serialization.JsonPropertyName(System.Text.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] + [Newtonsoft.Json.JsonProperty(Converters.EventClassNewtonsoftJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] + [System.Text.Json.Serialization.JsonPropertyName(Converters.EventClassSystemTextJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] public Types.Coupon[]? CouponList { get; set; } /// diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/QueryMerchantCustomsCustomDeclarationResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/QueryMerchantCustomsCustomDeclarationResponse.cs index 4906e379..599bb491 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/QueryMerchantCustomsCustomDeclarationResponse.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/MerchantCustoms/QueryMerchantCustomsCustomDeclarationResponse.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models { @@ -113,15 +114,19 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models internal static class Converters { - internal class ResponseClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase + internal class ResponseClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => QueryMerchantCustomsCustomDeclarationResponse._flattenProperty; } - internal class ResponseClassSystemTextJsonConverter : System.Text.Json.Converters.FlattenNArrayObjectConverterBase + internal class ResponseClassSystemTextJsonConverter : System.Text.Json.Serialization.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => QueryMerchantCustomsCustomDeclarationResponse._flattenProperty; } } + private readonly static PropertyInfo _flattenProperty = typeof(QueryMerchantCustomsCustomDeclarationResponse).GetProperty(nameof(RecordList), BindingFlags.Instance | BindingFlags.Public)!; + /// /// /// @@ -154,8 +159,8 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models /// /// 获取或设置记录列表。 /// - [Newtonsoft.Json.JsonProperty(Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] - [System.Text.Json.Serialization.JsonPropertyName(System.Text.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] + [Newtonsoft.Json.JsonProperty(Converters.ResponseClassNewtonsoftJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] + [System.Text.Json.Serialization.JsonPropertyName(Converters.ResponseClassSystemTextJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] public Types.Record[] RecordList { get; set; } = default!; /// diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/CreatePayRefundResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/CreatePayRefundResponse.cs index d097a6ee..9c09bd53 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/CreatePayRefundResponse.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/CreatePayRefundResponse.cs @@ -1,3 +1,5 @@ +using System.Reflection; + namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models { /// @@ -9,7 +11,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models { public static class Types { - public class Coupon + public class RefundCoupon { /// /// 获取或设置代金券 ID。 @@ -36,15 +38,19 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models internal static class Converters { - internal class ResponseClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase + internal class ResponseClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => CreatePayRefundResponse._flattenProperty; } - internal class ResponseClassSystemTextJsonConverter : System.Text.Json.Converters.FlattenNArrayObjectConverterBase + internal class ResponseClassSystemTextJsonConverter : System.Text.Json.Serialization.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => CreatePayRefundResponse._flattenProperty; } } + private readonly static PropertyInfo _flattenProperty = typeof(CreatePayRefundResponse).GetProperty(nameof(CouponRefundList), BindingFlags.Instance | BindingFlags.Public)!; + /// /// /// @@ -167,9 +173,9 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models /// /// 获取或设置代金券退款列表。 /// - [Newtonsoft.Json.JsonProperty(Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] - [System.Text.Json.Serialization.JsonPropertyName(System.Text.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] - public Types.Coupon[]? CouponRefundList { get; set; } + [Newtonsoft.Json.JsonProperty(Converters.ResponseClassNewtonsoftJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] + [System.Text.Json.Serialization.JsonPropertyName(Converters.ResponseClassSystemTextJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] + public Types.RefundCoupon[]? CouponRefundList { get; set; } /// /// 获取或设置现金支付金额(单位:分)。 diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/GetPayOrderResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/GetPayOrderResponse.cs index d34f1a3e..05756ea0 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/GetPayOrderResponse.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/GetPayOrderResponse.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reflection; namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models { @@ -227,15 +228,19 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models internal static class Converters { - internal class ResponseClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase + internal class ResponseClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => GetPayOrderResponse._flattenProperty; } - internal class ResponseClassSystemTextJsonConverter : System.Text.Json.Converters.FlattenNArrayObjectConverterBase + internal class ResponseClassSystemTextJsonConverter : System.Text.Json.Serialization.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => GetPayOrderResponse._flattenProperty; } } + private readonly static PropertyInfo _flattenProperty = typeof(GetPayOrderResponse).GetProperty(nameof(CouponList), BindingFlags.Instance | BindingFlags.Public)!; + /// /// /// @@ -381,8 +386,8 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models /// /// 获取或设置代金券使用列表。 /// - [Newtonsoft.Json.JsonProperty(Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] - [System.Text.Json.Serialization.JsonPropertyName(System.Text.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] + [Newtonsoft.Json.JsonProperty(Converters.ResponseClassNewtonsoftJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] + [System.Text.Json.Serialization.JsonPropertyName(Converters.ResponseClassSystemTextJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] public Types.Coupon[]? CouponList { get; set; } /// @@ -447,9 +452,5 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models [System.Text.Json.Serialization.JsonPropertyName("promotion_detail")] [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.Common.StringifiedObjectInJsonFormatConverter))] public Types.PromotionDetail? PromotionDetail { get; set; } - - [Newtonsoft.Json.JsonProperty] - [Newtonsoft.Json.JsonExtensionData] - public IDictionary? ExtensionData { get; set; } } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/GetPayRefundResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/GetPayRefundResponse.cs index 046788b5..178de023 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/GetPayRefundResponse.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Pay/GetPayRefundResponse.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models { @@ -92,15 +93,19 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models internal static class Converters { - internal class ResponseClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase + internal class ResponseClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => GetPayRefundResponse._flattenProperty; } - internal class ResponseClassSystemTextJsonConverter : System.Text.Json.Converters.FlattenNArrayObjectConverterBase + internal class ResponseClassSystemTextJsonConverter : System.Text.Json.Serialization.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => GetPayRefundResponse._flattenProperty; } } + private readonly static PropertyInfo _flattenProperty = typeof(GetPayRefundResponse).GetProperty(nameof(RefundList), BindingFlags.Instance | BindingFlags.Public)!; + /// /// /// @@ -185,8 +190,8 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models /// /// 获取或设置记录列表。 /// - [Newtonsoft.Json.JsonProperty(Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] - [System.Text.Json.Serialization.JsonPropertyName(System.Text.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] + [Newtonsoft.Json.JsonProperty(Converters.ResponseClassNewtonsoftJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] + [System.Text.Json.Serialization.JsonPropertyName(Converters.ResponseClassSystemTextJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] public Types.Refund[] RefundList { get; set; } = default!; /// diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Vehicle/GetTransitOrderResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Vehicle/GetTransitOrderResponse.cs index 679cf06b..b842b319 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Vehicle/GetTransitOrderResponse.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/Vehicle/GetTransitOrderResponse.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models { @@ -18,15 +19,19 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models internal static class Converters { - internal class ResponseClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase + internal class ResponseClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => GetTransitOrderResponse._flattenProperty; } - internal class ResponseClassSystemTextJsonConverter : System.Text.Json.Converters.FlattenNArrayObjectConverterBase + internal class ResponseClassSystemTextJsonConverter : System.Text.Json.Serialization.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => GetTransitOrderResponse._flattenProperty; } } + private readonly static PropertyInfo _flattenProperty = typeof(GetTransitOrderResponse).GetProperty(nameof(CouponList), BindingFlags.Instance | BindingFlags.Public)!; + /// /// /// @@ -142,8 +147,8 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models /// /// 获取或设置代金券使用列表。 /// - [Newtonsoft.Json.JsonProperty(Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] - [System.Text.Json.Serialization.JsonPropertyName(System.Text.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] + [Newtonsoft.Json.JsonProperty(Converters.ResponseClassNewtonsoftJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] + [System.Text.Json.Serialization.JsonPropertyName(Converters.ResponseClassSystemTextJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] public Types.Coupon[]? CouponList { get; set; } /// diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/VehiclePartner/GetTransitPartnerOrderResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/VehiclePartner/GetTransitPartnerOrderResponse.cs index 89675f71..ebfecaa3 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/VehiclePartner/GetTransitPartnerOrderResponse.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Models/VehiclePartner/GetTransitPartnerOrderResponse.cs @@ -1,3 +1,6 @@ +using System; +using System.Reflection; + namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models { /// @@ -5,7 +8,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models /// [Newtonsoft.Json.JsonConverter(typeof(Converters.ResponseClassNewtonsoftJsonConverter))] [System.Text.Json.Serialization.JsonConverter(typeof(Converters.ResponseClassSystemTextJsonConverter))] - public class GetTransitPartnerOrderResponse : GetTransitOrderResponse + public class GetTransitPartnerOrderResponse : WechatTenpaySignableResponse { public static new class Types { @@ -14,17 +17,43 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models } } - internal static new class Converters + internal static class Converters { - internal class ResponseClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase + internal class ResponseClassNewtonsoftJsonConverter : Newtonsoft.Json.Converters.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => GetTransitPartnerOrderResponse._flattenProperty; } - internal class ResponseClassSystemTextJsonConverter : System.Text.Json.Converters.FlattenNArrayObjectConverterBase + internal class ResponseClassSystemTextJsonConverter : System.Text.Json.Serialization.Internal.FlattenNArrayObjectConverterBase { + protected override PropertyInfo FlattenProperty => GetTransitPartnerOrderResponse._flattenProperty; } } + private readonly static PropertyInfo _flattenProperty = typeof(GetTransitPartnerOrderResponse).GetProperty(nameof(CouponList), BindingFlags.Instance | BindingFlags.Public)!; + + /// + /// + /// + [Newtonsoft.Json.JsonProperty("mch_id")] + [System.Text.Json.Serialization.JsonPropertyName("mch_id")] +#pragma warning disable CS8618 +#pragma warning disable CS8765 + public override string MerchantId { get; set; } +#pragma warning restore CS8765 +#pragma warning restore CS8618 + + /// + /// + /// + [Newtonsoft.Json.JsonProperty("appid")] + [System.Text.Json.Serialization.JsonPropertyName("appid")] +#pragma warning disable CS8618 +#pragma warning disable CS8765 + public override string AppId { get; set; } +#pragma warning restore CS8765 +#pragma warning restore CS8618 + /// /// 获取或设置子商户号。 /// @@ -39,11 +68,146 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Models [System.Text.Json.Serialization.JsonPropertyName("sub_appid")] public string? SubAppId { get; set; } + /// + /// 获取或设置终端设备号。 + /// + [Newtonsoft.Json.JsonProperty("device_info")] + [System.Text.Json.Serialization.JsonPropertyName("device_info")] + public string? DeviceInfo { get; set; } + + /// + /// 获取或设置用户唯一标识。 + /// + [Newtonsoft.Json.JsonProperty("openid")] + [System.Text.Json.Serialization.JsonPropertyName("openid")] + public string OpenId { get; set; } = default!; + + /// + /// 获取或设置用户是否订阅该公众号标识。 + /// + [Newtonsoft.Json.JsonProperty("is_subscribe")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.Internal.YesOrNoBooleanConverter))] + [System.Text.Json.Serialization.JsonPropertyName("is_subscribe")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.Internal.YesOrNoBooleanConverter))] + public bool? IsSubscribed { get; set; } + + /// + /// 获取或设置交易状态。 + /// + [Newtonsoft.Json.JsonProperty("trade_state")] + [System.Text.Json.Serialization.JsonPropertyName("trade_state")] + public string TradeState { get; set; } = default!; + + /// + /// 获取或设置交易状态描述。 + /// + [Newtonsoft.Json.JsonProperty("trade_state_desc")] + [System.Text.Json.Serialization.JsonPropertyName("trade_state_desc")] + public string TradeStateDescription { get; set; } = default!; + + /// + /// 获取或设置交易类型。 + /// + [Newtonsoft.Json.JsonProperty("trade_type")] + [System.Text.Json.Serialization.JsonPropertyName("trade_type")] + public string TradeType { get; set; } = default!; + + /// + /// 获取或设置付款银行。 + /// + [Newtonsoft.Json.JsonProperty("bank_type")] + [System.Text.Json.Serialization.JsonPropertyName("bank_type")] + public string BankType { get; set; } = default!; + + /// + /// 获取或设置订单金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("total_fee")] + [System.Text.Json.Serialization.JsonPropertyName("total_fee")] + [System.Text.Json.Serialization.JsonNumberHandling(System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString)] + public int TotalFee { get; set; } + + /// + /// 获取或设置货币类型。 + /// + [Newtonsoft.Json.JsonProperty("fee_type")] + [System.Text.Json.Serialization.JsonPropertyName("fee_type")] + public string? FeeType { get; set; } + + /// + /// 获取或设置应结订单金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("settlement_total_fee")] + [System.Text.Json.Serialization.JsonPropertyName("settlement_total_fee")] + [System.Text.Json.Serialization.JsonNumberHandling(System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString)] + public int? SettlementFee { get; set; } + + /// + /// 获取或设置代金券金额。 + /// + [Newtonsoft.Json.JsonProperty("coupon_fee")] + [System.Text.Json.Serialization.JsonPropertyName("coupon_fee")] + [System.Text.Json.Serialization.JsonNumberHandling(System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString)] + public int? CouponFee { get; set; } + + /// + /// 获取或设置代金券使用数量。 + /// + [Newtonsoft.Json.JsonProperty("coupon_count")] + [System.Text.Json.Serialization.JsonPropertyName("coupon_count")] + [System.Text.Json.Serialization.JsonNumberHandling(System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString)] + public int? CouponCount { get; set; } + /// /// 获取或设置代金券使用列表。 /// - [Newtonsoft.Json.JsonProperty(Newtonsoft.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] - [System.Text.Json.Serialization.JsonPropertyName(System.Text.Json.Converters.FlattenNArrayObjectConverterBase.PROPERTY_NAME_NARRAY)] - public new Types.Coupon[]? CouponList { get; set; } + [Newtonsoft.Json.JsonProperty(Converters.ResponseClassNewtonsoftJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] + [System.Text.Json.Serialization.JsonPropertyName(Converters.ResponseClassSystemTextJsonConverter.FLATTEN_PROPERTY_JSON_NAME)] + public Types.Coupon[]? CouponList { get; set; } + + /// + /// 获取或设置现金支付金额(单位:分)。 + /// + [Newtonsoft.Json.JsonProperty("cash_fee")] + [System.Text.Json.Serialization.JsonPropertyName("cash_fee")] + [System.Text.Json.Serialization.JsonNumberHandling(System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString)] + public int? CashFee { get; set; } + + /// + /// 获取或设置现金支付货币类型。 + /// + [Newtonsoft.Json.JsonProperty("cash_fee_type")] + [System.Text.Json.Serialization.JsonPropertyName("cash_fee_type")] + public string? CashFeeType { get; set; } + + /// + /// 获取或设置商户订单号。 + /// + [Newtonsoft.Json.JsonProperty("out_trade_no")] + [System.Text.Json.Serialization.JsonPropertyName("out_trade_no")] + public string OutTradeNumber { get; set; } = default!; + + /// + /// 获取或设置微信支付订单号。 + /// + [Newtonsoft.Json.JsonProperty("transaction_id")] + [System.Text.Json.Serialization.JsonPropertyName("transaction_id")] + public string TransactionId { get; set; } = default!; + + /// + /// 获取或设置附加数据。 + /// + [Newtonsoft.Json.JsonProperty("attach")] + [System.Text.Json.Serialization.JsonPropertyName("attach")] + public string? Attachment { get; set; } + + /// + /// 获取或设置支付完成时间。 + /// + [Newtonsoft.Json.JsonProperty("time_end")] + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.Common.DigitalDateTimeOffsetConverter))] + [System.Text.Json.Serialization.JsonPropertyName("time_end")] + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.Common.DigitalDateTimeOffsetConverter))] + public DateTimeOffset? EndTime { get; set; } } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/__Internal/ReflectionHelper.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/__Internal/ReflectionHelper.cs index 69d41e0e..866f8fa0 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/__Internal/ReflectionHelper.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV2/Utilities/__Internal/ReflectionHelper.cs @@ -1,74 +1,92 @@ using System; -using System.Linq; +using System.Collections; +using System.Linq.Expressions; +using System.Reflection; namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.Utilities { internal static class ReflectionHelper { - public static void CreateOrExpandArray(ref Array? array, Type elementType, int capacity) - { - if (elementType is null) throw new ArgumentNullException(nameof(elementType)); - if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity)); + private readonly static Hashtable _ctorCache = new Hashtable(capacity: 128); + private readonly static Hashtable _getterCache = new Hashtable(capacity: 128); + private readonly static Hashtable _setterCache = new Hashtable(capacity: 128); - if (array is null) + public static T CreateInstance() where T : new() + { + // 提供比 new T() 或 Activator.CreateInstance() 更快的无参实例化方法 + // 只可针对热点类型使用,否则可能会更慢 + + Type type = typeof(T); + + Func? ctor = _ctorCache[type] as Func; + if (ctor is null) { - array = Array.CreateInstance(elementType, capacity); - } - else if (array.Length < capacity) - { - Array tmpArray = Array.CreateInstance(elementType, capacity); - Array.Copy(array, tmpArray, array.Length); - array = tmpArray; + ctor = Expression.Lambda>(Expression.New(typeof(T))).Compile(); + _ctorCache[type] = ctor; } + + return ctor.Invoke(); } - public static void CreateOrExpandArrayElement(Array array, int index, Func? updateFactory) + public static T? GetPropertyValue(object targetObj, PropertyInfo property) { - if (array is null) throw new ArgumentNullException(nameof(array)); - if (index < 0 || index >= array.Length) throw new ArgumentOutOfRangeException(nameof(index)); - if (updateFactory is null) throw new ArgumentNullException(nameof(updateFactory)); + // 提供比 PropertyInfo.GetValue() 更快的属性取值方法 + // 只可针对热点类型使用,否则可能会更慢 - object? element = array.GetValue(index); - if (element is null) + if (targetObj is null) throw new ArgumentNullException(nameof(targetObj)); + if (property is null) throw new ArgumentNullException(nameof(property)); + + if (!property.CanRead) + throw new InvalidOperationException($"Property '{property.Name}' of type '{typeof(T).FullName}' does not have a getter."); + + Func? getter = _getterCache[property] as Func; + if (getter is null) { - Type elementType = array.GetType().GetElementType()!; - - if (elementType.IsAbstract || elementType.IsInterface) - { - throw new NotSupportedException(); - } - else if (elementType.IsArray) - { - element = Array.CreateInstance(elementType, 0); - } - else - { - element = Activator.CreateInstance(elementType); - } + ParameterExpression targetExpr = Expression.Parameter(typeof(object)); + UnaryExpression castTargetExpr = Expression.Convert(targetExpr, targetObj.GetType()); + MemberExpression getPropertyValueExpr = Expression.Property(castTargetExpr, property); + UnaryExpression castPropertyValueExpr = Expression.Convert(getPropertyValueExpr, typeof(T)); + getter = Expression.Lambda>(castPropertyValueExpr, targetExpr).Compile(); + _getterCache[property] = getter; } - element = updateFactory(element!); + return getter.Invoke(targetObj); + } + + public static void SetPropertyValue(object targetObj, PropertyInfo property, T? value) + { + // 提供比 PropertyInfo.SetValue() 更快的属性赋值方法 + // 只可针对热点类型使用,否则可能会更慢 + + if (targetObj is null) throw new ArgumentNullException(nameof(targetObj)); + if (property is null) throw new ArgumentNullException(nameof(property)); + + if (!property.CanWrite) + throw new InvalidOperationException($"Property '{property.Name}' of type '{typeof(T).FullName}' does not have a setter."); + + Action? setter = _setterCache[property] as Action; + if (setter is null) + { + ParameterExpression targetExpr = Expression.Parameter(typeof(object)); + ParameterExpression propertyValueExpr = Expression.Parameter(typeof(T)); + UnaryExpression castTargetExpr = Expression.Convert(targetExpr, targetObj.GetType()); + UnaryExpression castPropertyValueExpr = Expression.Convert(propertyValueExpr, property.PropertyType); + MethodCallExpression setPropertyValueExpr = Expression.Call(castTargetExpr, property.GetSetMethod()!, castPropertyValueExpr); + setter = Expression.Lambda>(setPropertyValueExpr, targetExpr, propertyValueExpr).Compile(); + _setterCache[property] = setter; + } + + setter.Invoke(targetObj, value); + } + + public static T? GetElementValue(Array array, int index) + { + return (T?)array.GetValue(index); + } + + public static void SetElementValue(Array array, int index, T? element) + { array.SetValue(element, index); } - - public static bool CheckTypeIsSubclassOf(Type childType, Type baseType) - { - bool IsTheRawGenericType(Type type) - => baseType == (type.IsGenericType ? type.GetGenericTypeDefinition() : type); - - bool isTheRawGenericType = childType.GetInterfaces().Any(IsTheRawGenericType); - if (isTheRawGenericType) - return true; - - Type? t = childType; - while (t is not null && t != typeof(object)) - { - isTheRawGenericType = IsTheRawGenericType(t); - if (isTheRawGenericType) return true; - t = t.BaseType; - } - - return false; - } } } diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestCase_JsonConverterOfFlattenNArrayObjectTest.cs b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestCase_JsonConverterOfFlattenNArrayObjectTest.cs index 2d7ede38..f9d2f342 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestCase_JsonConverterOfFlattenNArrayObjectTest.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests/TestCase_JsonConverterOfFlattenNArrayObjectTest.cs @@ -81,19 +81,32 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV2.UnitTests Assert.DoesNotContain("#n", actualJson); Assert.DoesNotContain("$n", actualJson); + Assert.DoesNotContain("_flattenProperty", actualJson); + Assert.DoesNotContain(nameof(Models.QueryMerchantCustomsCustomDeclarationResponse.RecordList), actualJson); - Assert.Contains("\"return_code\"", actualJson); - Assert.Contains("\"RETURN_CODE\"", actualJson); - Assert.Contains("\"return_msg\"", actualJson); - Assert.Contains("\"RETURN_MSG\"", actualJson); - Assert.Contains("\"sub_order_no_0\"", actualJson); - Assert.Contains("\"SUB_ORDER_NO_0\"", actualJson); - Assert.Contains("\"sub_order_id_0\"", actualJson); - Assert.Contains("\"SUB_ORDER_ID_0\"", actualJson); - Assert.Contains("\"sub_order_no_1\"", actualJson); - Assert.Contains("\"SUB_ORDER_NO_1\"", actualJson); - Assert.Contains("\"sub_order_id_1\"", actualJson); - Assert.Contains("\"SUB_ORDER_ID_1\"", actualJson); + Assert.Contains("\"return_code\":\"RETURN_CODE\"", actualJson); + Assert.Contains("\"return_msg\":\"RETURN_MSG\"", actualJson); + Assert.Contains("\"appid\":\"APPID\"", actualJson); + Assert.Contains("\"mch_id\":\"MCH_ID\"", actualJson); + Assert.Contains("\"result_code\":\"RESULT_CODE\"", actualJson); + Assert.Contains("\"err_code\":\"ERR_CODE\"", actualJson); + Assert.Contains("\"err_code_des\":\"ERR_CODE_DESC\"", actualJson); + Assert.Contains("\"transaction_id\":\"TRANSACTION_ID\"", actualJson); + Assert.Contains("\"sub_order_no_0\":\"SUB_ORDER_NO_0\"", actualJson); + Assert.Contains("\"sub_order_id_0\":\"SUB_ORDER_ID_0\"", actualJson); + Assert.Contains("\"mch_customs_no_0\":\"MCH_CUSTOMS_NO_0\"", actualJson); + Assert.Contains("\"customs_0\":\"CUSTOMS_0\"", actualJson); + Assert.Contains("\"duty_0\":10", actualJson); + Assert.Contains("\"fee_type_0\":\"FEE_TYPE_0\"", actualJson); + Assert.Contains("\"modify_time_0\":\"20000101112233\"", actualJson); + Assert.Contains("\"sub_order_no_1\":\"SUB_ORDER_NO_1\"", actualJson); + Assert.Contains("\"sub_order_id_1\":\"SUB_ORDER_ID_1\"", actualJson); + Assert.Contains("\"mch_customs_no_1\":\"MCH_CUSTOMS_NO_1\"", actualJson); + Assert.Contains("\"customs_1\":\"CUSTOMS_1\"", actualJson); + Assert.Contains("\"duty_1\":11", actualJson); + Assert.Contains("\"fee_type_1\":\"FEE_TYPE_1\"", actualJson); + Assert.Contains("\"modify_time_1\":\"20010101112233\"", actualJson); + Assert.Contains("\"count\":2", actualJson); }); }