Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

mitchdenny
Copy link
Member

@mitchdenny mitchdenny commented Feb 16, 2023

Partially addresses: #46335

This is still a work in progress but I wanted to share the approach early to get feedback. Here I am primarily focused on getting an end to end scenario working (that is being able to call TryParse on a broad selection of types). So far the PR is supporting:

  • Not calling TryParse on strings ;)
  • Calling TryParse on Int32
  • Calling TryParse on enums.
  • Calling TryParse on complex types
  • Various DateTime, DateTimeOffset, DateOnly, TimeOnly types

For our contrived test cases it works, but I need to add the following to make it more bulletproof:

  • Validates that the TryParse method exists, I'll be pulling across ParsabilityHelper (to shared source) from our analyzer to help with this.
  • Selecting the right TryParse to use based on precedence rules (this will be a separate method I call which returns a Func<string,string,string>.
  • Make sure generated code indentation gods don't smite me.

@ghost ghost added the area-runtime label Feb 16, 2023
Copy link
Member

@halter73 halter73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good so far. What are we waiting for to undraft this? Fixing the indentation? If so, I agree we could probably save this for a follow up PR. Getting string and nullablity support would be great, but those could probably also be follow-ups if you want.

@mitchdenny mitchdenny marked this pull request as ready for review February 22, 2023 02:13
@mitchdenny mitchdenny self-assigned this Feb 22, 2023
@mitchdenny
Copy link
Member Author

@captainsafia and @halter73 ... ready for review. Note that I made some more changes to the ParsabilityHelper that we have from the analyzers to allow me to switch on what kind of parsing method it found.

Copy link
Member

@captainsafia captainsafia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Some non-blocking things to note:

  • Do we support the same DateTime styles options that the RDF currently does? If not, we might want to track this as a follow-up issue.
  • I'm not sure what refactoring was tried here but I think some split up will be necessary when #46715 is rebased.

cc: @halter73 for sanity checking the way TryParse is invoked here

@halter73
Copy link
Member

halter73 commented Feb 22, 2023

cc: @halter73 for sanity checking the way TryParse is invoked here

I discussed this a bit with @mitchdenny yesterday. I think we tried to use the "default" NumberStyles where we could because before IParsable support, a lot of the numeric types required passing NumberStyles to TryParse if you also wanted to pass an IFormatProvider which we did for the invariant culture. Now that there's IParsable support, I think we get the same behavior by just calling that. We should verify by looking at the source code. For instance here's the NumberStyles we use for decimal:

else if (type == typeof(decimal))
{
method = typeof(decimal).GetMethod(
nameof(decimal.TryParse),
BindingFlags.Public | BindingFlags.Static,
new[] { typeof(string), typeof(NumberStyles), typeof(IFormatProvider), typeof(decimal).MakeByRefType() });
numberStyles = NumberStyles.Number;
}

And here's the IParsable implementation which also uses NumberStyles.Number:

https://github.com/dotnet/runtime/blob/86bfe2a0e2dc95eb15c124a8d18f4f8ac2bca3f5/src/libraries/System.Private.CoreLib/src/System/Decimal.cs#L1815-L1816

We should verify this for all the numeric types we special case in ParameterBindingMethodCache.TryGetNumberStylesTryGetMethod like I did for decimal. There aren't that many types we hard-code NumberStyles for in RDF, so it shouldn't be too hard to do manually. But maybe we could update the ParameterBindingMethodCache to prefer the IParsable implementation and see if all the tests still pass (other than some overly specific ones in ParameterBindingMethodCacheTests.

We should also add test cases for all the built-in types that we know support TryParse. I see there are some existing theories but they seem to be incomplete. I came up with the original test cases prior to .NET 6 by using reflection to scan the runtime for public TryParse methods. There could even be more now:

// string is not technically "TryParsable", but it's the special case.
new object[] { (Action<HttpContext, string>)Store, "plain string", "plain string" },
new object[] { (Action<HttpContext, int>)Store, "-42", -42 },
new object[] { (Action<HttpContext, uint>)Store, "42", 42U },
new object[] { (Action<HttpContext, bool>)Store, "true", true },
new object[] { (Action<HttpContext, short>)Store, "-42", (short)-42 },
new object[] { (Action<HttpContext, ushort>)Store, "42", (ushort)42 },
new object[] { (Action<HttpContext, long>)Store, "-42", -42L },
new object[] { (Action<HttpContext, ulong>)Store, "42", 42UL },
new object[] { (Action<HttpContext, IntPtr>)Store, "-42", new IntPtr(-42) },
new object[] { (Action<HttpContext, char>)Store, "A", 'A' },
new object[] { (Action<HttpContext, double>)Store, "0.5", 0.5 },
new object[] { (Action<HttpContext, float>)Store, "0.5", 0.5f },
new object[] { (Action<HttpContext, Half>)Store, "0.5", (Half)0.5f },
new object[] { (Action<HttpContext, decimal>)Store, "0.5", 0.5m },
new object[] { (Action<HttpContext, Uri>)Store, "https://example.org", new Uri("https://example.org") },
new object[] { (Action<HttpContext, DateTime>)Store, now.ToString("o"), now.ToUniversalTime() },
new object[] { (Action<HttpContext, DateTimeOffset>)Store, "1970-01-01T00:00:00.0000000+00:00", DateTimeOffset.UnixEpoch },
new object[] { (Action<HttpContext, TimeSpan>)Store, "00:00:42", TimeSpan.FromSeconds(42) },
new object[] { (Action<HttpContext, Guid>)Store, "00000000-0000-0000-0000-000000000000", Guid.Empty },
new object[] { (Action<HttpContext, Version>)Store, "6.0.0.42", new Version("6.0.0.42") },
new object[] { (Action<HttpContext, BigInteger>)Store, "-42", new BigInteger(-42) },
new object[] { (Action<HttpContext, IPAddress>)Store, "127.0.0.1", IPAddress.Loopback },
new object[] { (Action<HttpContext, IPEndPoint>)Store, "127.0.0.1:80", new IPEndPoint(IPAddress.Loopback, 80) },
new object[] { (Action<HttpContext, AddressFamily>)Store, "Unix", AddressFamily.Unix },
new object[] { (Action<HttpContext, ILOpCode>)Store, "Nop", ILOpCode.Nop },
new object[] { (Action<HttpContext, AssemblyFlags>)Store, "PublicKey,Retargetable", AssemblyFlags.PublicKey | AssemblyFlags.Retargetable },
new object[] { (Action<HttpContext, int?>)Store, "42", 42 },
new object[] { (Action<HttpContext, MyEnum>)Store, "ValueB", MyEnum.ValueB },
new object[] { (Action<HttpContext, MyTryParseRecord>)Store, "https://example.org", new MyTryParseRecord(new Uri("https://example.org")) },
new object?[] { (Action<HttpContext, int?>)Store, null, null },

I think the DateTimeStyles stuff still has impact. We have pretty special logic about adjusting/assuming UTC which I doubt is default.

var dateTimeStyles = type switch
{
Type t when t == typeof(DateTime) => DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces,
Type t when t == typeof(DateTimeOffset) => DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowWhiteSpaces,
_ => DateTimeStyles.AllowWhiteSpaces
};

We should add the equivalent following test cases for dates to the source generator tests:

new object?[] { (Func<DateTime, string>)dateTimeParsing, "9/20/2021 4:18:44 PM", "Time: 2021-09-20T16:18:44.0000000, Kind: Unspecified" },
new object?[] { (Func<DateTime, string>)dateTimeParsing, "2021-09-20 4:18:44", "Time: 2021-09-20T04:18:44.0000000, Kind: Unspecified" },
new object?[] { (Func<DateTime, string>)dateTimeParsing, " 9/20/2021 4:18:44 PM ", "Time: 2021-09-20T16:18:44.0000000, Kind: Unspecified" },
new object?[] { (Func<DateTime, string>)dateTimeParsing, "2021-09-20T16:28:02.000-07:00", "Time: 2021-09-20T23:28:02.0000000Z, Kind: Utc" },
new object?[] { (Func<DateTime, string>)dateTimeParsing, " 2021-09-20T 16:28:02.000-07:00 ", "Time: 2021-09-20T23:28:02.0000000Z, Kind: Utc" },
new object?[] { (Func<DateTime, string>)dateTimeParsing, "2021-09-20T23:30:02.000+00:00", "Time: 2021-09-20T23:30:02.0000000Z, Kind: Utc" },
new object?[] { (Func<DateTime, string>)dateTimeParsing, " 2021-09-20T23:30: 02.000+00:00 ", "Time: 2021-09-20T23:30:02.0000000Z, Kind: Utc" },
new object?[] { (Func<DateTime, string>)dateTimeParsing, "2021-09-20 16:48:02-07:00", "Time: 2021-09-20T23:48:02.0000000Z, Kind: Utc" },

new object?[] { (Func<DateTimeOffset, string>)dateTimeOffsetParsing, "09/20/2021 16:35:12 +00:00", "Time: 2021-09-20T16:35:12.0000000+00:00, Offset: 00:00:00" },
new object?[] { (Func<DateTimeOffset, string>)dateTimeOffsetParsing, "09/20/2021 11:35:12 +07:00", "Time: 2021-09-20T11:35:12.0000000+07:00, Offset: 07:00:00" },
new object?[] { (Func<DateTimeOffset, string>)dateTimeOffsetParsing, "09/20/2021 16:35:12", "Time: 2021-09-20T16:35:12.0000000+00:00, Offset: 00:00:00" },
new object?[] { (Func<DateTimeOffset, string>)dateTimeOffsetParsing, " 09/20/2021 16:35:12 ", "Time: 2021-09-20T16:35:12.0000000+00:00, Offset: 00:00:00" },

new object?[] { (Func<DateOnly, string>)dateOnlyParsing, "9/20/2021", "Time: 2021-09-20" },
new object?[] { (Func<DateOnly, string>)dateOnlyParsing, "9 /20 /2021", "Time: 2021-09-20" },

new object?[] { (Func<TimeOnly, string>)timeOnlyParsing, "4:34 PM", "Time: 16:34:00.0000000" },
new object?[] { (Func<TimeOnly, string>)timeOnlyParsing, " 4:34 PM ", "Time: 16:34:00.0000000" },

@mitchdenny
Copy link
Member Author

We should verify this for all the numeric types we special case in ParameterBindingMethodCache.TryGetNumberStylesTryGetMethod like I did for decimal. There aren't that many types we hard-code NumberStyles for in RDF, so it shouldn't be too hard to do manually. But maybe we could update the ParameterBindingMethodCache to prefer the IParsable implementation and see if all the tests still pass (other than some overly specific ones in ParameterBindingMethodCacheTests.

I just went and compared all the IParsable<T>.TryParse implementations with what we have in the method cache and they all line up. Given this I think we should go ahead and rely on the IParsable<T> implementation for these types in RDF like we are here in RDG.

#46788

@captainsafia
Copy link
Member

Thanks for the test pointers, @halter73. I'll try to get my test consolidation branch in shape so we can take advantage of that...

@halter73
Copy link
Member

I'll try to get my test consolidation branch in shape so we can take advantage of that...

👍 In the meantime, I think it should be very quick to add all the types from RDFTests.TryParsableParameters to the new RDGTests.MapAction_SingleNumericParam_StringReturn and similar. We've already defined all the source strings and what they should be parsed to. Doing this would have caught both the Uri issue and the UTC issue with DateTime/DateTimeOffset.

@halter73
Copy link
Member

We still need to use the DateTimeStyles to AllowWhiteSpaces and AdjustToUniversal/AssumeUniversal.

@mitchdenny
Copy link
Member Author

We still need to use the DateTimeStyles to AllowWhiteSpaces and AdjustToUniversal/AssumeUniversal.

Done. Found quite a few bugs picking up these test cases. Definitely worth the journey.

@captainsafia captainsafia self-requested a review February 24, 2023 16:11
@mitchdenny mitchdenny merged commit 446e27e into dotnet:main Feb 27, 2023
@mitchdenny mitchdenny deleted the rdg-try-parse branch February 27, 2023 04:48
@ghost ghost added this to the 8.0-preview3 milestone Feb 27, 2023
@amcasey amcasey added area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions and removed area-runtime labels Aug 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants