8000 Fix Out-GridView by replacing use of obsolete BinaryFormatter with custom implementation. by mawosoft · Pull Request #25497 · PowerShell/PowerShell · GitHub
[go: up one dir, main page]

Skip to content

Fix Out-GridView by replacing use of obsolete BinaryFormatter with custom implementation. #25497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 13, 2025

Conversation

mawosoft
Copy link
Contributor
@mawosoft mawosoft commented May 2, 2025

PR Summary

Fix Out-GridView by replacing use of obsolete BinaryFormatter with custom deep-copy implementation for the filter-related classes.

Fixes #24749. Closes #14054.

PR Context

After switching to .NET 9 in Powershell 7.5, filtering in the Out-GridView window is broken, because BinaryFormatter has been removed from the runtime.

PR Checklist

Copy link
Collaborator
@iSazonov iSazonov left a comment

Choose a reason for hiding this comment

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

@mawosoft Looks great! Thanks!

this.AvailableRules = new ValidatingSelectorValue<FilterRule>();
foreach (DataErrorInfoValidationRule rule in source.AvailableRules.ValidationRules)
{
this.AvailableRules.AddValidationRule(rule);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we clone the rule too?

Suggested change
this.AvailableRules.AddValidationRule(rule);
this.AvailableRules.AddValidationRule(rule.Clone());

Copy link
Contributor Author

Choose a reason for hiding this comment

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

DataErrorInfoValidationRule is stateless and not derived from FilterRule. Perhaps I should use different names for the loop variables to avoid confusion.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Are there classes based on DataErrorInfoValidationRule?

Copy link
Collaborator

Choose a reason for hiding this comment

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

In any case we should create new object to follow "DeepCopy".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only IsNotEmptyValidationRule, which is stateless as well.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks! I see. In any case we should create new object to follow "DeepCopy" as BinaryFormatter does.

Copy link
Collaborator
@iSazonov iSazonov left a comment

Choose a reason for hiding this comment

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

Please look CodeFactor issues.

Comment on lines +10 to +17
public interface IDeepCloneable
{
/// <summary>
/// Creates a deep copy of the current instance.
/// </summary>
/// <returns>A new object that is a deep copy of the current instance.</returns>
object DeepClone();
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd prefer strong typed interface. It is more safe.

Suggested change
public interface IDeepCloneable
{
/// <summary>
/// Creates a deep copy of the current instance.
/// </summary>
/// <returns>A new object that is a deep copy of the current instance.</returns>
object DeepClone();
}
public interface IDeepCloneable<TSelf>
where TSelf : class
{
/// <summary>
/// Creates a deep copy of the current instance.
/// </summary>
/// <returns>A new object that is a deep copy of the current instance.</returns>
TSelf DeepClone();
}

Copy link
Contributor Author
@mawosoft mawosoft May 3, 2025

Choose a reason for hiding this comment

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

I wanted to do that initially, but then I can no longer check if a type derived from TSelf provides the interface. Example:

public abstract class FilterRule : IDeepCloneable<FilterRule> {}
public class SomeRule<T> : FilterRule {}
public class Foo<T> {
    public T TypedValue { get; set; }
    public object Value { get; set; }
    public bool IsTypedValueDeepCloneable => typeof(IDeepCloneable<T>).IsAssignableFrom(typeof(T));
    public bool IsValueDeepCloneable => Value is IDeepCloneable<T>;
}
var foo = new Foo<SomeRule<int>>();
foo.TypedValue = new SomeRule<int>();
foo.Value = new SomeRule<int>();
var oops1 = foo.IsTypedValueDeepCloneable; // wrongly returns false.
var oops2 = foo.IsValueDeepCloneable; // wrongly returns false.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Strong typed interface assumes that we have to add it to every class and override DeepClone() in every class - such specific DeepClone() should use right constructor of the class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

FilterRule implements DeepClone() and instantiates the derived classes via copy constructor. The copy constructors are needed anyway, because initialization has to be done slightly different than the default ctor does. (what BinaryFormatter did via [OnDeserialized]).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here is a more comprehensive demo of the problems with the strongly typed interface, be it implemented on the base class or on every derived class. Neither works properly for all use cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I understand what you want but it doesn't work in real life the scenario we are dealing with.

We have cloneable types that contain other cloneable types as properties, the exact types of which are not precisely known. The generic type of such a property may be instantiated as a base class like FilterRule or ValidatingValueBase, some derived class like TextEqualsFilterRule or ValidatingValue, or it may be some other type that doesn't implement our clone interface, like DateTime or string.

We need to check for the interface being available to decide whether to (deep)clone or do a simple copy - which is impossible with your suggestion.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps I skip something. I believe all the work is about static FilterRule DeepCopy(this FilterRule rule) method using BinaryFormatter. So I don't understand "or it may be some other type that doesn't implement our clone interface, like DateTime or string". Can you clarify?

Copy link
Contributor Author
@mawosoft mawosoft May 4, 2025

Choose a reason for hiding this comment

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

The reason why the original author chose to use Binaryformatter instead of doing the work himself (presumably) is the complexity of the involved generic and non-generic types and how they are nested. If you recall, there also was an attempt at replacing Binaryformatter two years ago - which failed, because it did not fully take that complexity into account.

FilterRule is the base class of a hierarchy of ~20 classes, nested up to six levels. Some of them are generic. Their generic properties are of type ValidatingValue<T> and ValidatingSelectorValue<T>.

That type T can be an "incoming" type corresponding to the columns of the gridview, which I referred to with "some other type like string etc".

But T can also be another class derived from FilterRule like PropertyValueSelectorFilterRule<T2>, or another ValidatingValue<T3>. So you get multiple nested generics, some of which are generated dynamically at runtime via MakeGenericType().

Copy link
Collaborator

Choose a reason for hiding this comment

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

All the copying work is done by the new copy constructor (you have already created these constructors), which is called by a virtual method from the interface. Therefore, there should be no problems that you are talking about.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Again:

  • I cannot simply call the interface method, I first have to check if the interface exists.
  • I cannot check for the interface if it is typed to its implementing type, because I don't know which type that is.
  • I don't know which type that is, because the T I have to perform a type check like typeof(IDeepCloneable<T>) may not refer exactly to its implementing type, causing me to wrongly skip the cloning (as demoed by the gist I linked earlier).

I'm sorry I cannot explain it better, even though I spent almost more time on it than on the code itself.
Maybe ask someone else on your team to have a look at it. Otherwise, if my explanation is not sufficient, I'm closing this PR, because it will exceed my available capacity.

@iSazonov iSazonov added the CL-CodeCleanup Indicates that a PR should be marked as a Code Cleanup change in the Change Log label May 3, 2025
@mawosoft mawosoft changed the title WIP: Fix Out-GridView by replacing use of obsolete BinaryFormatter with custom implementation. Fix Out-GridView by replacing use of obsolete BinaryFormatter with custom implementation. May 3, 2025
Copy link
Member
@daxian-dbw daxian-dbw left a comment

Choose a reason for hiding this comment

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

@mawosoft Sorry for the long delay! The changes look great. Thank you very much for getting this done!

There are 4 Initialize methods declared with the [OnDeserialized] attribute. They were used with the BinaryFormatter to initialize an object right after deserialization. I think we can remove them now, what do you think?

I'm happy to go ahead remove those 4 methods if you cannot get to it any time soon :)

@mawosoft
Copy link
Contributor Author

I would only remove them if we also remove the [Serializable] attribute (yes, I'm paranoid). And if we do that, we should be consistent and do it for all the other classes as well.

If that's okay, I can do it tomorrow.

@daxian-dbw
Copy link
Member

Of course, I forgot about the Serializable attributes :) Using Serializable attribute would cause the warning SYSLIB0051. We removed the Serializable attributes from the code base back in v7.4 (#19696) and only left those used in the Microsoft.Management.UI.Internal project. Yes, they should be cleaned up too.

I will merge this PR as is, so that only the necessary changes will be backported to v7.5. I will submit a PR after merging this one to clean up the attributes. Thanks for your contribution, @mawosoft!

@daxian-dbw daxian-dbw merged commit cbafa5a into PowerShell:master May 13, 2025
37 checks passed
@github-project-automation github-project-automation bot moved this from PR In Progress to Done in PowerShell Team Reviews/Investigations May 13, 2025
@sba923
Copy link
Contributor
sba923 commented May 13, 2025

I presume this will land in both 7.5.2 and 7.6.0-preview.5?

@daxian-dbw
Copy link
Member

I verified that the changes in this PR fixed #20223 too. I also smoke tested other filters, and all seem to work fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Backport-7.5.x-Migrated CL-CodeCleanup Indicates that a PR should be marked as a Code Cleanup change in the Change Log
Projects
5 participants
0