由於開發功能的需要,又懶得新建太多的class,所以ValueTuple是個比較好的偷懶方法,但是,由於WebApi需要返回序列化后的json,默認的序列化只能將ValueTuple定義的各個屬性序列化成Item1...n
但是微軟還是良心的為序列化留下入口,編譯器會在每個返回ValueTuple<>的函數或者屬性上,增加一個TupleElementNamesAttribute特性,該類的TransformNames就是存着所設置的屬性的名稱(強烈需要記住:是每個使用到ValueTuple的函數或者屬性才會添加,而不是加在有使用ValueTuple的類上),比如 (string str1,string str2) 那么 TransformNames=["str1","str2"],那么現在有如下一個class
public class A<T1,T2> { public T1 Prop1{set;get;}
public T2 Prop2{set;get;}
public (string str5,int int2) Prop3{set;get;} }
經過測試,如下一個函數
public A<(string str1,string str2),(string str3,string str4)> testApi(){}
這樣一個函數testApi 的會加上 TupleElementNamesAttribute 特性,,TransformNames=["str1","str2","str3","str4","str5","int2"],注意了,,這里只會添加一個TupleElementNamesAttribute特性,然后把A里所有的名字按定義的順序包含進去.
然后我們需要定義一個JsonConverter,用來專門針對一個函數或一個屬性的返回值進行了序列化
public class ValueTupleConverter : JsonConverter { private string[] _tupleNames = null; private NamingStrategy _strategy = null; //也可以直接在這里傳入特性 public ValueTupleConverter(TupleElementNamesAttribute tupleNames, NamingStrategy strategy = null) { _tupleNames = tupleNames.TransformNames.ToArrayEx(); _strategy = strategy; } //這里在構造函數里把需要序列化的屬性或函數返回類型的names傳進來 public ValueTupleConverter(string[] tupleNames, NamingStrategy strategy = null) { _tupleNames = tupleNames; _strategy = strategy; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { if (value != null && value is ITuple v) { writer.WriteStartObject(); for (int i = 0; i < v.Length; i++) { var pname = _tupleNames[i]; //根據規則,設置屬性名 writer.WritePropertyName(_strategy?.GetPropertyName(pname, true) ?? pname); if (v[i] == null) { writer.WriteNull(); } else { serializer.Serialize(writer, v[i]); } } writer.WriteEndObject(); } } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { //只需要實現序列化,,不需要反序列化,因為只管輸出,所以,這個寫不寫無所謂 throw new NotImplementedException(); } public override bool CanConvert(Type objectType) { return objectType.IsValueTuple(); } }
接下來說說實現的原理:
1.newtonsoft.json的組件里,有一個ContactResolver類,用於對不同的類的解析,類庫中自帶的DefaultContractResolver默認定義了將類解析成各個JsonProperty,利用這個類,可用於將ValueTuple的定義的名字當做屬性,返回給序列化器
2.asp.net core的Formatter,可以對Action輸出的對象進行格式化,一般用於比如json的格式化器或者xml格式化器的定義,利用格式化器,在Action最后輸出的時候,配合ContractResolver進行序列化
下面的實現中,很多地方需要判斷是否為ValueTuple,為了節省代碼,因此,先寫一個Helper:
public static class ValueTupleHelper { private static ConcurrentDictionary<Type,bool> _cacheIsValueTuple=new ConcurrentDictionary<Type, bool>(); public static bool IsValueTuple(this Type type) { return _cacheIsValueTuple.GetOrAdd(type, x => x.IsValueType && x.IsGenericType && (x.FullName.StartsWith("System.ValueTuple") || x.FullName ?.StartsWith("System.ValueTuple`") == true) ); } }
那么開始來定義一個ContractResolver,實現的原理請看注釋
public class CustomContractResolver : DefaultContractResolver { private MethodInfo _methodInfo = null; private IContractResolver _parentResolver = null; public CustomContractResolver(MethodInfo methodInfo, IContractResolver? parentContractResolver = null) { _methodInfo = methodInfo; _parentResolver = parentContractResolver; } public override JsonContract ResolveContract(Type type) { if (!type.GetProperties() .Where(x => x.CanRead && x.PropertyType.IsValueTuple()) .Any()) //如果Type類中不包含可讀的ValueTuple類型的屬性,則調用預定義的Resolver處理,當前Resolver只處理包含ValueTuple的類 { return _parentResolver?.ResolveContract(type); } var rc = base.ResolveContract(type); return rc; } public MethodInfo Method => _methodInfo; protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { //CreateProperty函數的結果,不需要額外加緩存,因為每個Method的返回Type,只會調用一次 JsonProperty property = base.CreateProperty(member, memberSerialization); //先調用默認的CreateProperty函數,創建出默認JsonProperty var pi = member as PropertyInfo; if (property.PropertyType.IsValueTuple()) { var attr = pi.GetCustomAttribute<TupleElementNamesAttribute>(); //獲取定義在屬性上的特性 if (attr != null) { //如果該屬性是已經編譯時有添加了TupleElementNamesAttribute特性的,,則不需要從method獲取 //這里主要是為了處理 (string str1,int int2) Prop3 這種情況 property.Converter = new ValueTupleConverter(attr, this.NamingStrategy); } else { //從輸入的method獲取,並且需要計算當前屬性所屬的泛型是在第幾個,然后計算出在TupleElementNamesAttribute.Names中的偏移 //這個主要是處理比如T2 Prop2 T2=ValueTuple的這種情況 var mAttr = (TupleElementNamesAttribute)_methodInfo.ReturnTypeCustomAttributes.GetCustomAttributes(typeof(TupleElementNamesAttribute), true).FirstOrDefault(); //用來獲取valueTuple的各個字段名稱 var basePropertyClass = pi.DeclaringType.GetGenericTypeDefinition(); //屬性定義的泛型基類 如 A<T1,T2> var basePropertyType = basePropertyClass.GetProperty(pi.Name)!.PropertyType; //獲取基類屬性的返回類型 就是T1 ,比如獲取在A<(string str1,string str2),(string str3,string str4)> 中 Prop1 返回的類型是對應基類中的T1還是T2 var index = basePropertyType.GenericParameterPosition;//獲取屬性所在的序號,用於計算 mAttr.Names中的偏移量 var skipNamesCount = (pi.DeclaringType as TypeInfo).GenericTypeArguments .Take(index) .Sum(x => x.IsValueTuple() ? x.GenericTypeArguments.Length : 0); ; //計算TupleElementNamesAttribute.TransformNames中當前類的偏移量 var names = mAttr.TransformNames .Skip(skipNamesCount) .Take(pi.PropertyType.GenericTypeArguments.Length) .ToArrayEx(); //獲取當前類的所有name property.Converter = new ValueTupleConverter(names, this.NamingStrategy); //傳入converter } property.GetIsSpecified = x => true; property.ItemConverter = property.Converter; //傳入converter property.ShouldSerialize = x => true; property.HasMemberAttribute = false; } return property; } protected override JsonConverter? ResolveContractConverter(Type objectType) //該函數可用於返回特定類型類型的JsonConverter { var type = base.ResolveContractConverter(objectType); //這里主要是為了忽略一些在class上定義了JsonConverter的情況,因為有些比如 A<T1,T2> 在序列化的時候,並無法知道ValueTuple定義的屬性名,這里添加忽略是為了跳過已定義過的JsonConverter //如有需要,可在這里多添加幾個 if (type is ResultReturnConverter) { return null; } else { return type; } } }
為了能兼容用於預先定義的ContractResolver,因此,先定義一個CompositeContractResolver,用於合並多個ContractResolver,可看可不看:

/// <summary> /// 合並多個IContractResolver,,並只返回第一個返回非null的Contract,如果所有列表中的ContractResolver都返回null,則調用DefaultContractResolver返回默認的JsonContract /// </summary> public class CompositeContractResolver : IContractResolver, IEnumerable<IContractResolver> { private readonly IList<IContractResolver> _contractResolvers = new List<IContractResolver>(); private static DefaultContractResolver _defaultResolver = new DefaultContractResolver(); private ConcurrentDictionary<Type, JsonContract> _cacheContractResolvers=new ConcurrentDictionary<Type, JsonContract>(); /// <summary> /// 返回列表中第一個返回非null的Contract,如果所有列表中的ContractResolver都返回null,則調用DefaultContractResolver返回默認的JsonContract /// </summary> /// <param name="type"></param> /// <returns></returns> public JsonContract ResolveContract(Type type) { return _cacheContractResolvers.GetOrAdd(type, m => { for (int i = 0; i < _contractResolvers.Count; i++) { var contact = _contractResolvers[i].ResolveContract(type); if (contact != null) { return contact; } } return _defaultResolver.ResolveContract(type); }); } public void Add(IContractResolver contractResolver) { if (contractResolver == null) return; _contractResolvers.Add(contractResolver); } public IEnumerator<IContractResolver> GetEnumerator() { return _contractResolvers.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
接下來,就該定義OutputFormatter了
public class ValueTupleOutputFormatter : TextOutputFormatter { private static ConcurrentDictionary<Type, bool> _canHandleType = new ConcurrentDictionary<Type, bool>(); //緩存一個Type是否能處理,提高性能,不用每次都判斷 private static ConcurrentDictionary<MethodInfo, JsonSerializerSettings> _cacheSettings = new ConcurrentDictionary<MethodInfo, JsonSerializerSettings>(); //用於緩存不同的函數的JsonSerializerSettings,各自定義,避免相互沖突 private Action<ValueTupleContractResolver> _resolverConfigFunc = null; /// <summary> /// /// </summary> /// <param name="resolverConfigFunc">用於在注冊Formatter的時候對ContractResolver進行配置修改,比如屬性名的大小寫之類的</param> public ValueTupleOutputFormatter(Action<ValueTupleContractResolver> resolverConfigFunc = null) { SupportedMediaTypes.Add("application/json"); SupportedMediaTypes.Add("text/json"); SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.Unicode); _resolverConfigFunc = resolverConfigFunc; } protected override bool CanWriteType(Type type) { return _canHandleType.GetOrAdd(type, t => { return type.GetProperties() //判斷該類是否包含有ValueTuple的屬性 .Where(x => x.CanRead && (CustomAttributeExtensions.GetCustomAttribute<TupleElementNamesAttribute>((MemberInfo) x) != null || x.PropertyType.IsValueTuple())) .Any(); }); } public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding) { var acce = (IActionContextAccessor)context.HttpContext.RequestServices.GetService(typeof(IActionContextAccessor)); #if NETCOREAPP2_1 var ac = acce.ActionContext.ActionDescriptor as ControllerActionDescriptor; #endif #if NETCOREAPP3_0 var endpoint = acce.ActionContext.HttpContext.GetEndpoint(); var ac = endpoint.Metadata.GetMetadata<ControllerActionDescriptor>(); //用來獲取當前Action對應的函數信息 #endif var settings = _cacheSettings.GetOrAdd(ac.MethodInfo, m => //這里主要是為了配置settings,每個methodinfo對應一個自己的settings,當然也就是每個MethodInfo一個CustomContractResolver,防止相互沖突 { var orgSettings = JsonConvert.DefaultSettings?.Invoke(); //獲取默認的JsonSettings var tmp = orgSettings != null ? cloneSettings(orgSettings) : new JsonSerializerSettings(); //如果不存在默認的,則new一個,如果已存在,則clone一個新的 var resolver = new ValueTupleContractResolver(m, tmp.ContractResolver is CompositeContractResolver ? null : tmp.ContractResolver); //創建自定義ContractResolver,傳入函數信息 _resolverConfigFunc?.Invoke(resolver); //調用配置函數 if (tmp.ContractResolver != null) //如果已定義過ContractResolver,則使用CompositeContractResolver進行合並 { if (tmp.ContractResolver is CompositeContractResolver c) //如果定義的是CompositeContractResolver,則直接插入到最前 { c.Insert(0, resolver); } else { tmp.ContractResolver = new CompositeContractResolver() { resolver, tmp.ContractResolver }; } } else { tmp.ContractResolver = new CompositeContractResolver() { resolver }; } return tmp; }); var json = JsonConvert.SerializeObject(context.Object, Formatting.None, settings); //調用序列化器進行序列化 await context.HttpContext.Response.Body.WriteAsync(selectedEncoding.GetBytes(json)); } private JsonSerializerSettings cloneSettings(JsonSerializerSettings settings) { var tmp = new JsonSerializerSettings(); var properties = settings.GetType().GetProperties(); foreach (var property in properties) { var pvalue = property.GetValue(settings); if (pvalue is ICloneable p2) { property.SetValue(tmp, p2.Clone()); } else { property.SetValue(tmp, pvalue); } } return tmp; } }
到此,該定義的類都定義完了,下面是注冊方法:在Start.cs中:
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(opt => { opt.OutputFormatters.Insert(0,new ValueTupleOutFormatter(x => { x.NamingStrategy= new CamelCaseNamingStrategy(true,true); //這里主要是為了演示對CustomContractResolver的配置,設置了所有屬性首字母小寫 })); }).AddNewtonsoftJson(); }
注冊完成后,用下面的Action可測試:

public class ApiTestController : ControllerBase { [FromBodyJson()] public IActionResult test1(List<(string productid,int qty)> details) { return Content("success"); } public ResultReturn<(string str1, int int3)> Test() { return new SuccessResultReturn<(string str1, int int3)>(("2222",222)); } public Test<(string Y1, string Y2), (string str1, string t2)> Test2() { return new Test< (string Y1, string Y2),(string str1, string t2)>(("111","22222"),("3333","44444") ); } }
總結一下,上面實現的原理是: 自定義一個OutputFormatter,在WriteResponseBodyAsync中,可以獲取到當前的Action對應的MethodInfo,然后利用編譯器在所有返回ValueTuple的地方,都加了TupleElementNamesAttribute的功能,獲取到使用時定義的ValueTuple各個Item的名字,再利用ContractResolver的CreateProperty功能,將定義的各個Item轉換為對應的name.然后使用newtonsoft的序列化器,進行json序列化.
以上代碼只能處理返回時,返回的類型為ValueTuple<T1...n>或者返回的類型中包含了ValueTuple<T1....n>的屬性,但是對於函數內,不用於返回的,則無法處理,比如
public object Test2() { var s= new Test< (string Y1, string Y2),(string str1, string t2)>(("111","22222"),("3333","44444") ); JsonConvert.SerializeObject(s); return null; }
這種情況的變量s的序列化就沒辦法了
部分代碼地址: