Hi How Can I use a foreach on an IEnumerable<> lists? - ienumerable

`
public IEnumerable Merge(IEnumerable first, IEnumerable second)
{
}
`

Related

How to extend and override Add in a collection class

Background
I have a data object in PowerShell with 4 properties, 3 of which are strings and the 4th a hashtable. I would like to arrange for a new type that is defined as a collection of this data object.
In this collection class, I wish to enforce a particular format that will make my code elsewhere in the module more convenient. Namely, I wish to override the add method with a new definition, such that unique combinations of the 3 string properties add the 4th property as a hashtable, while duplicates of the 3 string properties simply update the hashtable property of the already existing row with the new input hashtable.
This will allow me to abstract the expansion of the collection and ensure that when the Add method is called on it, it will retain my required format of hashtables grouped by unique combinations of the 3 string properties.
My idea was to create a class that extends a collection, and then override the add method.
Code so far
As a short description for my code below, there are 3 classes:
A data class for a namespace based on 3 string properties (which I can reuse in my script for other things).
A class specifically for adding an id property to this data class. This id is the key in a hashtable with values that are configuration parameters in the namespace of my object.
A 3rd class to handle a collection of these objects, where I can define the add method. This is where I am having my issue.
Using namespace System.Collections.Generic
Class Model_Namespace {
[string]$Unit
[string]$Date
[string]$Name
Model_Namespace([string]$unit, [string]$date, [string]$name) {
$this.Unit = $unit
$this.Date = $date
$this.Name = $name
}
}
Class Model_Config {
[Model_Namespace]$namespace
[Hashtable]$id
Model_Config([Model_Namespace]$namespace, [hashtable]$config) {
$this.namespace = $namespace
$this.id = $config
}
Model_Config([string]$unit, [string]$date, [string]$name, [hashtable]$config) {
$this.namespace = [Model_Namespace]::new($unit, $date, $name)
$this.id = $config
}
}
Class Collection_Configs {
$List = [List[Model_Config]]#()
[void] Add ([Model_Config]$newConfig ){
$checkNamespaceExists = $null
$u = $newConfig.Unit
$d = $newConfig.Date
$n = $newConfig.Name
$id = $newConfig.id
$checkNamespaceExists = $this.List | Where { $u -eq $_.Unit -and $d -eq $_.Date -and $n -eq $_.Name }
If ($checkNamespaceExists){
($this.List | Where { $u -eq $_.Unit -and $d -eq $_.Date -and $n -eq $_.Name }).id += $id
}
Else {
$this.List.add($newConfig)
}
}
}
Problem
I would like the class Collection_Configs to extend a built-in collection type and override the Add method. Like a generic List<> type, I could simply output the variable referencing my collection and automatically return the collection. This way, I won't need to dot into the List property to access the collection. In fact I wouldn't need the List property at all.
However, when I inherit from System.Array, I need to supply a fixed array size in the constructor. I'd like to avoid this, as my collection should be mutable. I tried inheriting from List, but I can't get the syntax to work; PowerShell throws a type not found error.
Is there a way to accomplish this?
Update
After mklement's helpful answer, I modified the last class as:
Using namespace System.Collections.ObjectModel
Class Collection_Configs : System.Collections.ObjectModel.Collection[Object]{
[void] Add ([Model_Config]$newConfig ){
$checkNamespaceExists = $null
$newConfigParams = $newConfig.namespace
$u = $newConfigParams.Unit
$d = $newConfigParams.Date
$n = $newConfigParams.Name
$id = $newConfig.id
$checkNamespaceExists = $this.namespace | Where { $u -eq $_.Unit -and $d -eq $_.Date -and $n -eq $_.Name }
If ($checkNamespaceExists){
($this | Where { $u -eq $_.namespace.Unit -and $d -eq $_.namespace.Date -and $n -eq $_.namespace.Name }).id += $id
}
Else {
([Collection[object]]$this).add($newConfig)
}
}
}
Which seems to work. In addition to the inheritance, had to do some other corrections regarding how I dotted into my input types, and I also needed to load the collection class separately after the other 2 classes as well as use the base class's add method in my else statement.
Going forward, I will have to do some other validation to ensure that a model_config type is entered. Currently the custom collection accepts any input, even though I auto-convert the add parameter to model_config, e.g.,
$config = [model_config]::new('a','b','c',#{'h'='t'})
$collection = [Collection_Configs]::new()
$collection.Add($config)
works, but
$collection.Add('test')
also works when it should fail validation. Perhaps it is not overriding correctly and using the base class's overload?
Last update
Everything seems to be working now. The last update to the class is:
using namespace System.Collections.ObjectModel
Class Collection_Configs : Collection[Model_Config]{
[void] Add ([Model_Config]$newConfig ){
$checkNamespaceExists = $null
$namespace = $newConfig.namespace
$u = $namespace.Unit
$d = $namespace.Date
$n = $namespace.Name
$id = $newConfig.id
$checkNamespaceExists = $this.namespace | Where { $u -eq $_.Unit -and $d -eq $_.Date -and $n -eq $_.Name }
If ($checkNamespaceExists){
($this | Where { $u -eq $_.namespace.Unit -and $d -eq $_.namespace.Date -and $n -eq $_.namespace.Name }).id += $id
}
Else {
[Collection[Model_Config]].GetMethod('Add').Invoke($this, [Model_Config[]]$newConfig)
}
}
}
Notice in the else statement that ....GetMethod('Add')... is necessary for Windows PowerShell, as pointed out in the footnote of mklement0's super useful and correct answer. If you are able to work with Core, then mklement0's syntax will work (I tested).
Also mentioned by mklement0, the types need to be loaded separately. FYI this can be done on the commandline for quick provisional testing by typing in the model_namespace and model_config classes and pressing enter before doing the same for Collection_Configs.
In summary this will create a custom collection type with custom methods in PowerShell.
It is possible to subclass System.Collections.Generic.List`1, as this simplified example, which derives from a list with [regex] elements, demonstrates:[1]
using namespace System.Collections.Generic
# Subclass System.Collections.Generic.List`1 with [regex] elements.
class Collection_Configs : List[regex] {
# Override the .Add() method.
# Note: You probably want to override .AddRange() too.
Add([regex] $item) {
Write-Verbose -Verbose 'Doing custom things...'
# Call the base-class method.
([List[regex]] $this).Add($item)
}
}
# Sample use.
$list = [Collection_Configs]::new()
$list.Add([regex] 'foo')
$list
However, as you note, it is recommended to derive custom collections from base class System.Collections.ObjectModel.Collection`1:
using namespace System.Collections.ObjectModel
# Subclass System.Collections.ObjectModel`1 with [regex] elements.
class Collection_Configs : Collection[regex] {
# Override the .Add() method.
# Note: Unlike with List`1, there is no .AddRange() method.
Add([regex] $item) {
Write-Verbose -Verbose 'Doing custom things...'
# Call the base-class method.
([Collection[regex]] $this).Add($item)
}
}
As for the pros and cons:
List`1 has more built-in functionality (methods) than ObjectModel`1, such as .Reverse(), Exists(), and .ForEach().
In the case of .ForEach() that actually works to the advantage of ObjectModel`1: not having such a method avoids a clash with PowerShell's intrinsic .ForEach() method.
Note that in either case it is important to use the specific type that your collection should be composed of as the generic type argument for the base class: [regex] in the example above, [Model_Config] in your real code (see next section).
If you use [object] instead, your collection won't be type-safe, because it'll have a void Add(object item) method that PowerShell will select whenever you call the .Add() method with an instance of a type that is not the desired type (or cannot be converted to it).
However, there's an additional challenge in your case:
As of PowerShell 7.3.1, because the generic type argument that determines the list element type is another custom class, that other class must unexpectedly be loaded beforehand, in a separate script, the script that defines the dependent Collection_Configs class.
This requirement is unfortunate, and at least conceptually related to the general (equally unfortunate) need to ensure that .NET types referenced in class definitions have been loaded before the enclosing script executes - see this post, whose accepted answer demonstrates workarounds.
However, given that all classes involved are part of the same script file in your case, a potential fix should be simpler than the one discussed in the linked post - see GitHub issue #18872.
[1] Note: There appears to be a bug in Windows PowerShell, where calling the base class' .Add() method fails if the generic type argument (element type) happens to be [pscustomobject] aka [psobject]: That is, while ([List[pscustomobject]] $this).Add($item) works as expected in PowerShell (Core) 7+, an error occurs in Windows PowerShell, which requires the following reflection-based workaround: [List[pscustomobject]].GetMethod('Add').Invoke($this, [object[]] $item)
There were a few issues with the original code:
The Using keyword was spelled incorrectly. It should be using.
The $List variable in the Collection_Configs class was not declared with a type. It should be [List[Model_Config]]$List.
The Add method in the Collection_Configs class was missing its return type. It should be [void] Add ([Model_Config]$newConfig).
The Add method was missing its opening curly brace.

alter .txt file and save it with PowerShell

I am doing classic find&replace in some .txt file with powershell.
How can I "save" file at the end? I tried with | Set-Content but nothing happens.
Maybe I need to Add-Content first?
#Find what?
$optionBuilderStringToFind = "optionsBuilder.UseSqlServer"
$findUsingKeywordString = "using Microsoft."
#Replace with
$namespaceAdd = "using Microsoft.Extensions.Configuration;"
$optionBuilderConfigurable ="optionsBuilder.UseSqlServer(_configuration.GetConnectionString(`"Database`")
);"
gc -Path .\APSContext.cs | % {
if ($_ -match "using System;") {
$_ = $_ + "`n" + $namespaceAdd
#write-host $_
}
if ($_ -match "optionsBuilder.UseSqlServer") {
$_ = $optionBuilderConfigurable
#write-host $_
}
} | Set-Content -Path .\test.cs
Update: Here is testFile where I am looking to alter it. The content of the file is not important. I want to add another reference like "using Something.Something" and in the middle of the file to replace "optionsBuilder.UseSqlServer("dfjidfjljfiejf88");" with "optionsBuilder.UseSqlServer("_configtest");":
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace SRC.APS.Model.APSDB
{
public partial class APSContext : DbContext
{
public APSContext()
{
}
public APSContext(DbContextOptions<APSContext> options)
: base(options)
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer("dfjidfjljfiejf88");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasAnnotation("ProductVersion", "2.2.0-rtm-35687");
}
Maybe I need to use Out-String instead?
Any script block in Powershell can produce values. This is done by not storing a value in a variable:
{ $foo = 3 } produces nothing
{ 3 } produces the integer 3.
In the same way
ForEach-Object { $_ = "something" } produces nothing
ForEach-Object { $_ = "something"; $_ } produces the string "something"
Your loop body does not output anything, it's like the upper example above. Therefore, Set-Content has nothing to do. Modify the block to actually return the changed value of $_:
$replacements = #(
#{regex='using (System|Potentially|Others);'; replacement='using Microsoft.$1;' }
#{regex='optionsBuilder\.UseSqlServer\("[^"]*"\)'; replacement='optionsBuilder.UseSqlServer("Database")' }
# more search/replace pairs
)
Get-Content .\APSContext.cs -Encoding UTF8 | ForEach-Object {
foreach ($item in $replacements) {
$_ = $_ -replace $item.regex, $item.replacement
}
$_ # <--- this is what produces the line
} | Set-Content -Path .\test.cs -Encoding UTF8
That being said, never load or save text files without specifying their encoding. For C# source code files, I think UTF-8 is the default.
And that being said, modifying source code with regular expressions is not a good thing to do. If this is a one-off, fine. If you plan to do this on a regular basis, you are doing something wrong. Work with configuration files or environment variables instead of keeping in your codebase literal values that are subject to regular change.
as i mentioned - and as Tomalak pointed out - your mucking with the current pipeline object is the source of the glitch. [grin] this code fixes a logic error in your IF cascade [which may be better done with a switch block], removes the unwise $_ fiddling, and then ensures there is always some output.
# fake reading in a text file
# in real life, use Get-Content
$InStuff = #'
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace SRC.APS.Model.APSDB
{
public partial class APSContext : DbContext
{
public APSContext()
{
}
public APSContext(DbContextOptions<APSContext> options)
: base(options)
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer("dfjidfjljfiejf88");
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasAnnotation("ProductVersion", "2.2.0-rtm-35687");
}
'# -split [environment]::NewLine
#Find what?
$optionBuilderStringToFind = "optionsBuilder.UseSqlServer"
$findUsingKeywordString = "using Microsoft."
#Replace with
$namespaceAdd = 'using Microsoft.Extensions.Configuration;'
$optionBuilderConfigurable ='optionsBuilder.UseSqlServer(_configuration.GetConnectionString("Database"));'
$InStuff | ForEach-Object {
if ($_ -match "using System;")
{
$_ + "`n" + $namespaceAdd
#write-host $_
}
elseif ($_ -match "optionsBuilder.UseSqlServer")
{
$optionBuilderConfigurable
#write-host $_
}
else
{
$_
}
}
output ...
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
namespace SRC.APS.Model.APSDB
{
public partial class APSContext : DbContext
{
public APSContext()
{
}
public APSContext(DbContextOptions<APSContext> options)
: base(options)
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer(_configuration.GetConnectionString("Database"));
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasAnnotation("ProductVersion", "2.2.0-rtm-35687");
}

Implement ForEach Iterator in Powershell

I have created a custom class in PowerShell which I would like to be able to access via the foreach command.
In PHP, I can do this by using implements iterator in my class declaration and implementing some variables and functions. Is there something similar in PowerShell? How do I let my PowerShell class be accessed by foreach?
Note: I'm using PowerShell Core
You can extend some IEnumerable implementation like this:
class Test : System.Collections.Generic.List[PSObject]
{
Test($items)
{
$items -split ';' |% { $this.Add($_) }
}
}
$test = New-Object Test('a;1;x;y;z;2;3;4')
foreach ($item in $test)
{
Write-Host $item
}
The result:
a
1
x
y
z
2
3
4

Using a variables string value in variable name

It should work like:
$part = 'able'
$variable = 5
Write-Host $vari$($part)
And this should print "5", since that's the value of $variable.
I want to use this to call a method on several variables that have similar, but not identical names without using a switch-statement. It would be enough if I can call the variable using something similar to:
New-Variable -Name "something"
but for calling the variable, not setting it.
Editing to add a concrete example of what I'm doing:
Switch($SearchType) {
'User'{
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJUsers_ListBox.Items.Add($Item)
}
}
'Computer' {
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJComputers_ListBox.Items.Add($Item)
}
}
'Group' {
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJGroups_ListBox.Items.Add($Item)
}
}
}
I want this to look like:
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJ$($SearchType)s_ListBox.Items.Add($Item)
}
You're looking for Get-Variable -ValueOnly:
Write-Host $(Get-Variable "vari$part" -ValueOnly)
Instead of calling Get-Variable every single time you need to resolve a ListBox reference, you could pre-propulate a hashtable based on the partial names and use that instead:
# Do this once, just before launching the GUI:
$ListBoxTable = #{}
Get-Variable OBJ*_ListBox|%{
$ListBoxTable[($_.Name -replace '^OBJ(.*)_ListBox$','$1')] = $_.Value
}
# Replace your switch with this
foreach($Item in $OBJResults_ListBox.SelectedItems) {
$ListBoxTable[$SearchType].Items.Add($Item)
}

Using custom type in Powershell 5.0 class [duplicate]

This question already has answers here:
Using .NET objects within a Powershell (V5) class
(3 answers)
Closed 7 years ago.
Here is example code that is causing me lots of headaches at the moment.
if (("Win32.NativeMethods" -as [type]) -eq $null){
Add-Type -MemberDefinition '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
' -name NativeMethods -namespace Win32
}
class AppInstance
{
[string]$App = 'Notepad.exe'
[IntPtr]$hWnd = 0
[System.Object]$process
AppInstance () {
Start-Process $this.App
$this.process = get-process ($this.App.split('.'))[0]
start-sleep -Milliseconds 100
$this.hWnd = $this.process.MainWindowHandle
}
[void] Show () {
[Win32.NativeMethods]::ShowWindowAsync($this.hWnd, 3)
}
[void] Hide () {
[Win32.NativeMethods]::ShowWindowAsync($this.hWnd, 2)
}
}
This class can be used like so
$notepad = [AppInstance]::new()
$notepad.Hide()
$notepad.Show()
Basically, what I'm trying to do is to import a function from user32.dll as type [Win32.NativeMethods] and then use this type in a class.
If I execute the Add-Type statement separately in Powershell_ISE the type gets created and subsequently the script works just fine.
However, when I try to execute the whole script before creating the type manually, I get the following Powershell parser error
At C:\class.ps1:26 char:10
+ [Win32.NativeMethods]::ShowWindowAsync($this.hWnd, 3)
+ ~~~~~~~~~~~~~~~~~~~
Unable to find type [Win32.NativeMethods].
At C:\Uclass.ps1:31 char:10
+ [Win32.NativeMethods]::ShowWindowAsync($this.hWnd, 2)
+ ~~~~~~~~~~~~~~~~~~~
Unable to find type [Win32.NativeMethods].
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : TypeNotFound
Looks like the parser is ignoring the Add-Type statement and exiting before execution.
Is there any way to overcome this issue? Maybe with a using statement?
Or, is there any way to tell the parser that the type is dynamically created?
EDIT 1:
I have read the answer to Using .Net Objects within a Powershell (V5) Class and the accepted answer is not an answer to my question. Splitting a simple script into multiple files is not really an answer.
What I'm asking is weather there is a way to tell the parser that the type is dynamically created.
EDIT 2:
To clarify this a little further here is code equivalent to the one above, but implemented using functions instead of classes.
if (("Win32.NativeMethods" -as [type]) -eq $null){
Add-Type -MemberDefinition '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
' -name NativeMethods -namespace Win32
}
[string]$script:App = 'Notepad.exe'
$process = Start-Process $App -PassThru
function Show () {
[Win32.NativeMethods]::ShowWindowAsync($process.MainWindowHandle, 3)
}
function Hide () {
[Win32.NativeMethods]::ShowWindowAsync($process.MainWindowHandle, 2)
}
This code will parse and execute perfectly fine. Are classes handled by the parser in a different way to rest of the script?
As you've found out yourself, when defining functions the parser is not nearly as strict as when it comes to classes - the simple reason is that function definitions need no compilation, so the parser only checks for syntax not type resolution.
You can use this observation to work around your problem - simply define a function outside the Class definition that wraps the call to [Win32.NativeMethods]::ShowWindowAsync() and then call that function from inside your class method:
function __ShowWindowAsync
{
param([IntPtr]$WindowHandle,[int]$ShowState)
if (("Win32.NativeMethods" -as [type]) -eq $null){
Add-Type -MemberDefinition '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);' -Name NativeMethods -namespace Win32
}
[Win32.NativeMethods]::ShowWindowAsync($this.hWnd, $ShowState)
}
class AppInstance
{
[string]$App = 'Notepad.exe'
[IntPtr]$hWnd = 0
[System.Object]$process
AppInstance () {
# this is way more reliable than running Get-Process subsequently
$this.process = Start-Process $this.App -PassThru
start-sleep -Milliseconds 100
$this.hWnd = $this.process.MainWindowHandle
}
[void] Show () {
$Maximized = 3
__ShowWindowAsync -WindowHandle $this.hWnd -ShowState $Maximized
}
[void] Hide () {
$Minimized = 2
__ShowWindowAsync -WindowHandle $this.hWnd -ShowState $Minimized
}
}