dotnet / dotnet/aspnetcore

MinimalAPI explicit IParsable.TryParse resolution fails on generic custom types

Open
#58,136 0 comments 2 reactions 0 assignees View on GitHub
area-minimal
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 10h
Merged PRs (30d)
281

Description

### Is there an existing issue for this?

- [X] I have searched the existing issues

### Describe the bug

If a custom type is used as a route segment parameter, an ASP.NET Core web application will fail to start with the error
```
An unhandled exception has occurred while executing the request.
System.InvalidOperationException: TryParse method found on MyTypedId with incorrect format. Must be a static method with format
bool TryParse(string, IFormatProvider, out MyTypedId)
bool TryParse(string, out MyTypedId)
but found
static Boolean TryParse(System.String, System.IFormatProvider, MyTypedId ByRef)
```

if the custom type is a generic type but implements `System.IParsable<>` explicitely.

```csharp
app.MapGet("/{myId}", ([FromRoute] MyTypedId myId) => $"Called with {myId.Guid}");

// ---

public interface ITypedGuid : IParsable
where TTypedGuid : struct, ITypedGuid
{
Guid Guid { get; }

static abstract implicit operator TTypedGuid(Guid guid);

///
static TTypedGuid IParsable.Parse(string s, IFormatProvider? provider) => Guid.Parse(s, provider);

///
static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out TTypedGuid result)
{
if (Guid.TryParse(s, provider, out var guid))
{
result = guid;
return true;
}

result = default;
return false;
}
}

public readonly struct MyTypedId : ITypedGuid
{
private MyTypedId(Guid guid) => Guid = guid;

///
public Guid Guid { get; }

///
public static implicit operator MyTypedId(Guid guid) => new(guid);
}
```

### Expected Behavior

The endpoint mapping will succeed because the type that is used as a route segment successfully implements `System.IParsable<>`.

FIY: The analyzer works fine in all cases and recognizes the implementation.

### Steps To Reproduce

Reproduction repo: https://github.com/rklfss/aspnetcore-rdf-tryparse-failure

There are three different endpoint mappings inside of which
- one does not work if IParsable is implemented explicitely by the generic interface itself
- one does not work if IParsable is implemented explicitely on a geneic type/class
- one does work if IParsable is implemented implicitely on a generic type

Start project with one of them in use.

### Exceptions (if any)

For the non working endpoint mappings this is:

```
An unhandled exception has occurred while executing the request.
System.InvalidOperationException: TryParse method found on MyTypedId with incorrect format. Must be a static method with format
bool TryParse(string, IFormatProvider, out MyTypedId)
bool TryParse(string, out MyTypedId)
but found
static Boolean TryParse(System.String, System.IFormatProvider, MyTypedId ByRef)
```

### .NET Version

8.0.303

### Anything else?

Cause of the error is the method that searches for the explicitely implemented TryParse method in ParameterBindingMethodCache:

```csharp
private static bool TryGetExplicitIParsableTryParseMethod(Type type, out MethodInfo methodInfo)
{
// Nested types by default use + as the delimeter between the containing type and the
// inner type. However when doing a method search this '+' symbol needs to be a '.' symbol.
var typeName = TypeNameHelper.GetTypeDisplayName(type, fullName: true, nestedTypeDelimiter: '.');
var name = $"System.IParsable<{typeName}>.TryParse";
methodInfo = type.GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic)!;
return methodInfo is not null;
}
```

as the name the method tries to search for is `System.IParsable.TryParse` but if implemented in a generic type the name of the method is `System.IParsable.TryParse` with the type paramater name in place, not the actual type name.

A possible solution would be to use the interface mapping to search for the implementation. This will not only take care of this but will find the correct implementation of IParsable<>.TryParse no matter whether the implementation is implicit or explicit.

```csharp
private static bool TryGetImplementedIParsableTryParseMethod(Type type, [MaybeNullWhen(false)] out MethodInfo methodInfo)
{
var desiredIParsableType = typeof(IParsable<>).MakeGenericType(type);

if (type.IsAssignableTo(desiredIParsableType))
{
var interfaceMapping = type.GetInterfaceMap(desiredIParsableType);
var index = Array.FindLastIndex(interfaceMapping.InterfaceMethods, m => m.Name == "TryParse");
if (index != -1)
{
methodInfo = interfaceMapping.TargetMethods[index];
return true;
}
}

methodInfo = null;
return false;
}
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.