Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Add TypeConverter fallback to DefaultValueAttribute #19354

Merged
merged 13 commits into from
Aug 15, 2018
Merged

Add TypeConverter fallback to DefaultValueAttribute #19354

merged 13 commits into from
Aug 15, 2018

Conversation

MarcoRossignoli
Copy link
Member

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);
Copy link
Member

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.

Copy link
Member Author

@MarcoRossignoli MarcoRossignoli Aug 8, 2018

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?

Copy link
Member

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)
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

@jkotas jkotas Aug 8, 2018

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?

Copy link
Member Author

@MarcoRossignoli MarcoRossignoli Aug 8, 2018

Choose a reason for hiding this comment

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

Copy link
Member Author

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); ?

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

@MarcoRossignoli MarcoRossignoli Aug 8, 2018

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?

Copy link
Member

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.

Copy link
Member Author

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!

Copy link
Member Author

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)
Copy link
Member

Choose a reason for hiding this comment

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

nit: missing private

Copy link
Member Author

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

Copy link
Member

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.

Copy link
Member Author

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!

Copy link
Member

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.

Copy link
Member Author

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 });
Copy link
Member

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.

Copy link
Member Author

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!

Copy link
Member Author

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;
Copy link
Member

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) }));
Copy link
Member

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 MethodInfos? 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.

Copy link
Member Author

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!

Copy link
Member

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).

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member Author

@MarcoRossignoli MarcoRossignoli Aug 9, 2018

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

Copy link
Member

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.

Copy link
Member Author

@MarcoRossignoli MarcoRossignoli Aug 10, 2018

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;

Copy link
Member

@jkotas jkotas Aug 10, 2018

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).

Copy link
Member Author

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);
}
Copy link
Member Author

Choose a reason for hiding this comment

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

@jkotas @ericstj added this check to be sure that helper method exists on "corelib users". I'm not sure if this makes sense, is corefx the only "user" of coreclr today and in future?

Copy link
Member

@jkotas jkotas Aug 11, 2018

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.

Copy link
Member Author

@MarcoRossignoli MarcoRossignoli Aug 11, 2018

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));
}

Copy link
Member Author

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
Copy link
Member

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.

Copy link
Member Author

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);
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member Author

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...

Copy link
Member Author

Choose a reason for hiding this comment

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

done

@jkotas
Copy link
Member

jkotas commented Aug 13, 2018

The existing DefaultValueAttributeTests CoreFX test is failing:

   System.ComponentModel.Tests.DefaultValueAttributeTests.Ctor [FAIL]
Assert.Equal() Failure
Expected: Monday
Actual:   (null)
Stack Trace:
/home/aand/repositories/corefx/src/System.Runtime/tests/System/ComponentModel/DefaultValueAttributeTests.cs(32,0): at System.ComponentModel.Tests.DefaultValueAttributeTests.Ctor()

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.

@MarcoRossignoli
Copy link
Member Author

MarcoRossignoli commented Aug 13, 2018

@jkotas i think it's correct...we removed throwOnBindFailure the current CoreFx build doesn't have new custom helper...so bind throw exception "This method overload is equivalent to calling the CreateDelegate(Type, Type, String, Boolean, Boolean) method overload, specifying true for throwOnBindFailure."
Throw exception goes to empty catch and so null value. With throwOnBindFailure = false we got new object for s_convertFromInvariantString with correct return false and fallback.
The key is you assert we should expect that the helper method exists too, but now doesn't exist. Does my suspect makes sense?
In my local cross repo build/test works well.

EDIT: I think it's not so easy...but could be "super" run CI with a CoreFx picked up from PR @dotnet-bot test Windows_NT x64 Release CoreFX Tests PR12345.

@jkotas
Copy link
Member

jkotas commented Aug 13, 2018

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.

@MarcoRossignoli
Copy link
Member Author

@dotnet-bot test this please

@MarcoRossignoli
Copy link
Member Author

@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)?

@jkotas
Copy link
Member

jkotas commented Aug 14, 2018

#19377 needs to go through to pick up updated CoreFX.

@MarcoRossignoli
Copy link
Member Author

needs to go through to pick up updated CoreFX.

Ok so for now stand-by.

@jkotas
Copy link
Member

jkotas commented Aug 14, 2018

@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);
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

}
catch
{

Copy link
Member

Choose a reason for hiding this comment

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

Nit: Unnecessary new line?

Copy link
Member Author

Choose a reason for hiding this comment

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

done

@jkotas
Copy link
Member

jkotas commented Aug 14, 2018

@dotnet-bot test OSX10.12 x64 Checked Innerloop Build and Test please

@jkotas jkotas merged commit 431d041 into dotnet:master Aug 15, 2018
dotnet-maestro-bot pushed a commit to dotnet-maestro-bot/corefx that referenced this pull request Aug 15, 2018
…9354)

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
dotnet-maestro-bot pushed a commit to dotnet-maestro-bot/corert that referenced this pull request Aug 15, 2018
…9354)

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
jkotas pushed a commit to dotnet/corert that referenced this pull request Aug 15, 2018
…9354)

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
jkotas pushed a commit to dotnet/corefx that referenced this pull request Aug 15, 2018
…9354)

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
@MarcoRossignoli MarcoRossignoli deleted the defaultvalueattributetypeconverter branch August 15, 2018 07:16
MaximLipnin pushed a commit to MaximLipnin/corefx that referenced this pull request Jan 14, 2019
…9354)

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
MaximLipnin pushed a commit to MaximLipnin/corefx that referenced this pull request Jan 14, 2019
…9354)

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>
marek-safar pushed a commit to mono/corefx that referenced this pull request Jan 17, 2019
* Add TypeConverter fallback to DefaultValueAttribute (dotnet/coreclr#19354)

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>

* Workaround for mcs

* Use mcs specific define
MaximLipnin added a commit to MaximLipnin/corefx that referenced this pull request Jan 18, 2019
* Add TypeConverter fallback to DefaultValueAttribute (dotnet/coreclr#19354)

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>

* Workaround for mcs

* Use mcs specific define
MaximLipnin added a commit to MaximLipnin/corefx that referenced this pull request Jan 18, 2019
* Add TypeConverter fallback to DefaultValueAttribute (dotnet/coreclr#19354)

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>

* Workaround for mcs

* Use mcs specific define
marek-safar pushed a commit to mono/corefx that referenced this pull request Jan 18, 2019
* Add TypeConverter fallback to DefaultValueAttribute (dotnet/coreclr#19354)

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>

* Workaround for mcs

* Use mcs specific define
marek-safar pushed a commit to mono/corefx that referenced this pull request Jan 18, 2019
* Add TypeConverter fallback to DefaultValueAttribute (dotnet/coreclr#19354)

Signed-off-by: dotnet-bot <dotnet-bot@microsoft.com>

* Workaround for mcs

* Use mcs specific define
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants