Skip to content

Commit 78ccc69

Browse files
authored
Merge pull request #6 from Rabadash8820/many-members
Can now bind ValueTuples with more than 8 members
2 parents f5fb23e + d40f66e commit 78ccc69

File tree

2 files changed

+110
-30
lines changed

2 files changed

+110
-30
lines changed

M6T.Core.TupleModelBinder/TupleModelBinder.cs

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
using Microsoft.AspNetCore.Mvc.ModelBinding;
1+
using Microsoft.AspNetCore.Mvc.ModelBinding;
22
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
33
using Newtonsoft.Json;
44
using Newtonsoft.Json.Linq;
55
using System;
6+
using System.Collections.Generic;
67
using System.IO;
78
using System.Linq;
89
using System.Reflection;
@@ -32,15 +33,24 @@ public IModelBinder GetBinder(ModelBinderProviderContext context)
3233

3334
public class TupleModelBinder : IModelBinder
3435
{
36+
private static readonly Type[] VALUE_TUPLE_TYPES = new[] {
37+
typeof(ValueTuple<>),
38+
typeof(ValueTuple<,>),
39+
typeof(ValueTuple<,,>),
40+
typeof(ValueTuple<,,,>),
41+
typeof(ValueTuple<,,,,>),
42+
typeof(ValueTuple<,,,,,>),
43+
typeof(ValueTuple<,,,,,,>),
44+
typeof(ValueTuple<,,,,,,,>),
45+
};
46+
3547
public async Task BindModelAsync(ModelBindingContext bindingContext)
3648
{
3749
if (bindingContext == null)
3850
{
3951
throw new ArgumentNullException(nameof(bindingContext));
4052
}
4153

42-
//var modelName = bindingContext.ModelName;
43-
4454
var reader = new StreamReader(bindingContext.HttpContext.Request.Body);
4555

4656
var body = await reader.ReadToEndAsync();
@@ -51,39 +61,49 @@ public async Task BindModelAsync(ModelBindingContext bindingContext)
5161
if (tupleAttr == null)
5262
{
5363
bindingContext.Result = ModelBindingResult.Failed();
64+
return;
5465
}
55-
else
56-
{
57-
var tupleType = bindingContext.ModelType;
58-
object tuple = ParseTupleFromModelAttributes(body, tupleAttr, tupleType);
59-
bindingContext.Result = ModelBindingResult.Success(tuple);
60-
}
61-
}
6266

63-
public static object ParseTupleFromModelAttributes(string body, TupleElementNamesAttribute tupleAttr, Type tupleType)
64-
{
6567
var jobj = JObject.Parse(body);
66-
var parameters = tupleAttr.TransformNames.Zip(tupleType.GetConstructors()
67-
.Single()
68-
.GetParameters())
69-
.Select(x => GetValue(jobj, x.First, x.Second))
70-
.ToArray();
68+
int parameterIndex = 0;
69+
object tuple = ParseTupleFromModelAttributes(jobj, tupleAttr.TransformNames, bindingContext.ModelType, ref parameterIndex);
7170

72-
object tuple = Activator.CreateInstance(tupleType, parameters);
71+
bindingContext.Result = ModelBindingResult.Success(tuple);
72+
}
73+
74+
public static object ParseTupleFromModelAttributes(JObject jobject, IList<string> parameterNames, Type parameterType, ref int parameterIndex)
75+
{
76+
if (parameterType.GenericTypeArguments.Length > VALUE_TUPLE_TYPES.Length)
77+
throw new InvalidOperationException($"Cannot bind model of ValueTuple with more than {VALUE_TUPLE_TYPES.Length} generic type arguments.");
7378

74-
return tuple;
79+
bool isNestedValueTuple = parameterType.GenericTypeArguments.Length == VALUE_TUPLE_TYPES.Length;
80+
int numSimpleGenericTypeArgs = Math.Min(VALUE_TUPLE_TYPES.Length - 1, parameterType.GenericTypeArguments.Length);
81+
object[] parameters = new object[parameterType.GenericTypeArguments.Length];
82+
for (int a = 0; a < numSimpleGenericTypeArgs; ++a)
83+
{
84+
parameters[a] = GetValue(jobject, parameterNames[parameterIndex], parameterType.GenericTypeArguments[a]);
85+
++parameterIndex;
86+
}
87+
if (isNestedValueTuple)
88+
{
89+
Type nestedValueTupleType = parameterType.GenericTypeArguments[VALUE_TUPLE_TYPES.Length - 1];
90+
parameters[VALUE_TUPLE_TYPES.Length - 1] = ParseTupleFromModelAttributes(jobject, parameterNames, nestedValueTupleType, ref parameterIndex);
91+
}
92+
Type genericValueTupleType = VALUE_TUPLE_TYPES[parameterType.GenericTypeArguments.Length - 1];
93+
Type concreteValueTupleType = genericValueTupleType.MakeGenericType(parameterType.GenericTypeArguments);
94+
return Activator.CreateInstance(concreteValueTupleType, parameters);
7595
}
7696

77-
static object GetValue(JObject jobject, string name, ParameterInfo info)
97+
static object GetValue(JObject jobject, string name, Type parameterType)
7898
{
7999
var value = jobject.GetValue(name, StringComparison.CurrentCultureIgnoreCase);
80100

81101
if (value == null || value.Type == JTokenType.Null)
82102
{
83-
if (IsNullable(info.ParameterType))
84-
return Convert.ChangeType(null, info.ParameterType);
103+
if (IsNullable(parameterType))
104+
return Convert.ChangeType(null, parameterType);
85105
else
86-
return Activator.CreateInstance(info.ParameterType); //default value
106+
return Activator.CreateInstance(parameterType); //default value
87107
}
88108

89109
/*
@@ -92,15 +112,15 @@ static object GetValue(JObject jobject, string name, ParameterInfo info)
92112
* This currently supports all Guid format types stated in
93113
* https://docs.microsoft.com/en-us/dotnet/api/system.guid.tostring?view=net-5.0
94114
*/
95-
if (info.ParameterType == typeof(Guid) && value.Type == JTokenType.String)
115+
if (parameterType == typeof(Guid) && value.Type == JTokenType.String)
96116
{
97117
return Guid.Parse(value.ToString());
98118
}
99119

100-
if (info.ParameterType.IsPrimitive || info.ParameterType == typeof(string) || info.ParameterType == typeof(decimal))
101-
return Convert.ChangeType(value, info.ParameterType);
120+
if (parameterType.IsPrimitive || parameterType == typeof(string) || parameterType == typeof(decimal))
121+
return Convert.ChangeType(value, parameterType);
102122
else
103-
return Convert.ChangeType(JsonConvert.DeserializeObject(value.ToString(), info.ParameterType), info.ParameterType);
123+
return Convert.ChangeType(JsonConvert.DeserializeObject(value.ToString(), parameterType), parameterType);
104124
}
105125

106126
static bool IsNullable(Type type)

ModelBinderTests/ModelBinderTests.cs

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using M6T.Core.TupleModelBinder;
2+
using Newtonsoft.Json.Linq;
23
using System;
34
using System.Collections.Generic;
45
using System.Runtime.CompilerServices;
@@ -45,11 +46,13 @@ public void TestTupleModelBinder()
4546
}";
4647

