Implementing a .NET Class in PowerShell v5

Introduction

You might have heard that PowerShell version 5.0 has introduced support for building .NET classes. Indeed, this is a powerful, new capability that has not previously existed in native PowerShell syntax. Before the new class-building syntax existed, if you wanted to build custom objects in PowerShell, you generally would either: 1) use the [PSCustomObject] type, or 2) build a .NET class in C#, and use the Add-Type command to import it into the PowerShell session.

Creating a Class

Classes basically allow us to define custom object types, and the behaviors that we use to interact with them. For example, you might define a class called Server, implement properties that describe it called Name and OperatingSystem, and create methods that allow you to interact with the server such as Start or Shutdown.

PowerShell 5.0 September 2014 Preview introduces the new class keyword, which is directly followed by a valid class identifier (name). For example, we can create a new, empty object class using the following syntax:

class Beer {
}

Now we have a new type of object called a “Beer.” We haven’t actually created a Beer object yet, but we have defined the top-level skeleton / blueprint for what a beer could look like. Once we’ve defined the class itself, we can start implementing class members, such as properties and methods. The properties and methods are what make the class “work.”

In the remainder of this article, we will implement properties and methods that build out the functionality of the Beer class.

Adding Properties to a Class

Properties expose data about an object, and are backed by .NET fields. PowerShell doesn’t allow us to differentiate between fields and properties, so we will simply declare properties. The implementation of .NET fields is done automatically behind the scenes when you declare a property. However, in the C# language, you can implement fields separately from properties. Except in rare circumstances, you don’t need to worry about defining fields by hand, so the PowerShell implementation is adequate.

To define a property, specify the type name of the property, followed by an identifier. Let’s go ahead and add a Size property to our Beer class, and make it an unsigned 32-bit integer. This property will hold the amount of beer in our beer glass.

class Beer {
    [UInt32] $Size;
}

Perhaps we should also create a Name property for the Beer class, so that we can assign an owner to the beer. We wouldn’t want people mixing up beers now, would we? Because someone’s name is just a string of characters, let’s use the .NET [string] class as the property type.

class Beer {
    [UInt32] $Size;
    [String] $Name;
}

Implementing an Object Constructor

When we create a new object, from our custom class, we can optionally implement a constructor. The purpose of a constructor is to initialize the values of properties, so that the object is “ready to go,” so to speak. For example, when we create a new Beer object, we may want to initialize the Size property to a certain amount, and initialize the Name property, so we know who the beer belongs to. After all, we wouldn’t want to have any beers not being assigned to people, would we?

To implement a constructor, specify the class name, and a series of parameters (or none) after it. The parameters are surrounded by parentheses; you aren’t required to specify the parameter type, however it is good practice to specify types, to ensure proper input constraints. Constructors look very similar to methods (see next section), however the key difference is that constructors cannot declare a return type. Methods can return values, but constructors cannot.

class Beer {
    [Uint32] $Size;
    [String] $Name;

    Beer([UInt32] $NewSize, [String] $NewName) {
        # Set the Beer size
        $this.Size = $NewSize;
        # Set the Beer name
        $this.Name = $NewName;
    }
}

Once we’ve created the object constructor, we can create a new instance (instantiate) the Beer class with this constructor.

Instantiating a Class

New Beer Object

New Beer Object

To create a new instance of our Beer class, we must use the :: operator (generally used to call static methods in PowerShell), to call the new() method. If your constructor has input parameters — and our examples does have them — then the input parameter values will go inside the parentheses. Here is an example of how to create a new Beer object, using the constructor that we created above:

# Create a new beer object, with a size of 16 ounces, and 'Trevor' as its name
$MyBeer = [Beer]::new(16, 'Trevor');
$MyBeer | Format-Table -AutoSize;

Now we have created a new beer object! Let’s move on to the next section, where we talk about implementing methods in classes.

Adding Methods to a Class

In programming terminology, a method is something that you “do” with an object. For example, if your class is Beer, you might have a method called Drink. When you call the Drink method on the Beer class, the amount of beer in the glass may decrease by a certain amount. The amount of beer that is consumed could be represented in ounces, as an integer. This would be called a parameter declared on the method, called Amount. Since you cannot drink a negative amount of beer, you might want to declare the parameter as an unsigned integer, instead of a signed integer. Unsigned integers do not allocate a bit to designating positive or negative value, so they are always positive.

