Skip to content

Commit ff800a5

Browse files
authored
Add support for explicit IParsable<T>.TryParse in RDF. (#49317)
* Add support for explicit IParsable<T>.TryParse in RDF.
1 parent 1b16209 commit ff800a5

File tree

4 files changed

+71
-2
lines changed

4 files changed

+71
-2
lines changed

src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
#nullable enable
55

6-
using System.Diagnostics.CodeAnalysis;
76
using System.Globalization;
87
using System.Linq.Expressions;
98
using System.Reflection;
@@ -165,6 +164,14 @@ public void HasTryParseStringMethod_ReturnsTrueWhenMethodExists(ParameterInfo pa
165164
Assert.True(new ParameterBindingMethodCache().HasTryParseMethod(parameterInfo.ParameterType));
166165
}
167166

167+
[Fact]
168+
public void FindTryParseStringMethod_FindsExplicitlyImplementedIParsable()
169+
{
170+
var type = typeof(TodoWithExplicitIParsable);
171+
var methodFound = new ParameterBindingMethodCache().FindTryParseMethod(type);
172+
Assert.NotNull(methodFound);
173+
}
174+
168175
[Fact]
169176
public void FindTryParseStringMethod_WorksForEnums()
170177
{
@@ -1516,4 +1523,17 @@ public MockParameterInfo(Type type, string name)
15161523
NameImpl = name;
15171524
}
15181525
}
1526+
1527+
public class TodoWithExplicitIParsable : IParsable<TodoWithExplicitIParsable>
1528+
{
1529+
static TodoWithExplicitIParsable IParsable<TodoWithExplicitIParsable>.Parse(string s, IFormatProvider? provider)
1530+
{
1531+
throw new NotImplementedException();
1532+
}
1533+
1534+
static bool IParsable<TodoWithExplicitIParsable>.TryParse(string? s, IFormatProvider? provider, out TodoWithExplicitIParsable result)
1535+
{
1536+
throw new NotImplementedException();
1537+
}
1538+
}
15191539
}

src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateCreationTests.TryParse.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public static object[][] TryParsableParameters
5555
new object[] { "MyEnum", "ValueB", MyEnum.ValueB },
5656
new object[] { "MyTryParseRecord", "https://example.org", new MyTryParseRecord(new Uri("https://example.org")) },
5757
new object[] { "int?", "42", 42 },
58-
new object[] { "int?", null, null },
58+
new object[] { "int?", null, null }
5959
};
6060
}
6161
}
@@ -200,6 +200,23 @@ public async Task MapAction_SingleComplexTypeParam_StringReturn()
200200
await VerifyAgainstBaselineUsingFile(compilation);
201201
}
202202

203+
[Fact]
204+
public async Task MapAction_ExplicitIParsable()
205+
{
206+
var (results, compilation) = await RunGeneratorAsync("""
207+
app.MapGet("/hello", ([FromQuery]TodoWithExplicitIParsable p, HttpContext context) => context.Items["p"] = p);
208+
""");
209+
var endpoint = GetEndpointFromCompilation(compilation);
210+
211+
var httpContext = CreateHttpContext();
212+
httpContext.Request.QueryString = new QueryString("?p=1");
213+
214+
await endpoint.RequestDelegate(httpContext);
215+
var p = httpContext.Items["p"];
216+
217+
Assert.NotNull(p);
218+
}
219+
203220
[Fact]
204221
public async Task MapAction_SingleEnumParam_StringReturn()
205222
{

src/Http/Http.Extensions/test/RequestDelegateGenerator/SharedTypes.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
using Microsoft.Extensions.DependencyInjection;
1313
using Microsoft.AspNetCore.Mvc;
1414
using Microsoft.AspNetCore.Routing;
15+
using System.Diagnostics.CodeAnalysis;
16+
using Microsoft.Diagnostics.Runtime.Interop;
1517

1618
namespace Microsoft.AspNetCore.Http.Generators.Tests;
1719

@@ -1014,3 +1016,17 @@ public class TodoChild : Todo
10141016
public string? Child { get; set; }
10151017
}
10161018
#nullable restore
1019+
1020+
public class TodoWithExplicitIParsable : IParsable<TodoWithExplicitIParsable>
1021+
{
1022+
static TodoWithExplicitIParsable IParsable<TodoWithExplicitIParsable>.Parse(string s, IFormatProvider provider)
1023+
{
1024+
return new TodoWithExplicitIParsable();
1025+
}
1026+
1027+
static bool IParsable<TodoWithExplicitIParsable>.TryParse(string s, IFormatProvider provider, out TodoWithExplicitIParsable result)
1028+
{
1029+
result = new TodoWithExplicitIParsable();
1030+
return true;
1031+
}
1032+
}

src/Shared/ParameterBindingMethodCache.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ public bool HasBindAsyncMethod(ParameterInfo parameter) =>
8080
{
8181
MethodInfo? methodInfo;
8282

83+
if (TryGetExplicitIParsableTryParseMethod(type, out var explicitIParsableTryParseMethod))
84+
{
85+
return (expression, formatProvider) => Expression.Call(explicitIParsableTryParseMethod, TempSourceStringExpr, formatProvider, expression);
86+
}
87+
8388
if (type.IsEnum)
8489
{
8590
if (_enumTryParseMethod.IsGenericMethod)
@@ -416,6 +421,17 @@ static bool ValidateReturnType(MethodInfo methodInfo)
416421
return TValue.BindAsync(httpContext, parameter);
417422
}
418423

424+
[RequiresUnreferencedCode("Performs reflection on type hierarchy. This cannot be statically analyzed.")]
425+
private static bool TryGetExplicitIParsableTryParseMethod(Type type, out MethodInfo methodInfo)
426+
{
427+
// Nested types by default use + as the delimeter between the containing type and the
428+
// inner type. However when doing a method search this '+' symbol needs to be a '.' symbol.
429+
var typeName = TypeNameHelper.GetTypeDisplayName(type, fullName: true, nestedTypeDelimiter: '.');
430+
var name = $"System.IParsable<{typeName}>.TryParse";
431+
methodInfo = type.GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic)!;
432+
return methodInfo is not null;
433+
}
434+
419435
[RequiresUnreferencedCode("Performs reflection on type hierarchy. This cannot be statically analyzed.")]
420436
private MethodInfo? GetStaticMethodFromHierarchy(Type type, string name, Type[] parameterTypes, Func<MethodInfo, bool> validateReturnType)
421437
{

0 commit comments

Comments
 (0)