4748

49+
int parameterIndex = 0;
50+
var jobj = JObject.Parse(body);
4851
var tupleElementNames = (TupleElementNamesAttribute)prop.GetCustomAttributes(typeof(TupleElementNamesAttribute), true)[0];
4952

5053
var result =
5154
((TestUserClass User, string SomeData, string NullCheck, bool BooleanCheck, TestUserClass ComplexNullCheck))
52-
TupleModelBinder.ParseTupleFromModelAttributes(body, tupleElementNames, tupleType);
55+
TupleModelBinder.ParseTupleFromModelAttributes(jobj, tupleElementNames.TransformNames, tupleType, ref parameterIndex);
5356

5457
Assert.NotNull(result.User);
5558
Assert.Equal("Test", result.User.String);
@@ -63,6 +66,54 @@ public void TestTupleModelBinder()
6366
Assert.True(result.BooleanCheck);
6467
}
6568

69+
[Fact]
70+
public void TestNestedTupleModelBinder()
71+
{
72+
var type = typeof(NestedTupleMemberTestData);
73+
var prop = type.GetProperty("Value");
74+
var tupleType = prop.PropertyType;
75+
string body = @"
76+
{
77+
""User"" : {
78+
""String"":""Test"",
79+
""Integer"":444,
80+
""Double"": 1.44,
81+
""Decimal"": 1.44
82+
},
83+
""SomeData"" : ""Test String Root"",
84+
""NullCheck"": null,
85+
""BooleanCheck"": true,
86+
""ComplexNullCheck"":null,
87+
""IntParam6"": 6,
88+
""IntParam7"": 7,
89+
""IntParam8"": 8,
90+
""IntParam9"": 9,
91+
}";
92+
93+
int parameterIndex = 0;
94+
var jobj = JObject.Parse(body);
95+
var tupleElementNames = (TupleElementNamesAttribute)prop.GetCustomAttributes(typeof(TupleElementNamesAttribute), true)[0];
96+
97+
var result =
98+
((TestUserClass User, string SomeData, string NullCheck, bool BooleanCheck, TestUserClass ComplexNullCheck, int IntParam6, int IntParam7, int IntParam8, int IntParam9))
99+
TupleModelBinder.ParseTupleFromModelAttributes(jobj, tupleElementNames.TransformNames, tupleType, ref parameterIndex);
100+
101+
Assert.NotNull(result.User);
102+
Assert.Equal("Test", result.User.String);
103+
Assert.Equal(444, result.User.Integer);
104+
Assert.Equal(1.44d, result.User.Double);
105+
Assert.Equal(1.44m, result.User.Decimal);
106+
107+
Assert.Equal("Test String Root", result.SomeData);
108+
Assert.Null(result.NullCheck);
109+
Assert.Null(result.ComplexNullCheck);
110+
Assert.True(result.BooleanCheck);
111+
Assert.Equal(6, result.IntParam6);
112+
Assert.Equal(7, result.IntParam7);
113+
Assert.Equal(8, result.IntParam8);
114+
Assert.Equal(9, result.IntParam9);
115+
}
116+
66117
[Fact]
67118
public void TestNullHandling()
68119
{
@@ -76,11 +127,13 @@ public void TestNullHandling()
76127
""SomeNullData"":null
77128
}";
78129