Methods can return values, as we previously touched on, but they don’t have to. If you don’t want to return a value from your method, use the return type [void]. Otherwise, specify the appropriate type that will be returned from your method, such as [Int32], [String], or another type of complex object.

Let’s go ahead and implement the Drink method in our Beer class.

class Beer {
    # Property: Holds the current size of the beer.
    [Uint32] $Size;
    # Property: Holds the name of the beer's owner.
    [String] $Name;

    # Constructor: Creates a new Beer object, with the specified
    #              size and name / owner.
    Beer([UInt32] $NewSize, [String] $NewName) {
        # Set the Beer size
        $this.Size = $NewSize;
        # Set the Beer name
        $this.Name = $NewName;
    }

    # Method: Drink the specified amount of beer.
    # Parameter: $Amount = The amount of beer to drink, as an 
    #            unsigned 32-bit integer.
    [void] Drink([UInt32] $Amount) {
        $this.Size = $this.Size - $Amount;
    }
}

Update for November 2014 Preview

UPDATE (2015-02-14): This article was originally based on the Windows Management Framework Core 5.0 September 2014 Preview. In the November 2014 Preview, the PowerShell team enhanced the class feature by requiring $this to reference class properties inside of methods. Therefore, the method above should be changed to the following. Thanks to David O’Brien for pointing out that the original article no longer worked.

    [void] Drink([UInt32] $Amount) {
        $this.Size = $this.Size - $Amount;
    }
PowerShell Class :: Calling the Drink Method

Calling the Drink Method

Our Beer class is starting to look like something real, isn’t it? Now we have a couple of properties, a constructor, and a method that allows us to Drink some of the beer! Let’s go ahead and call our new Drink method.

# Create a new 16 ounce beer, named 'Trevor'
$MyBeer = [Beer]::new(16, 'Trevor');

# We drink 10 out of 16 ounces, leaving 6 left.
$MyBeer.Drink(10);

# The second call fails, because we only have 6 ounces left.
$MyBeer.Drink(10);

Uh oh, what happened? You should have received an error, stating:

Exception setting “Size”: “Cannot convert value “-4” to type “System.UInt32”. Error: “Value was either too large or too small for a UInt32.””

What happened here, is that the Size property we declared is an unsigned integer, however we tried to assign the value -4 to it. When we created the Beer using the constructor, we told it to create a 16 ounce beer. After that, we called the Drink method twice, totaling 20 ounces of beer. Since we only had 16 ounces to start with, drinking 20 ounces would put us at a total of -4 ounces, which can’t be assigned to an unsigned integer. That is the nature of the exception / error we received.

To solve for this scenario, we need to put some logic into our Drink method to politely fail if the user tries to drink more beer than we have available. Let’s change our Drink method to look like the following, which uses the PowerShell try..catch statement to handle exceptions gracefully.

[void] Drink([UInt32] $Amount) {
    try {
        $this.Size = $this.Size - $Amount;
    }
    catch {
        Write-Warning -Message 'You tried to drink more beer than was available!';
    }
}
PowerShell Class :: Graceful Error Handling

Graceful Error Handling

After we implement this update to the Drink method, we now are given a warning, instead of a nasty error, when we try to drink too much beer. Personally, I submit that there is no such thing as drinking too much beer, but everyone has their own opinions I’m sure.

BreakGlass Method

For our final example, we will implement a new method called BreakGlass. It happens to the best of us. We’re enjoy our beer all jolly-like, and suddenly you hear the crack of a glass breaking on the floor. What effectively happens at this point, is that the beer moves from being contained inside the glass to being emptied out onto the floor. Insert the following code inside your Beer class definition.

# Method: BreakGlass resets the beer size to 0.
[void] BreakGlass() {
    Write-Warning -Message 'The beer glass has been broken. Resetting size to 0.';
    $this.Size = 0;
}
PowerShell Class :: BreakGlass Method

Beer BreakGlass Method

