1

Imagine that I have created a C# class that extended Collection<T> to add additional functionality for a specific use case.

public class MyCollection<T> : Collection<T> {}

Now, if I import this class into my PowerShell script, and instantiate such an object, calling member methods of the T object from said object will perform a sort of "magic" under-the-hood in which it calls the member method one after another for each item in the collection.

Note that in this example, the fact that I'm using my own MyCollection[String] instead of Collection[String] makes no difference.

Add-Type -Path ".\MyCollection.cs"
$test = New-Object -TypeName MyCollection[String]
$test.Add("one")
$test.Add("two")
$test.Add("three")
$test.Length
# 3
# 3
# 5

This behavior has more-or-less been dubbed "Member Enumeration". Go here for an explanation of what I'm talking about: https://devblogs.microsoft.com/powershell/new-v3-language-features/

Since it's not actually running the ForEach-Object cmdlet (refer to the above link), I was hoping to find what is actually going on and how, if it's possible, I could override that behavior without making a hacky workaround (like creating a wrapper method in the MyCollection class for each method in the object's class).

For example:

public class MyCollection<T> : Collection<T> {
    public new void ForEach-Object(this Action action) {
        DoSomethingAsync(this, action);
    }
}

I'm intentionally using invalid syntax above in order to help make a point. I really have no idea if this can even be done.

Alternatively, if there is another way to achieve the same thing without doing the workaround I mentioned before, I would be interested in hearing those ideas as well.

Shenk
  • 352
  • 4
  • 12

2 Answers2

2

You can not prevent PowerShell's member-access enumeration (without also making your collection non-enumerable), given that it is behavior built into ., the member-access operator.

  • However, you can bypass it with extra effort when you access a member, namely via the intrinsic .psbase property:[1]

    ((Get-Item /), (Get-Item /)).Name         # Member(-access) enumeration
    ((Get-Item /), (Get-Item /)).psbase.Name  # NO enumeration, only the array's
                                              # own members.
    

However, if you refer to a property that exists on the collection object itself, that collection-level property is used, overriding member-access enumeration.

That is, $test.Count - given that .Count is an (instance) member of your collection type - would return just 3, the number of elements in the collection.

GitHub suggestion #7445 asks for a distinct syntax (operator) for member-access enumeration, but as long as PowerShell remains committed to backward compatibility (which may be forever), the behavior of . will not change.


[1] Note that the .psbase property only mediates member access that is restricted to the type-native members. The value of .psbase itself is a [pscustomobject] instance with an ETS (Extended Type System) name of System.Management.Automation.PSMemberSet.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • So basically your answer is "currently, no." Because the suggestion you're offering seems the same as the hacky solution I mentioned—i.e., creating a wrapper method in the MyCollection class for each method in the object's class. Unless I can figure out a way to abstract that, that will become a lot of duplicated code. +1 for that GitHub suggestion. It is interesting, though it's unfortunate that we may never see it implemented. – Shenk Nov 25 '19 at 14:15
  • @Shenk: More generally: No just currently no, but no as long as PowerShell remains committed to backward compatibility, which may well be forever. I didn't mean to offer a suggestion, because there is no solution, if you want your collection to remain enumerable by standard means. (Of course, you can define your own custom enumeration method without implementing `IEnumerable` or perhaps even define your own enumeration interface that your classes then implement - but no code expecting `IEnumerable`s will work with them - and you won't be able to derive from `Collection`). – mklement0 Nov 25 '19 at 14:25
1

Ultimately, all of these collection types are going to implement the IEnumerable interface, which is using the GetEnumerator method to iterate over the object collection, whether in a .NET foreach loop on in your PowerShell Foreach-Object cmdlet.

Some of the concrete Collection implementations are going to allow you to supply your own GetEnumerator implementation, while others aren't. I can't think of ANY that do allow it off the top of my head, but I know Collection<T> does not. If you need to override this functionality, you could try shadowing GetEnumerator with the new keyword as you're demonstrating above, and it should work just fine. Another option is to write your own collection class that implements IEnumerable, but that sounds like a pain.

Steve Danner
  • 21,818
  • 7
  • 41
  • 51
  • This sounded really promising, but after trying it by creating a new GetEnumerator method to hide the base GetEnumerator method, running the member enumeration still only did its previous behavior. I will try to fiddle around a little more to see if I was missing something before potentially attempting to implement my own IEnumerable. – Shenk Nov 25 '19 at 14:19
  • Okay, it turns out that I had to implement IList in order for PowerShell to look at the IEnumerator GetEnumerator of my class rather than the base class. While that doesn't quite solve *my* problem, it does solve this question, and it did lead me to a potential solution for my particular problem. – Shenk Nov 25 '19 at 15:18
  • Good feedback, glad it helped! I was worried that shadowing the `GetEnumerator` method was going to yield unexpected results as well, but it sounds like that didn't get in the way. – Steve Danner Nov 25 '19 at 20:23