Introduction
When PowerShell v5 was in pre-release, I was still fairly new
to PowerShell. I remember being very excited that PowerShell would be adding
native classes as I had just discovered that the only way to classes in
PowerShell was to write them in C#, which I had not yet begun to learn. Classes
are something I have worked with in PHP, Python, PERL, C++, and C#. When I was
learning C++ I fell in love with classes and object oriented programming. Most
of my exposure to classes has been as a consumer and not an author, but I have
written a few classes over the years for various things.
When PowerShell v5 was released, the first thing I did was
go and learn how to make v5 classes. This was painful because there was not
much documentation beyond basic class structures. As I had no urgent need for
classes I let these sit on the back burner for quite some time. I was quite
disappointed. A few months ago, I started seeing some of the more experienced
PowerShell redditors advocating the use of classes for the purpose of
controlling the “shape” of objects. I decided it was probably time to commit to
learning PowerShell v5 classes. I’m still disappointed.
As I understand it, the initial use case for inclusion of
native classes in v5 is their use within DSC. I spent about a month acclimating
to DSC and from that exposure to the DSC syntax, I can certainly see how class
based DSC resources would be somewhat easier to write. Consequently, this use
case is also the only one that is decently documented. Before I go off on why
v5 classes suck, I wanted to make it clear here that I do acknowledge there is
a legitimate use case for them where they do shine brilliantly.
BUT…
As the title of this blog suggest, I would recommend avoiding
PowerShell v5 classes for now. The implementation of native classes is a mere
shadow of .NET classes. As the implementation
is limited that makes complex objects less likely candidates as classes. The
already uber-flexible PSCustomObject objects can already meet the needs of most
objects while also offering a higher level of compatibility.
This post will not have much code to it. It is mostly an
opinion post with some facts sprinkled in. This post may also be a bit rough
for novice coders or those new to PowerShell. If you came here looking for an intro
to PowerShell v5 classes, this post will surely and sorely disappoint you.
Anyway, on to my specific gripes…
Version Locking
The first obvious problem with PowerShell v5 Classes is that
they version lock you to PowerShell v5 or newer. Features do have to start
somewhere and I can’t speak ill of the developers for not making v5 classes backwards
compatible. However, this is a concern as v5 is still “new”. If you work at a
smaller shop, or you happen to work at a large shop that has a very good
Continuous Delivery pipeline, getting to v5 is not a huge hurtle. But, in a
more traditional slow moving enterprise, getting to v5 is still a long way off just to get
past all the red tape, let alone implement. Before you rush out and start
writing v5 classes you need to be cognizant of your current PowerShell
ecosystem. Again, this isn’t a real criticism of v5 Classes, but more of a
caution against them for now.
No Private Properties and Methods
PowerShell v5 Classes do not support Private Properties or Private
Methods. PowerShell v5 does offer a hidden keyword which can be applied to
properties and methods. This hides the property or method from the default list of properties
and methods from Get-Member and from autocompletion and intellisense. The
properties and methods can still be directly accessed by the class consuming code and
they can still be listed by Get-Member with the -Force parameter switch.
Effectively, hidden is a cosmetic change to the class.
One of the benefits of classes in other languages is that
they provide strict control over the class data by having real values in
Private Properties acted on by Private Methods. This keeps the validation logic
in class consuming functions to a minimum because they only need to validate
the object type being passed to it. The rightful assumption is that the class
is handing its own validation so the consumers can relax.
Absent private properties and methods, PowerShell v5 classes
cannot be assumed to be tightly controlled. This means class consuming code
must still include validation logic, error handling, or just let everything
fail.
I realize that with some work it is possible to manipulate
private data on .NET classes in both PowerShell and C#. The problem is that
this is not as easy as simply assigning a value to a property or directly
calling the method, as is possible with a hidden property on PowerShell v5 classes. If you begin with a standard property and later make it to hidden without renaming it,
legacy class consuming code may still be manipulating the now hidden properties
directly and causing undesired results in your class behaviors.
No Get or Set Accessors
PowerShell v5 Class Properties do not have getter and setter
accessors. I could be wrong here, but I have dug around for days and have asked
in several forums and if people even knew what I was talking about they
certainly didn’t know how to do this with PowerShell v5 classes. Since it seems
to be a foreign concept in the PowerShell ecosystem, I recommend that if you do not
know what they are that you read up on C# Class accessors before continuing.
Not all languages with classes have this functionality. But,
for those that do it allows for greater control over the property values. As an
example, let’s say you had a class that defined a Honda Civic. Honda Civics
come in 2 varieties: a 2-door coupe and a 4-door sedan. Honda Civics never have
more than 4 doors and they certainly cannot have less than 0 doors. On our HondaCivic
class, we would define the Doors property as an Int. Since a PowerShell v5
class cannot implement a set accessor for the Doors property, someone could set
it to -80 (negative eighty). Now all the methods on the class or consumers of the class
must implement sanity checks to ensure the Doors property is within the bounds
of 0-4 inclusive.
Another use case is to have that class adjust other properties when another changed. Continuing with the HondaCivic class example, lets say that the class also has an Enum property CarType. This Enum has 2 possible settings: Sedan or Coupe. In a class that supports get and set accessors on properties, the class could be coded such that setting the Doors property to 4 would change the CarType property to Sedan and when the CarType is set to Coupe the Doors property would be set to 2.
Another use case is to have that class adjust other properties when another changed. Continuing with the HondaCivic class example, lets say that the class also has an Enum property CarType. This Enum has 2 possible settings: Sedan or Coupe. In a class that supports get and set accessors on properties, the class could be coded such that setting the Doors property to 4 would change the CarType property to Sedan and when the CarType is set to Coupe the Doors property would be set to 2.
One work around is to use ScriptProperty or CodeProperty properties.
A ScriptProperty is a PowerShell property type that allows for setting a get
ScriptBlock and a Set ScriptBlock. A CodeProperty allows for a CLR based
property that supports both a setter and getter (I only learned about these in
researching for this post and there are not many examples readily available).
Both ScriptProperty and CodeProperty are available for PSCustomObject and any
other object, so it is not unique to classes.
The problem with a ScriptProperty is that it does not emit a
type. What type emission provides is the ability to resolve the type specific
methods and properties. For command line, this allows for tab completion and
discovery of the properties and methods of an object (the object in this case
being the property of a class). When programming, it can be used to call out
typos of properties and methods as well as intellisense completion and
selection. All in all, type emission is very handy, but not critical.
The problem with a CodeProperty is that you must pull it
from compiled CLR code. In other words, you must write in something like C# and
either import the compiled DLL or runtime compile it using Add-Type. With that
much work, you might as well write the whole class in C#. But, it is handy if
you want to borrow properties from existing .NET classes.
Again, the problem with both workarounds is that they are
not native or unique to classes. They can be added to classes in exactly the
same ways as they can be added to PSCustomObject or any other object.
It’s rather disappointing that the C# parent language supports this functionality but it is absent
from PowerShell v5 classes. But, to a degree it makes sense. Since get and set
accessors usually update and call private values from the class, and PowerShell
v5 does not support private values, it probably made sense not to include the
functionality. The problem I have is that it makes no difference to use a
PSCustomObject. If Classes supported the property accessors, it would be a
strong argument for using classes to more tightly control the class data.
Module Based Classes are a Pain
One of the problems I ran into with PowerShell v5 classes
that ultimately led me to writing this blog entry was my experience trying to
include classes in a module. The way I like to do modules is to separate out
code into individual files. For example, each function in the module is in its
own file. I then call these as nested modules in the psd1 of the module. Other
module authors use a dot sourcing code block in their psm1. One of the benefits of making a module out of
your code to break it up into more digestible and easier to maintain pieces.
While working on my Bencode module, I ran into a roadblock
trying to get the classes defined in the module to be properly recognized in
the calling scope. I had originally defined the classes as I normally do with
calling them as nested modules from the psd1. When I would import the module
and run a function with one of my classes defined as the output type I would
get the error message about an unknown type.
No problem, right? You can just use the Using statement to
use the modules types. WRONG! In this configuration, the Using command does not
import the class from the module. I could call the Using statement directly
against the ps1 where the class is stored, obviously, but that defeats the
purpose.
I then tried to dot source the class files from my psm1 and
I got the same results. The functions failed with an unknown output type and
the Using statement failed as well.
Next, I tried directly defining the classes in the psm1
file. This time it worked. The functions recognized the output type without a
Using statement and if I wanted to just use the classes from my module in a
script, the Using statement worked as expected. But, now I had a ton of code in
my psm1 file for the classes each which have their own complex set of methods
and constructors and their many overloads (another minor gripe about classes is
that you can’t include source form other files in the class, the entire class
must be defined in a single file). This makes the maintainability of the psm1 a
nightmare.
I tried several other things that would result in either
breaking one or both of either the ability to import classes with the Using
statement or the functions properly emitting the type. Ultimately I found that
if I defined the ps1 files for each class in the ScriptsToProcess section of
the psd1 file, both would work. This kind of cool, but also bad module
practice. The files run in the ScriptsToProcess section are done so in the calling
scope rather than the module scope. This means unloading your module leaves
these bits behind. I haven’t tested it, but believe it will cause issues when
you call your module from another module and then try to call the functions of
your module from the global scope. In any case, it is a messy and terrible work
around.
I read somewhere that loading multiple psm1 files when a module loaded will be supported in the future which will open up the ability to separate out classes into their own files. I think this will be a neat feature regardless of its impact on classes, but, it probably will only work with newer version likely leaving 5.0 and 5.1 systems in an odd spot where newer modules aren't loading classes from the extra psm1 files.
I read somewhere that loading multiple psm1 files when a module loaded will be supported in the future which will open up the ability to separate out classes into their own files. I think this will be a neat feature regardless of its impact on classes, but, it probably will only work with newer version likely leaving 5.0 and 5.1 systems in an odd spot where newer modules aren't loading classes from the extra psm1 files.
The internet is littered with questions about importing classes from modules and
the response is usually about the Using statement. This boils my blood a bit. I
import the module and I expect the module's classes to be available, just like
when I import modules that include compiled DLL classes. It’s a valid
assumption given the history of PowerShell and non-native classes. However, you
must use the Using statement to work directly with PowerShell v5 classes
defined in modules from the calling scope.
Another fun fact about the Using statement: in a script, the
Using statement(s) must be defined at the very beginning to work properly. I
don’t know what all can be defined before the Using statement, but pretty much
anything before the Using statement will break it. In the interactive shell,
you can call Using at any time and it will work, but that is not the case with
scripts.
This all makes PowerShell v5 classes very anti-pattern for
what we are used to with PowerShell modules and non-native classes. It makes
the v5 classes into special snowflakes. Special snowflakes have a way of not
being used because they are a pain to maintain.
Lack of Arbitrary Namespaces
This is, admittedly, a cosmetic gripe and not a functional
one. One nice aspect of the .NET framework is that everything is partitioned
into namespaces. If you are not familiar with this concept, I’m referring to
all those lovely dots in the .NET class names. For example, the System.Management.Automation
namespace contains some classes you are probably familiar with, like the
PSObject and PSCredential. The point of these namespaces is one of
organization. A group of related classes are placed in an arbitrary namespace
to make them easy to enumerate and to avoid clashes with objects of similar
names in other namespaces.
A use case for this would be developing a module. You may
not know what other modules and classes might be imported by consumers of your
module. To avoid any name clashes between your classes and another module’s
class, you might define your classes under an arbitrary name space that is the
same as your module. Let’s say you were creating a CarBuilder module and you
had several classes such as Door, Engine, Wheel, and Car. With .NET, you could
define the CarBuilder namespace and then your classes under it as
Carbuilder.Door, CarBuilder.Engine, CarBuilder.Wheel, and CarBuilder.Car.
This appears to be impossible with PowerShell v5 classes.
You can still define your classes with a prefix in the name, but you cannot
define them in a namespace. Continuing with the CarBuilder example, you would
have to name your classes CarBuilderDoor, CarBuilderEngine, CarBuilderWheel,
and CarBuilderCar. It’s somewhat cosmetic in nature, but, it also means that
enumerating, autocompleting and intellisensing your classes can only be done by name matching instead of via namespace.
On Performance
One of the areas that initially turned me away from
PowerShell v5 classes was that they were less performant than just creating a
PSCustomObject with the type accelerator. I say “were” because I planned to
repeat those tests for this blog post and I discovered something odd. This time
around, a simple class with a String and Int as properties was out performing
the PSCustomObject accelerator with the same properties. I’m not sure if my
testing method is flawed or if they have made performance improvements in the 5.1.14393.479
version I’m testing with versus the 5.0 when I originally encountered the
performance issues. The difference in my initial findings showed the
PSCustomObject to be 5 times faster, but this time around the class is 2 times
faster. In any case, it’s worth noting that a PowerShell v5 class may or be
more or less performant than a PSCustomObject of a similar “shape”. You should
test for yourself on your own systems if performance matters.
Why Not Just Use PSCustomObject?
I ask myself this often. There is very little difference
between a PowerShell v5 class and a PSCustomObject at this point. Method
functionality can be duplicated with ScriptMethod properties. You can set
arbitrary type names on PSCustomObject with the use of PSTypeName. You can set
custom object types as output types for functions. You can even filter for them
in Param blocks. You can use ScriptProperty on either and you can even create
hidden properties on PSCustomObject.
The one thing that classes offer over a PSCustomObject at
this point is the strict type casting of properties. At least, I haven’t
discovered how to duplicate this functionality with a PSCustomObject, but it
may exist. Another plus for classes is the ability to explicitly document, via
code, what the structure of the object looks like. Since a PSCustomObject is
somewhat like an amorphous blob that is defined at creation, it can create
problems if you are not strictly controlling how they are created or are
creating them in more than one section of code. And finally, using PowerShell
v5 classes for class based resources in DSC is an extremely good use case.
But, if you have any kind of version issues or are wanting
to use classes in modules, be aware of the issue they cause.
Conclusion
I know that the PowerShell developers have many valid
reasons for not implementing all the features I’m whining about here. I’m also
not saying everyone should abandon classes. But, I am suggesting that it is not
yet time for us to all jump on the class bandwagon. I have hopes that classes
will improve in future versions and maybe some of the features lacking now will
be added. There were other technical issues with classes I didn’t mention here
because they are already slated to be fixed in upcoming versions. It is
apparent that the devs care about the classes and won’t leave them feeble for
long. But, for now, we should probably stop recommending classes all the time.
I understand that they are sexy and new and everyone wants to play with the new
toy. I just don’t think we are there yet.