130+
int parameterIndex = 0;
131+
var jobj = JObject.Parse(body);
79132
var tupleElementNames = (TupleElementNamesAttribute)prop.GetCustomAttributes(typeof(TupleElementNamesAttribute), true)[0];
80133

81134
var result =
82135
((string SomeData, string SomeNullData, string NullCheck, bool BooleanNullCheck, TestUserClass ComplexNullCheck))
83-
TupleModelBinder.ParseTupleFromModelAttributes(body, tupleElementNames, tupleType);
136+
TupleModelBinder.ParseTupleFromModelAttributes(jobj, tupleElementNames.TransformNames, tupleType, ref parameterIndex);
84137

85138
Assert.Equal("Test String Root", result.SomeData);
86139
Assert.Null(result.SomeNullData);
@@ -113,11 +166,13 @@ public void TestGuidHandling()
113166
string expectedGuid = testGuid.ToString(guidFormat);
114167
string body = @"{""clientId"":""" + expectedGuid + @""",""name"":""Name1"",""email"":""[email protected]""}";
115168

169+
int parameterIndex = 0;
170+
var jobj = JObject.Parse(body);
116171
var tupleElementNames = (TupleElementNamesAttribute)prop.GetCustomAttributes(typeof(TupleElementNamesAttribute), true)[0];
117172

118173
var result =
119174
((Guid clientId, string name, string email, string NullCheck, bool BooleanNullCheck, TestUserClass ComplexNullCheck))
120-
TupleModelBinder.ParseTupleFromModelAttributes(body, tupleElementNames, tupleType);
175+
TupleModelBinder.ParseTupleFromModelAttributes(jobj, tupleElementNames.TransformNames, tupleType, ref parameterIndex);
121176

122177
var resultGuid = result.clientId.ToString(guidFormat);
123178
Assert.Equal(expectedGuid, resultGuid);
@@ -139,6 +194,11 @@ public class TupleMemberTestData
139194
public (TestUserClass User, string SomeData, string NullCheck, bool BooleanCheck, TestUserClass ComplexNullCheck) Value { get; set; }
140195
}
141196

197+
public class NestedTupleMemberTestData
198+
{
199+
public (TestUserClass User, string SomeData, string NullCheck, bool BooleanCheck, TestUserClass ComplexNullCheck, int IntParam6, int IntParam7, int IntParam8, int IntParam9) Value { get; set; }
200+
}
201+
142202
public class NullMemberTestData
143203
{
144204
public (string SomeData, string SomeNullData, string NullCheck, bool BooleanNullCheck, TestUserClass ComplexNullCheck) Value { get; set; }

0 commit comments

Comments
 (0)