Testing singleton initialization and throwing appropriate exceptions - powershell

I am continuing to try to grok a singleton implementation with PowerShell classes, and having issues now with handling exceptions.
My test singleton needs to be initialized the first time with a list of files to process, and all following references should have no arguments, since I never want to reprocess files, just reference what was processed initially. To that end I have this...
class TestClass {
# Properties
[collections.arrayList]$Files = [collections.arrayList]::New()
static [TestClass] $instance = $null
# Constructor
TestClass([string[]]$filePaths){
foreach ($path in $filePaths) {
$this.Files.Add("$($path): Processed")
}
}
[void] ListFiles() {
foreach ($processedFile in $this.Files) {
Write-Host "$processedFile!"
}
}
# Singleton Methods
static [TestClass] GetInstance() {
if ([TestClass]::Instance -ne $null) {
$caller = (Get-PSCallStack)[-1]
throw "Px Tools Exception: Singleton must be initialized ($caller)"
}
return [TestClass]::Instance
}
static [TestClass] GetInstance([string[]]$filePaths) {
if ([TestClass]::Instance -eq $null) {
[TestClass]::Instance = [TestClass]::New($filePaths)
} else {
$caller = (Get-PSCallStack)[-1]
throw "Px Tools Exception: Singleton cannot be reinitialized ($caller)"
}
return [TestClass]::Instance
}
}
A properly done first initialization works, i.e.
$firstInstance = [TestClass]::GetInstance($unprocessedFiles)
$firstInstance.ListFiles()
will initialize the singleton and list the files. So the conditional if ([TestClass]::Instance -eq $null) works.
Also, if I try to reinitialize like this
$firstInstance = [TestClass]::GetInstance($unprocessedFiles)
$secondInstance = [TestClass]::GetInstance($unprocessedFiles)
I get the appropriate
Exception: Singleton cannot be reinitialized
However, if I make the first reference without the files to process, like this...
$firstInstance = [TestClass]::GetInstance()
I get no error. So, why does if ([TestClass]::Instance -eq $null) seem to work correctly but if ([TestClass]::Instance -ne $null) does not? And, is there a better way to address this? I thought perhaps I could test the $Files property, either for $null or a Count of 0, but I can't reference a non static property from a static method, so barring the kludgy approach of a static $Initialized property, what is the solution?
EDIT: Duh. Hopefully this helps someone else. Don't test for the value, test for existence, like this.
if (-not [TestClass]::Instance) {

Related

Get index of object in Generic/List based on value of a property

I can populate a Generic.List with instances of my class, each with a unique value in the ID property, like this.
$packages = [System.Collections.Generic.List[object]]::New()
class Package {
# Properties
[String]$ID
# Constructor
Package ([String]$id) {
$this.ID = $id
}
}
foreach ($i in 65..90) {
$packages.Add([Package]::New("$([char]$i)"))
}
Now I want to get the index of a particular item, along the lines of $packages.IndexOf('C'). But doing this in C# seems to require the use of a lambda, and PowerShell doesn't seem to support that. I would rather not have to initialize my own index variable and iterate through all the items in the list checking their IDs and incrementing the index along the way. But, maybe this is that grey area between using PowerShell with only native PS cmdlets and just using C#, where you miss out on the sugar in both extremes and have to make your own?
You can use a [scriptblock] as a predicate:
$packages.FindIndex({$args[0].ID -eq 'C'})
# or
$packages.FindIndex({param($item) $item.ID -eq 'C'})
Assuming your package IDs are unique, consider using a dictionary instead of a list:
$packages = [System.Collections.Generic.Dictionary[string,Package]]::new([StringComparer]::InvariantCultureIgnoreCase)
class Package {
# Properties
[String]$ID
# Constructor
Package ([String]$id) {
$this.ID = $id
}
}
foreach ($i in 65..90) {
$ID = "$([char]$i)"
$packages.Add($ID, [Package]::new($ID))
}
Now you don't need to worry about the position of the item, anymore, now you can simply remove one by ID:
$wasRemoved = $packages.Remove("C")
Remove() returns $true when it successfully removes an entry, $false when the key wasn't found

Powershell: Inherited classes calling Parent's empty constructors, even when passed objects

In powershell 5 I'm running into a strange inheritance problem with classes.
I want to enforce that we are passed an object during setup like [class]::new($mailbox_object), and I was intending to do this by causing [class]::new() to throw an error if it's associated object isn't assigned (say by a child constructor).
But powershell is calling empty parent constructors BEFORE calling the child constructor that was passed the object and I can't figure out if this is a bug or expected, and more importantly how to enforce that we have to be given an object at creation time
Design pattern speak: I'm trying to implement what I call a Unified Interface pattern, which is a Facade pattern to simplify/unify interactions with similar but differently typed objects, where actions for those objects are selected using a Strategy pattern and the the strategy is chosen automatically by the Facade when created (currently by trying to use an invisible Factory hidden within the Facade)
IRL Example: trying to create a unified interface for Exchange Mailbox/Group objects, and implement a MemberOf function (to return which groups it's a member of). But Mailboxes and Groups use different commands (despite matching functionality) AND 365 and On Premises versions also use different commands (get-unifiedgroup instead of get-distributiongroup) so I'm trying to hide that complexity behind a unified Facade for clarity and usability
I'm open to changing my approach, particularly if there's a better way to do this. Just keep in mind there will be at minimum the following types of disparate objects each of which will need their own implementation of .MemberOf(): Interface_Mailbox_365, Interface_Mailbox_OnPremises, Interface_Group_365, Interface_Group_OnPremises, and I may implement Offline and Generic versions eventually.
MRE below, lines with > are the output. Since I've narrowed it to the an issue with the Interface creation, I've not included the Facade or Factory, but I can add them if they end up being needed.
class Interface_MailObject
{
$MailObject = "Interface_MailObject class - initial"
Interface_MailObject(){write-warning "Interface_MailObject::new() MailObject: {$($this.MailObject)}"}
static [Interface_MailObject] Build($object)
{
if
($object -eq "Mailbox Standin")
{return [Interface_Mailbox_365]::new($object)}
else
{throw("we don't reach here")}
}
}
Class Interface_Mailbox : Interface_MailObject
{
$MailObject = "Interface_Mailbox class - initial"
Interface_Mailbox () {write-warning "Interface_Mailbox::new() MailObject: {$($this.MailObject)}"}
Interface_Mailbox ($MailObject) {$this.MailObject = "Interface_Mailbox class - {$($MailObject)}"}
}
Class Interface_Mailbox_365 : Interface_Mailbox
{
$MailObject = "Interface_Mailbox_365 class - initial"
Interface_Mailbox_365 () {write-warning "Interface_Mailbox_365::new() MailObject: {$($this.MailObject)}"}
Interface_Mailbox_365 ($MailObject) {$this.MailObject = "Interface_Mailbox_365 class - {$($MailObject)}"}
[object[]] MemberOf(){throw("Interface_Mailbox_365.MemberOf TBD")}
}
[Interface_MailObject]::new("Mailbox Standin")|tee -va a
> WARNING: Interface_MailObject::new() MailObject: {Interface_Mailbox_365 class - initial}
> WARNING: Interface_Mailbox::new() MailObject: {Interface_Mailbox_365 class - initial}
>
> MailObject
> ----------
> Interface_Mailbox_365 class - {Mailbox Standin}
Notice that even though we called [Interface_Mailbox_365]::new("Mailbox Standin") powershell executed the grandparent's empty constructor, then the parent's empty constructor, before running the one we called.
If they executed in the other order, it would be fine. If they called parent constructors that match the same parameter qty and type, that would also be fine
But it's doing neither, and I don't know how to resolve it without using some weird acrobatics with a Singleton factory which seems like an excessive amount of micromanagement for something that should be a common need (requiring an input parameter during initialization) so I'm guessing I'm overlooking something
TL:DR
Use child(object):base(object){} to declare the constructor instead of child(object){}
Thanks to #Mathias R. Jessen for helping me work it out.
At first I thought I had to decouple the Facade/Factory from the Template, rather than being able to have them be the same class. Unfortunately this would mean I'm calling [MailObject_Interface]::Build($object) but not returning a [MailObject_Interface] type.
After doing some research I realized what Mathias was saying is a child constructor child(object){} is inferred to mean child(object):base(){} and you can override this by explicitly stating child(object):base(object){}
Paring that with an additional piece to verify the parent isn't called directly I was able to achieve success
Class MailObject_Interface
{
[string] $MailObject
MailObject_Interface ()
{throw("You must call ::Build(`$object), because we return specialized types based on the mail object")}
MailObject_Interface ($object) {[MailObject_Interface]::new()} # this triggers the error above
MailObject_Interface ($object, $codephrase)
{
Write-Warning "calling MailObject_Interface::New($($object), $($codephrase)) {$($this.MailObject)}"
# the Codephrase ensures
# either we're being called from one of our children,
# or whomever calls us is aware of our internal workings and is taking responsibility for making sure we're handled correctly
if
($codephrase -eq "Shazam!")
{$this.MailObject = $object}
else
{[MailObject_Interface]::new()} # this triggers the error above
}
# We run through ::Build instead of ::New because we want to return a child typed object rather than ourselves
static [MailObject_Interface] Build($object)
{
if
($object -eq "Mailbox Standin")
{return [Interface_Mailbox_365]::new($object)}
else
{throw("we don't reach here")}
}
}
Class Interface_MailObject_Template : MailObject_Interface
{
Interface_MailObject_Template ($object) : base ($object, "Shazam!") {Write-Warning "calling Interface_MailObject_Template::New($($object)) {$($this.MailObject)}"}
[object[]] MemberOf(){throw(".MemberOf will be type+context specific")}
}
Class Interface_Mailbox : Interface_MailObject_Template
{
Interface_Mailbox ($object) : base ($object) {Write-Warning "calling Interface_Mailbox::New($($object)) {$($this.MailObject)}"}
[object[]] MemberOf(){throw("Mailbox.MemberOf will be context specific")}
}
Class Interface_Mailbox_365 : Interface_Mailbox
{
Interface_Mailbox_365 ($object) : base ($object) {Write-Warning "calling Interface_Mailbox_365::New($($object)) {$($this.MailObject)}"}
[object[]] MemberOf(){throw("Interface_Mailbox_365.MemberOf TBD")}
}
#\/ Rough Tests \/#
# should succeed
function Test_Correct()
{
Try
{
[MailObject_Interface]$a = [MailObject_Interface]::Build("Mailbox Standin")
return "Succeded ($a)"
}
Catch
{return "Failed"}
}
# should fail
function Test_New_WithObject_MissingCodephrase()
{
Try
{
$a = [MailObject_Interface]::New("Mailbox Standin")
return "Succeded: ($a)"
}
Catch
{return "Failed"}
}
# should fail
function Test_EmptyBuild()
{
Try
{
$a = [MailObject_Interface]::Build()
return "Succeded: ($a)"
}
Catch
{return "Failed"}
}
# should fail
function Test_EmptyNew()
{
Try
{
$a = [MailObject_Interface]::New()
return "Succeded: ($a)"
}
Catch
{return "Failed"}
}
"$(Test_Correct):`tTest_Correct (should have succeeded)"
"$(Test_New_WithObject_MissingCodephrase):`tTest_New_WithObject_MissingCodephrase (should have failed)"
"$(Test_EmptyBuild):`tTest_EmptyBuild (should have failed)"
"$(Test_EmptyNew):`tTest_EmptyNew (should have failed)"
And here are the test results
> WARNING: calling MailObject_Interface::New(Mailbox Standin, Shazam!) {}
> WARNING: calling Interface_MailObject_Template::New(Mailbox Standin) {Mailbox Standin}
> WARNING: calling Interface_Mailbox::New(Mailbox Standin) {Mailbox Standin}
> WARNING: calling Interface_Mailbox_365::New(Mailbox Standin) {Mailbox Standin}
> Succeded (Interface_Mailbox_365): Test_Correct (should have succeeded)
> Failed: Test_New_WithObject_MissingCodephrase (should have failed)
> Failed: Test_EmptyBuild (should have failed)
> Failed: Test_EmptyNew (should have failed)
Also sorry about the unusual format in the Testing functions. I forgot to convert the bracket indenting to the normal standard, I use a nonstandard approach that I find more functional because it frames control logic with whitespace making it more naturally follow the perimeter walk your eyes do when skimming

Referring to current object from different scope

I want to be able to refer to $_ object in catch block from the function I call in catch block like this:
function foo
{
$something = ((Get-Variable -Name "_" -Scope 1).Value).Exception.Message
Write-Host $something
}
I want to use this in situations like these:
foo #write-host should print empty line
try {
throw
} catch {
foo #write-host should print $_.Exception.Message from this catch block
}
How to do that properly?
The goal is to avoid passing $_ as parameter to foo every time I use it in a catch block, and not to print anything when I call foo not in a catch block.
I have also tried this:
function foo
{
$something = (($ExecutionContext.SessionState.PSVariable.Get("_")).Value).Exception.Message
Write-Host $something
}
This seems to produce the result I want when working interactively, but not when launching script.
The clean way of doing this is, of course, an (optional) parameter on foo, called with foo $_ from catch blocks. This makes it perfectly clear what we're calling foo with, it has no issues with scoping, it makes foo testable, all that niceness. You will see this approach in most anything that prints errors (as in the answers to this question, for example).
Even so, if you insist on plucking the error variable from the parent frame, it can be done:
function foo {
$lastError = (Get-PSCallStack)[1].GetFrameVariables()["_"].Value
if ([Object]::ReferenceEquals($lastError.Exception, $error[0].Exception)) {
$lastError.Exception.Message
}
}
Note the extra trickery with Object.ReferenceEquals to be absolutely sure that $_ is referring to the last error record and not some arbitrary pipeline item (which also use $_). This will still fail if the error record itself is in the pipeline (e.g. $error |% { foo }), but at that point you might as well call it a feature.

Set a property for a PowerShell class on Instantiation

Is it possible to have the value of a property of a PowerShell class defined on instantiation without using a constructor?
Let's say there's a cmdlet that will return Jon Snow's current status (alive or dead). I want that cmdlet to assign that status to a property in my class.
I can do this using a constructor, but I'd like this to happen regardless of which constructor is used, or even indeed if one is used at all.
function Get-JonsCurrentStatus {
return "Alive"
}
Class JonSnow {
[string]
$Knowledge
[string]
$Status
#Constructor 1
JonSnow()
{
$this.Knowledge = "Nothing"
$this.Status = Get-JonsCurrentStatus
}
#Constructor 2
JonSnow([int]$Season)
{
if ($Season -ge 6)
{
$this.Knowledge = "Still nothing"
$this.Status = Get-JonsCurrentStatus #I don't want to have to put this in every constructor
}
}
}
$js = [JonSnow]::new()
$js
Unfortunately, you cannot call other constructors in the same class with : this() (though you can call a base class constructor with : base())[1]
Your best bet is a workaround with a (hidden) helper method:
function Get-JonsCurrentStatus {
return "Alive"
}
Class JonSnow {
[string]
$Knowledge
[string]
$Status
# Hidden method that each constructor must call
# for initialization.
hidden Init() {
$this.Status = Get-JonsCurrentStatus
}
#Constructor 1
JonSnow()
{
# Call shared initialization method.
$this.Init()
$this.Knowledge = "Nothing"
}
#Constructor 2
JonSnow([int]$Season)
{
# Call shared initialization method.
$this.Init()
if ($Season -ge 6)
{
$this.Knowledge = "Still nothing"
}
}
}
$js = [JonSnow]::new()
$js
[1] The reason for this by-design limitation, as provided by a member of the PowerShell team is:
We did not add : this() syntax because there is a reasonable alternative that is also somewhat more intuitive syntax wise
The linked comment then recommends the approach used in this answer.
You can initialise class properties on instantiation this way:
$jon = new-object JonSnow -Property #{"Status" = Get-JonsCurrentStatus; "Knowledge" = "Nothing"}

Method invocation failed because System.Object[]] does not contain a method name indexOf in PowerShell

I have error IndexOf when i tried to run the following powershell script. Any advise
$unlicensedUsers = Get-MsolUser -UnlicensedUsersOnly
foreach ($aUser in $unlicensedUsers)
{
if ($unlicensedUsers.IndexOf($aUser) % 10 -eq 0) {
Write-Host -ForegroundColor yellow $unlicensedUsers.IndexOf($aUser)
}
}
Error:
IndexOf : Method invocation failed because System.Object[]] does not contain a method name indexOf
IndexOf() is a method on the List<T> type, which is usually not used in PowerShell. Most of the time the variables that you use in foreach are going to be object arrays, as in your example, or some other type of collection. Object arrays don't have an equivalent method, so you'll have to hold your own copy of the array index:
$unlicensedUsers = Get-MsolUser -UnlicensedUsersOnly
for ($i = 0; $i -lt $unlicensedUsers.count; $i++)
{
if ($i % 10 -eq 0) {
Write-Host -ForegroundColor yellow $i
}
}
Nacht's helpful answer contains the correct solution, but an incorrect explanation:
System.Array instances do have an .IndexOf() method via an explicit implementation of the IList.IndexOf() interface method.
Up to PSv2, such explicit interface implementations were not accessible at all.
In PSv3+, explicit interface implementations are available directly on the implementing type, without the need to reference the interface, so your code would work, but Nacht's answer is still the better solution in the case at hand.
That said, even in PSv2 the [System.Array] type has a static .IndexOf() method, which can be invoked as follows:
[array]::IndexOf($unlicensedUsers, $aUser) # return index of $aUser in array $unlicensedUsers