-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Add TypeConverter fallback to DefaultValueAttribute #19354
Add TypeConverter fallback to DefaultValueAttribute #19354
Conversation
static DefaultValueAttribute() | ||
{ | ||
// We discover/cache types for conversion fallback on first load | ||
s_typeDescriptorTypeCached = Type.GetType("System.ComponentModel.TypeDescriptor, System, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", throwOnError: false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should not be in static constructor. This should be done as lazy as possible only when it is needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could i use Lazy<T>/LazyInitializer.EnsureInitialized
or usually there are other pattern on codebase? I mean i could simply check null and pay possible benign multiple instances on race to keep code simpler as possible...could be ok?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -44,14 +56,32 @@ public DefaultValueAttribute(Type type, string value) | |||
{ | |||
_value = TimeSpan.Parse(value); | |||
} | |||
else | |||
else if (type.Module == typeof(string).Module) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This condition looks suspect. There are intrinsic type converters for many random types in CoreLib: https://github.com/dotnet/corefx/blob/e0ba7aa8026280ee3571179cc06431baf1dfaaac/src/System.ComponentModel.TypeConverter/src/System/ComponentModel/ReflectTypeDescriptionProvider.cs#L103
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we just always use the TypeConverters for everything when they are available?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think that the idea comes from https://referencesource.microsoft.com/#System/compmod/system/componentmodel/DefaultValueAttribute.cs,b956ede3a6b29f1f,references same "intrinsics" are present also on .net full? @ericstj
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we just always use the TypeConverters for everything when they are available?
If custom converter is not defined GetConverter return a "default type converter". Do you mean check if return something different than "default type converter" and in that case fallback to _value = Convert.ChangeType(value, type, CultureInfo.InvariantCulture);
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the upstack type converters should take care of all conversions. I do not think we need any fallbacks once the upstack type converter is called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If type converter is present, use it. If not, use the old code. There are still cases where folks won't have TypeConverter in their app (eg: trimmed selfcontained app) and we shouldn't break those.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If type converter is present, use it
As i said above if converter is not present we get default type converter(not null). Do you mean that if GetConverter return something different than default right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean that if the TypeConverter assembly itself is missing (your reflection lookups fail) use the old logic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok trimmed assemblies understood thank's!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} | ||
catch | ||
{ | ||
} | ||
|
||
object ConvertFromInvariantString(Type typeToConvert, string stringValue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: missing private
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a local func...if i'm not mistaken it's not possibile add access modifier
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, totally missed the placement of the closing brace. Not sure I've seen any usage of local functions yet in our codebase and so far our style guidelines are mum on it. @jkotas do you have any opinion on style guidelines for local functions? Normally I'd ask @stephentoub @danmosemsft @weshaggard but they're all out.
Just some initial thoughts on style:
place local functions at the end of the parent member and an explicit return before the local function to make it visually clear where the parent member ends.
include a comment above the local function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used a local func because we are replacing simple TypeConverter call(with a lot of code) and it's used only in this method(ctor), to me it's more clear...but let me know if i need to tranform to normal private method!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I've seen any usage of local functions yet in our codebase
I know people have been adding them here and there for a while. Here is an example:
https://github.com/dotnet/corefx/pull/24389/files#diff-8df21afb33a350b9ca945d04a5296b9bR119. The style is about what described.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
if (localTypeDescriptor == null || localConvertFromInvariantStringMethod == null) | ||
return null; | ||
|
||
MethodInfo typeDescriptorTypeGetConverter = localTypeDescriptor.GetMethod("GetConverter", new Type[] { typeToConvert }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need to call this every time, cache it as with other reflection. TypeDescriptor.GetConverter signature does not change based on typeToConvert.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'are right! Thank's!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@@ -23,6 +21,10 @@ public class DefaultValueAttribute : Attribute | |||
/// </devdoc> | |||
private object _value; | |||
|
|||
// We cache reflection types for TypeConverter conversion | |||
static Func<MethodInfo> s_getConverterMethodCached; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I would omit the Cached
suffix.
if (s_getConverterMethodCached == null) | ||
{ | ||
Type typeDescriptorType = Type.GetType("System.ComponentModel.TypeDescriptor, System, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", throwOnError: false); | ||
Volatile.Write(ref s_getConverterMethodCached, () => typeDescriptorType?.GetMethod("GetConverter", new Type[] { typeof(Type) })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just cache the MethodInfo
s? Having to re-run typeDescriptorType?.GetMethod("GetConverter", new Type[] { typeof(Type) }
every time the cache is accessed is not a great cache.
I would just have a static object s_getConverterMethodCached;
field that is either null
(which means we didn't populate it), or set to a MethodInfo
(which means we populated the cache and succeeded), or something else (new object()
?) (which means we tried to populate it, but couldn't find the method or type - and we should not try again). It's more lightweight than a lambda capture and a delegate.
Most importantly, it makes it easier for static analysis tools to analyze the reflection usage. IL Linkers will typically remove methods that don't appear used, and reflection usage is notoriously hard to statically analyze. Using patterns that are as straightforward as possible makes it less likely these things will get removed. The existing code is passing the type reflected on through a lambda capture and that's one of the harder things to analyze.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i followed this pattern, but there makes more sense Principal could change. Ok thank's for advice!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not think you should be caching MethodInfos here at all. It should be all just delegates (much faster in steady state than MethodInfo).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be all just delegates
could you elaborate? Do you mean use Delegate.Create?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, like in the Principal example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...
var converter = getConverter(typeToConvert);
Func<string, object> del = (Func<string, object>)Delegate.CreateDelegate(typeof(Func<string, object>), converter, "ConvertFromInvariantString");
...
@jkotas to call instance method ConvertFromInvariantString with delegate i need to pass concrete converter. What do you think?It's better than invoke(i don't have numbers)?Or we need to cache(key/value) ConvertFromInvariantString delegate? I think it's not correct to cache...are instances...i did a test and seems cached instance today...but could change in future
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to create open delegate that takes this
as argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jkotas @ericstj i worked on open delegate(first time...very fun, if i could not eat to live i'd work 24/24 on this codebase for free, i'm learning a lot, exciting...thanks).
I cannot create Func<object,string,object>
delegate because open delegate creation fail(this
could not be object), so i used a on-the-fly generated generics. But now i cannot do a typed invocation but only dynamic one(cannot cast to Func<object,string,object>
).
What do you think?
...
Volatile.Write(ref s_convertFromInvariantStringMethod, typeConverterType == null ? new object() : Delegate.CreateDelegate(typeof(Func<,,>).MakeGenericType(typeConverterType, typeof(string), typeof(object)), null, typeConverterType.GetMethod("ConvertFromInvariantString", new Type[] { typeof(string) })));
...
if (!(s_getConverterMethod is Func<Type, object> getConverter) || !(s_convertFromInvariantStringMethod is Delegate convertFromInvariantString))
return false;
conversionResult = convertFromInvariantString.DynamicInvoke(getConverter(typeToConvert), stringValue);
return true;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I think the root cause of them problem is that you cannot reference TypeConverter type in CoreLib.
I guess the options are to either give up on performance and just use MethodInfo; or add a helper method to System.ComponentModel.TypeConverter that does the whole conversion for you. It is https://github.com/dotnet/corefx/blob/b9d82ae7fd6912b6c0c758e47d0824689adaeafc/src/System.Runtime.Extensions/src/System/AppDomain.cs#L412 has done too - the GetDefaultInstance method is not part of the public surface, it is specific method for use by the AppDomain reflection (https://github.com/dotnet/corefx/blob/master/src/System.Security.Claims/src/System/Security/Claims/GenericPrincipal.cs#L88).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or add a helper method to System.ComponentModel.TypeConverter that does the whole conversion for you
I'll try this way!
if (typeDescriptorType != null) | ||
{ | ||
convertFromInvariantString = Delegate.CreateDelegate(typeof(Func<Type, string, object>), typeDescriptorType, "InternalConvertFromInvariantString", ignoreCase: false, throwOnBindFailure: false); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (typeDescriptorType != null)
check is fine. It is the whole point of the fallback path.
For Delegate.CreateDelegate
, I do not think we need throwOnBindFailure: false
. If System.ComponentModel.TypeDescriptor
exists, we should expect that the helper method exists too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should expect that the helper method exists too.
My fault i wasn't clear, but you answered! If i remove throwOnBindFailure: false
i can collapse all on assignment...if InternalConvertFromInvariantString
will miss throw exception, and no fallback...Value
will be null.
// lazy init reflection objects
if (s_convertFromInvariantString == null)
{
Type typeDescriptorType = Type.GetType("System.ComponentModel.TypeDescriptor, System, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", throwOnError: false);
Volatile.Write(ref s_convertFromInvariantString, typeDescriptorType == null ? new object() : Delegate.CreateDelegate(typeof(Func<Type, string, object>), typeDescriptorType, "InternalConvertFromInvariantString", ignoreCase: false));
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
// We cache reflection types for TypeConverter conversion | ||
static Func<MethodInfo> s_getConverterMethodCached; | ||
static Func<MethodInfo> s_convertFromInvariantStringMethodCached; | ||
// Delegate 'TypeDescriptor.InternalConvertFromInvariantString' reflection object cache |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I think we can call the method just ConvertFromInvariantString
and drop the internal from all the names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
// lazy init reflection objects | ||
if (s_convertFromInvariantString == null) | ||
{ | ||
Type typeDescriptorType = Type.GetType("System.ComponentModel.TypeDescriptor, System, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", throwOnError: false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should load the type from System.ComponentModel.TypeConverter directly, not from System facade.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should load the type from System.ComponentModel.TypeConverter directly, not from System facade.
Because we'll know if TypeConverter exists when we call conversion method and so check could pass but fail after?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
System.dll is .NET Framework compatibility shim. It should not be loaded or pulled unless really necessary
System.ComponentModel.TypeConverter is the native .NET Core implementation assembly for this, so it is where we should be loading this from.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhh i misread...understood i used System...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
The existing DefaultValueAttributeTests CoreFX test is failing:
It sounds like that fallback path has been handling more cases than the TypeConverter path. Ugh. Maybe we need to move the TypeConverter path later to handle the existing cases. |
@jkotas i think it's correct...we removed EDIT: I think it's not so easy...but could be "super" run CI with a CoreFx picked up from PR |
I see. Could you please split your CoreFX to two: One that adds the helper method and second with the test? We will merge the helper method first, then the CoreCLR change, and the tests the last. It will allow us to get the change through without having failing tests. |
@dotnet-bot test this please |
@jkotas i re-run build...CoreFx tests leg pick up "last commit" repo or there are a different workflow(maybe tell to ci to use new commits)? |
#19377 needs to go through to pick up updated CoreFX. |
Ok so for now stand-by. |
@dotnet-bot test this please |
// lazy init reflection objects | ||
if (s_convertFromInvariantString == null) | ||
{ | ||
Type typeDescriptorType = Type.GetType("System.ComponentModel.TypeDescriptor, System.ComponentModel.TypeConverter, Version=0.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", throwOnError: false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: The other places where we do this omit the Version=0.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
part.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} | ||
catch | ||
{ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Unnecessary new line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
@dotnet-bot test OSX10.12 x64 Checked Innerloop Build and Test please |
…9354) Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
…9354) Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
…9354) Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
…9354) Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
…9354) Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
…9354) Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
* Add TypeConverter fallback to DefaultValueAttribute (dotnet/coreclr#19354) Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com> * Workaround for mcs * Use mcs specific define
* Add TypeConverter fallback to DefaultValueAttribute (dotnet/coreclr#19354) Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com> * Workaround for mcs * Use mcs specific define
* Add TypeConverter fallback to DefaultValueAttribute (dotnet/coreclr#19354) Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com> * Workaround for mcs * Use mcs specific define
* Add TypeConverter fallback to DefaultValueAttribute (dotnet/coreclr#19354) Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com> * Workaround for mcs * Use mcs specific define
* Add TypeConverter fallback to DefaultValueAttribute (dotnet/coreclr#19354) Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com> * Workaround for mcs * Use mcs specific define
contributes to https://github.com/dotnet/corefx/issues/30648
/cc @ericstj @Anipik @jkotas