Now that we’ve implemented the BreakGlass method, let’s test it out. As you can see, the BreakGlass method prints a warning message, and resets the size of the Beer object to 0! The code below is what we use to test out the BreakGlass method, and also make sure that our Drink method is still working as expected. You might notice that the Format-Table -AutoSize command is used to nicely format the properties of the Beer object, for human readability.

# Create a new 16 ounce beer, named 'Trevor'
$MyBeer = [Beer]::new(16, 'Trevor');

# We drink 10 out of 16 ounces, leaving 6 left.
$MyBeer.Drink(10);

# The second call fails gracefully now, because we only have 6 ounces left.
$MyBeer.Drink(10);

$MyBeer.BreakGlass();
$MyBeer | Format-Table -AutoSize;

Entire Class Definition

For the sake of future reference, I am including the entire class definition here, so you can easily copy/paste it into your own PowerShell editor to play with.

This first version of the class was originally developed against PowerShell 5.0 September 2014 Preview. However, classes are considered an experimental feature, and the feature is subject to change. See below for a version that works in later versions of PowerShell.

##################################################
####### WMF 5.0 September 2014 Preview ###########
##################################################
class Beer {
    # Property: Holds the current size of the beer.
    [Uint32] $Size;
    # Property: Holds the name of the beer's owner.
    [String] $Name;

    # Constructor: Creates a new Beer object, with the specified
    #              size and name / owner.
    Beer([UInt32] $NewSize, [String] $NewName) {
        # Set the Beer size
        $Size = $NewSize;
        # Set the Beer name
        $Name = $NewName;
    }

    # Method: Drink the specified amount of beer.
    # Parameter: $Amount = The amount of beer to drink, as an 
    #            unsigned 32-bit integer.
    [void] Drink([UInt32] $Amount) {
        try {
            $this.Size = $this.Size - $Amount;
        }
        catch {
            Write-Warning -Message 'You tried to drink more beer than was available!';
        }
    }

    # Method: BreakGlass resets the beer size to 0.
    [void] BreakGlass() {
        Write-Warning -Message 'The beer glass has been broken. Resetting size to 0.';
        $this.Size = 0;
    }
}

PowerShell 5.0 November 2014 Preview

This version of the class has a minor change that enables it to work in PowerShell 5.0 November 2014 Preview (and hopefully later versions). See the “Methods” section of this article for more information.

##################################################
####### WMF 5.0 November 2014 Preview ###########
##################################################
class Beer {
    # Property: Holds the current size of the beer.
    [Uint32] $Size;
    # Property: Holds the name of the beer's owner.
    [String] $Name;

    # Constructor: Creates a new Beer object, with the specified
    #              size and name / owner.
    Beer([UInt32] $NewSize, [String] $NewName) {
        # Set the Beer size
        $this.Size = $NewSize;
        # Set the Beer name
        $this.Name = $NewName;
    }

    # Method: Drink the specified amount of beer.
    # Parameter: $Amount = The amount of beer to drink, as an 
    #            unsigned 32-bit integer.
    [void] Drink([UInt32] $Amount) {
        try {
            $this.Size = $this.Size - $Amount;
        }
        catch {
            Write-Warning -Message 'You tried to drink more beer than was available!';
        }
    }

    # Method: BreakGlass resets the beer size to 0.
    [void] BreakGlass() {
        Write-Warning -Message 'The beer glass has been broken. Resetting size to 0.';
        $this.Size = 0;
    }
}

Conclusion

During this article, we have discussed the process of implementing a .NET class using the new PowerShell version 5.0 syntax. You can construct your own .NET objects that make sense for your business or personal scenarios, using the same techniques that we used to build the Beer class in this article. Thanks for reading.

NOTE: Much of the information that was used to produce this article was thanks to a European PowerShell Summit 2014 video by Dan Harman at Microsoft. Thanks, Dan!

  • Pingback: Dew Drop – October 27, 2014 (#1885) | Morning Dew()

  • Igal Shkolnik

    You actually never fixed $Size => $this.Size in all examples here

    • Igal,

      Thanks for the notification about this. I’ve gone through and updated the article. If you find any other inconsistencies, please let me know. Thanks again!

      Cheers,
      Trevor Sullivan