Class-based DSC Resources

Class-based DSC Resources provide a simplified implementation of DSC Resources for managing the settings of a system. This article explains their structure and requirements.

Structure of a class-based DSC Resource

A class-based DSC Resource is defined as a PowerShell class in a module file (.psm1). The class-based DSC Resource doesn't have special requirements for where it's defined. You can define it:

Regardless where the DSC Resource is defined, the DSC Resource must be listed in the DscResourcesToExport property of a module manifest (.psd1) file. The Get-DscResource cmdlet, the [Import-DSCResource] dynamic keyword, and DSC itself, when compiling a DSC Configuration, will fail if the DSC Resource isn't listed in a manifest.

For more information on creating a module manifest, see New-ModuleManifest. For more information on the settings of a module manifest, see about_Module_Manifests.

A class-based DSC Resource must:

  1. Use the DscResource attribute.
  2. Declare one or more properties with the DscProperty attribute. At least one of the properties must be a Key property.
  3. Implement the Get(), Test(), and Set() methods.
  4. Define a default constructor if it defines any alternate constructors.

The DscResource attribute

The definition of the class must have the DscResource attribute. This attribute indicates that the class defines a DSC Resource.

To add the DscResource attribute to a class, declare it on the line immediately before the class definition.

[DscResource()]
class MyDscResource {
}

As well as identifying the class as a DSC Resource, the DscResource attribute applies parse-time validation to the class-based DSC Resource. PowerShell raises a parse error for the definition of a class-based DSC Resource when:

  • One or more of the Get(), Test(), and Set() methods is incorrectly defined or missing
  • The class doesn't have at least one Key property
  • The class defines a non-default constructor without defining a default constructor

The DscResource attribute can also be specified with the RunAsCredential property to specify the class-based DSC Resource's behavior when using the PsDscRunAsCredential property:

  • Optional - A user may use the PsDscRunAsCredential property with this DSC Resource. This is the default behavior. This behavior can also be specified as Default instead of Optional.
  • NotSupported - A user can't use the PsDscRunAsCredential property with this DSC Resource.
  • Mandatory - A user must use the PsDscRunAsCredential property with this DSC Resource.

Note

While you can set the RunAsCredential property for a class-based DSC Resource, it has no effect when used with DSC v2.0 and later. The PsDscRunAsCredential property is only supported in DSC v1.1 and earlier. If you're writing a class-based DSC Resource and supporting it in DSC v1.1, specifying this property in the DscResource attribute provides some clarity and an improved user experience for that scenario.

Class-based DSC Resource properties

The schema of a class-based DSC Resource is defined by the properties of the class. For a property to be recognized as part of the schema, it must have the DscProperty attribute.

The definition of the DscProperty determines how DSC treats that property:

  • [DscProperty(Key)] - Indicates that this property uniquely identifies an instance of this DSC Resource. Every DSC Resource must have at least one Key property. If a DSC Resource has more than one Key property, those properties are used together to uniquely identify an instance of the DSC Resource. If any Key properties of a DSC Resource aren't specified when using Invoke-DscResource, the cmdlet raises an error. If any Key properties aren't specified when authoring a DSC Configuration, compiling the configuration raises an error.
  • [DscProperty(Mandatory)] - Indicates that the property must be specified when using this DSC Resource. If any Mandatory properties of a DSC Resource aren't specified when using Invoke-DscResource, the cmdlet raises an error. If any Mandatory properties aren't specified when authoring a DSC Configuration, compiling the configuration raises an error.
  • [DscProperty(NotConfigurable)] - Indicates that the property is ReadOnly. It isn't a manageable setting of this DSC Resource, but will contain a value after the Get() method of the DSC Resource is called.
  • [DscProperty()] - Indicates that the property is a configurable setting of this DSC Resource. Specifying this property is optional when using Invoke-DscResource or authoring a DSC Configuration.

Like all PowerShell class properties, the properties of a class-based DSC Resource must specify a type. This type is used to validate the specified setting when calling Invoke-DscResource or compiling a DSC Configuration.

Consider this definition snippet of the MyDscResource class-based DSC Resource:

[DscResource()]
class MyDscResource {
    [DscProperty(Key)]
    [string] $Path

    [DscProperty(Mandatory)]
    [hashtable]$Settings

    [DscProperty(NotConfigurable)]
    [datetime] $LastModified

    [DscProperty()]
    [string] $Format = 'YAML'
}

It defines four properties:

  • Path is a Key property for the DSC Resource and expects to get a String for its value.
  • Settings is a Mandatory property for the DSC Resource and expects to get a Hashtable for its value.
  • LastModified is a ReadOnly property for the DSC Resource and expects to get a DateTime for its value from the Get() method.
  • Format is an optional property for the DSC Resource and expects to get a String for its value.

Default property values

When a class-based DSC Resource instance is created, the default parameterless constructor is used. Unless you override this behavior, the default parameterless constructor intializes every property to the default value for its type. If the property is defined with an explicit default, that value is used instead. For more information on constructors for class-based DSC Resources, see Constructors.

Whether the property is a reference type or a value type determines the default value. Reference types initialize to $null. Value types initialize to the value specified in their definition.

Unlike other implementations, class-based DSC Resources manage all defined properties that have the DscProperty attribute, even when you don't specify that property in a DSC Configuration or when using Invoke-DscResource. Because value type properties have a non-null default value, there's no way for a class-based DSC Resource to distinguish between a property that wasn't specified and one that was specified as the default value.

Important

When you use a class-based DSC Resource, any value type properties you don't explicitly set are always set to their default value as defined in the DSC Resource.

It isn't possible to use a class-based DSC Resource without managing every value type property defined for that DSC Resource. Unlike other DSC Resources, removing a property from your DSC Configuration or from the call to Invoke-DscResource doesn't ignore that property's state. It resets it to the default.

Only reference type properties can be unmanaged for a class-based DSC Resource.

You can check whether a type is a reference or value type by inspecting its BaseType.

function Test-IsValueType ($Object) {
    $base = $Object.GetType().BaseType
    while ($true) {
        switch ($base.FullName) {
            'System.Object' { return $false }
            'System.ValueType' { return $true }
            default {
                $base = $base.BaseType
            }
        }
    }
}

Test-IsValueType 'foo'
Test-IsValueType 5
False
True

This table includes a few types commonly used in DSC Resources. It notes whether they're reference or value types and, if they're value types, what their default value is when a DSC Resource is initialized.

Name Type Default Value
System.Enum Value 0
System.Int32 Value 0
System.Double Value 0
System.DateTime Value January 1, 0001 12:00:00 AM
System.TimeSpan Value All fields set to 0.
System.String Reference $null
System.Management.Automation.PSCredential Reference $null

Validation property attributes

The properties of a class-based DSC Resource may also use validation attributes to constrain the user-specified values for a property. The validation is applied when you compile a DSC Configuration or call Invoke-DSCResource. VS Code doesn't validate the values specified in your DSC Configuration while you're editing it.

Caution

When using Invoke-DscResource, validation failures for the properties don't stop the cmdlet from invoking the Get(), Test(), or Set() method. To prevent the cmdlet from invoking a method when the input fails a property's validation attribute, specify your ErrorActionPreference as Stop.

Defining validation attributes for properties is simpler than implementing the same logic in a method and is discoverable metadata about the class that defines your DSC Resource. Where possible, it's better to be explicit about the values your DSC Resource's properties accept. This clarifies usage and provides a safer and more reliable experience.

Consider this definition snippet of the MyDscResource class-based DSC Resource:

[DscResource()]
class MyDscResource {
    [DscProperty()]
    [string]
    [ValidateSet('JSON', 'YAML')]
    $Format
}

It uses the ValidateSet attribute to limit the valid values of the Format property to JSON and YAML. If you use Invoke-DscResource with an invalid value for Format, the cmdlet errors:

$Parameters = @{
    Name       = 'MyDscResource'
    ModuleName = 'MyDscResources'
    Method     = 'Get'
    Property   = @{
        Path     = '/Dsc/Example/config.yaml'
        Format   = 'Incorrect'
        Settings = @{
            Foo = 'Bar'
        }
    }
}
Invoke-DscResource @Parameters -ErrorAction Stop
Invoke-DscClassBasedResource: Exception setting "Format": "The argument
"Incorrect" does not belong to the set "YAML,JSON" specified by the
ValidateSet attribute. Supply an argument that is in the set and then
try the command again."

For more information about the validation attributes, see about_Functions_Advanced_Parameters.

Enum properties

For a better authoring and user experience than using the ValidateSet attribute, you can define an Enum that specifies a set of valid values.

For example, you could define a FormatOption enum and use it as the type for the Format property of a class-based DSC Resource:

enum FormatOption {
    JSON
    YAML
}

[DscResource()]
class MyDscResource {
    [DscProperty()]
    [FormatOption]
    $Format
}

The error message for an invalid enum value is shorter and clearer than for ValidateSet:

$Parameters = @{
    Name       = 'MyDscResource'
    ModuleName = 'MyDscResources'
    Method     = 'Get'
    Property   = @{
        Path     = '/Dsc/Example/config.yaml'
        Format   = 'Incorrect'
        Settings = @{}
    }
}
Invoke-DscResource @Parameters -ErrorAction Stop
Invoke-DscClassBasedResource: Exception setting "Format": "Cannot
convert value "Incorrect" to type "FormatOption". Error: "Unable to
match the identifier name Incorrect to a valid enumerator name. Specify
one of the following enumerator names and try again: JSON, YAML""

Enums are also useful for when you need to use the same property across several class-based DSC Resources. You can define the enum once and use it everywhere you need to, whereas with a ValidateSet attribute, you need to update every DSC Resource that shares the property.

For more information on enums in PowerShell, see about_Enum.

The Ensure property

Many DSC Resources have an Ensure property, which controls the state of an instance of a DSC Resource. For example, the User DSC Resource in the PSDscResources module has an Ensure property that takes the values Present (indicating the user should exist) and Absent (indicating the user shouldn't exist).

For class-based DSC Resources, the suggested practice is to create an Ensure enum and use it for any ensurable DSC Resources as the type for a property named Ensure. For example:

enum Ensure {
    Absent
    Present
}

[DscResource()]
class MyDscResource {
    [DscProperty()]
    [Ensure]
    $Ensure
}

While it's common for the Ensure enum to define the values Absent and Present, it can define any values that make sense for the DSC Resource.

Using this article's SimpleConfig DSC Resource as an example, instead of only having Absent (the config file shouldn't exist) and Present, you could define Ensure to have these values:

  • Absent - The configuration shouldn't exist at the specified Path. If it does, the DSC Resource should remove it from the system.
  • Exactly - The configuration should exist at the specified Path with the settings defined in the Settings property only. If any settings have the wrong value, the DSC Resource should correct them. If the configuration file has any settings not specified in the Settings property, it should remove them.
  • Include - The configuration should exist at the specified Path with the settings defined in the Settings property. If any settings have the wrong value, the DSC Resource should correct them. If the configuration file has any settings not specified in the Settings property, it should ignore them.
  • Present - The configuration should exist at the specified Path. If it doesn't, the DSC Resource should create it with the settings defined in the Settings property. If it does, the DSC Resource should report the instance as being in the desired state even if the settings don't match those specified in the Settings property.

That enum could be defined like this:

enum Ensure {
    Absent
    Exactly
    Include
    Present
}

Complex properties

Some properties of your DSC Resource might have subproperties. For example, the Settings property of this article's SimpleConfig DSC Resource has been specified earlier in this article with the Hashtable type. That allows a user to specify any key names and any value types for every key.

Instead, to control the valid options, you can write a class that represents a complex property. For the properties of this class to be recognized by DSC as subproperties, they must have the DscProperty attribute.

class SimpleConfigSettings {
    [DscProperty()] [string]
    $ProfileName

    [DscProperty()] [string]
    $Description

    [DscProperty()] [int]
    [ValidateRange(0,90)]
    $CheckForUpdates
}

The SimpleConfigSettings class defines three settings: ProfileName as a String, Description as a String, and CheckForUpdates as an Int whose value must be between 0 and 90.

Note

Even though complex properties are defined as classes and their subproperties must have the DscProperty attribute, the property attribute on the DSC Resource is the only one that applies any behavioral changes to the properties. Marking the subproperties as Key, Mandatory, or NotConfigurable has no effect on the behavior of DSC when compiling a DSC Configuration or using Invoke-DscResource.

With the class defined, it can be used as a property of the DSC Resource:

[DscResource()]
class SimpleConfig {
    [DscProperty(Mandatory)] [SimpleConfigSettings]
    $Settings
}

With Settings defined as the SimpleConfigSettings type, users get clear information about the value they need to supply.

$Parameters = @{
    Name       = 'MyDscResource'
    ModuleName = 'MyDscResources'
    Method     = 'Get'
    Property   = @{
        Path   = '/Dsc/Example/config.yaml'
        Format = 'YAML'
        Settings  = @{ Name = 'Foo' }
    }
}

Invoke-DscResource @Parameters -ErrorAction Stop
Invoke-DscClassBasedResource: Exception setting "Settings": "Cannot
create object of type "SimpleConfigSettings". The Name property was not
found for the SimpleConfigSettings object. The available property is:
[ProfileName <System.String>] , [Description <System.String>] ,
[CheckForUpdates <System.Int32>]"

Complex properties can have properties that are also complex properties. There is no limit to the level of nesting you can use. For the best user experience, limit the depth of complex properties to three levels.

class SimpleConfigUpdateSettings {
    [DscProperty()] [int]
    [ValidateRange(0,90)]
    $Interval

    [DscProperty()] [string]
    $Url
}

class SimpleConfigSettings {
    [DscProperty()] [string]
    $ProfileName

    [DscProperty()] [string]
    $Description

    [DscProperty()] [SimpleConfigUpdateSettings]
    $Updates
}

[DscResource()]
class SimpleConfig {
    [DscProperty(Mandatory)] [SimpleConfigSettings]
    $Settings
}

This example shows how defining nested complex properties provides validation. This validation provides useful error messages when you call Invoke-DscResource.

$Parameters = @{
    Name       = 'MyDscResource'
    ModuleName = 'MyDscResources'
    Method     = 'Get'
    Property   = @{
        Path     = '/Dsc/Example/config.yaml'
        Format   = 'YAML'
        Settings = @{
            ProfileName = 'Foo'
            Updates = @{
                Interval = 30
                Oops = 'Invalid property'
            }
        }
    }
}

Invoke-DscResource @Parameters -ErrorAction Stop
Invoke-DscClassBasedResource: Exception setting "Settings": "Cannot
create object of type "SimpleConfigSettings". Cannot create object of
type "SimpleConfigUpdateSettings". The Interval property was not found
for the SimpleConfigUpdateSettings object. The available property is:
[UpdateInterval <System.Int32>] , [UpdateUrl <System.String>]"

The Reasons property

If your class-based DSC Resource is intended for use with Azure Automanage's machine configuration feature, your DSC Resource must have Reasons property that meets the following requirements:

  • It must declared with the NotConfigurable property on the DscProperty attribute.
  • It must be an array of objects that have a String property named Code, a String property named Phrase, and no other properties. You can define the object type as hashtable or create the object as a complex property.

Machine configuration uses the Reasons property to standardize how compliance information is presented. Each object returned by the Get() method for the Reasons property identifies how and why an instance of the DSC Resource isn't compliant.

Machine configuration expects the Code and Phrase properties. The Phrase property should be a human-readable string that explains how the instance is out of the desired state. The Code property must be formatted specifically so machine configuration can clearly display the audit information for the DSC Resource.

The Code property uses this formatting without any spaces:

<ResourceName>:<ResourceName>:<Identifier>

In this formatting, replace <ResourceName> with the actual name of the DSC Resource and <Identifier> with a short name for why the DSC Resource is out of state. The Code property must not include any whitespace characters. For example, if a DSC Resource called Tailspin was out of compliance because the configuration file doesn't exist, the Code might be Tailspin:Tailspin:ConfigFileNotFound.

This snippet shows how you can define the Reasons property as a hashtable:

class MyDscResource {
    [DscProperty(NotConfigurable)]
    [hashtable[]] $Reasons
}

To define the Reasons property as a complex property, you need to define a class for it, then add it to the DSC Resource:

class MyModuleReason {
    [DscProperty()]
    [string] $Code

    [DscProperty()]
    [string] $Phrase
}

class MyDscResource {
    [DscProperty(NotConfigurable)]
    [MyModuleReason[]] $Reasons
}

Note

The class defined for the Reasons property is named MyModuleReason instead of Reason, using the module's name as a prefix. While you can give the class any name, if two or more modules define a class with the same name and are both used in a configuration, PowerShell raises an exception.

To avoid exceptions caused by name conflicts in DSC and machine configuration, always prefix the name of the class you define for the Reasons property.

Non-resource properties

When defining a class-based DSC Resource, you can add properties that don't have the DscProperty attribute. These properties can't be used directly with Invoke-DscResource or in a DSC Configuration. They can be used internally by the class or directly by a user creating an instance of the class themselves.

For more information on class properties, see about_Classes.

Class-based DSC Resource methods

Class-based DSC Resources must implement three methods:

  • Get() to retrieve the current state of the DSC Resource
  • Test() to validate whether the DSC Resource is in the desired state
  • Set() to enforce the desired state of the DSC Resource

A method's signature is defined by its expected output type and parameters. The class won't be recognized as a valid DSC Resource if it doesn't contain the correct signatures for these methods.

PowerShell class methods are different from functions in a few important ways. For the purposes of writing a class-based DSC Resource, these are the most important:

  • In class methods, no objects get sent to the pipeline except those mentioned in the return statement. There's no accidental output to the pipeline from the code. If the method has an output type other than Void, you must specify the return statement to emit an object of that type.
  • Non-terminating errors written to the error stream from inside a class method aren't passed through. You must use throw to surface a terminating error.
  • To access the value of a property of the class instance, use $this.<PropertyName>. Don't set or update any properties of the class that have the DscProperty attribute. Those are set when the user specifies the Property parameter with Invoke-DscResource or when defining the DSC Resource block in a DSC Configuration.

For more information on class methods, see about_Classes.

The methods can call cmdlets and native commands, including ones defined in the same module as the class-based DSC Resource.

Get

The Get() method is used to retrieve the current state of the DSC Resource and return it as an object. It must define its output as the class itself and take no parameters.

For example, a class-based DSC Resource called MyDscResource must have this signature:

[MyDscResource] Get() {
    # Implementation here
}

To make the Get() method return the correct object, you need to create an instance of the class. Then you can populate that instance's properties with the current values from the system. Finally, use the return keyword to output the current state.

[MyDscResource] Get() {
    $CurrentState = [MyDscResource]::new()

    if (Test-Path -Path $this.Path) {
        $CurrentState.Ensure = [Ensure]::Present
        $CurrentState.Path   = $this.Path
    } else {
        $CurrentState.Ensure = [Ensure]::Absent
    }

    return $CurrentState
}

In the example implementation, the Get() method initializes the $CurrentState variable with the default constructor for the MyDscResource class. It checks to see if the file specified in the Path property exists. If it does, the method sets the Ensure and Path properties on $CurrentState to Present and the appropriate path. If it doesn't, the method sets Ensure to Absent. Finally, the method uses a return statement to send the current state as output.

Test

The Test() method is used to validate whether the DSC Resource is in the desired state and returns $true if it's in the desired state or $false if it isn't. It must define boolean output and take no parameters.

The Test() method's signature should always match this:

[bool] Test() {
    # Implementation here
}

Instead of reimplementing the logic from the Get() method, call the Get() method and assign its output to a variable.

[bool] Test() {
    $InDesiredState = $true

    $CurrentState = $this.Get()

    if ($CurrentState.Ensure -ne $this.Ensure) {
        $InDesiredState = $false
    }

    # Check remaining properties as needed.

    return $InDesiredState
}

In the example implementation, the Test() method initializes a variable, $InDesiredState, as $true before setting $CurrentState to the output of the Get() method for the class-based DSC Resource.

It then checks the values of the properties of $CurrentState against those specified on the DSC Resource instance, setting $InDesiredState to $false if any properties aren't in the desired state.

Finally, it outputs whether the DSC Resource instance is in the desired state with a return statement.

Set

The Set() method is used to enforce the desired state of the DSC Resource. It doesn't have any output and takes no parameters.

The Set() method's signature should always match this:

[void] Set() {
    # Implementation here
}

The implementation of the Set() method can't use any return statements. It should be written to idempotently enforce the desired state.

Note

You may need to retrieve the current state with the Get() method if you need to enforce the desired state depending on the current state of the system.

For example, you might have logic for creating a service when it doesn't exist instead of correcting an incorrect property value.

This is also why it's important when using Invoke-DscResource to call the Test() method and only call the Set() method if Test() returns $false. While all DSC Resources should be idempotent, you have no guarantee that any DSC Resource is truly idempotent without reviewing its implementation.

Optional methods

Beyond the required Get(), Test(), and Set() methods, a class-based DSC Resource can define any number of additional methods. One use case for this is to define helper methods, such as for code used in more than one of the required methods.

This is also useful when defining a class that represents a software component for more than just configuration. For example, you might define a class that's both a DSC Resource enabling idempotent configuration of an application and calling the application itself to perform tasks. That would enable you to define the class once and use it both with DSC and functions exported from your module for using the application.

Constructors

A constructor is a method that creates a new instance of a class. The name of the method is always the same as the name of the class and doesn't define an output type.

It isn't mandatory to define any constructors for a class-based DSC Resource. If you don't define a constructor, a default parameterless constructor is used. This constructor initializes all members to their default values. Object types and strings are given null values.

When you define a constructor, no default parameterless constructor is created. A class-based DSC Resource must have a parameterless constructor, so if you define any custom constructors, at least one of them must be defined without any parameters.

When you use Invoke-DscResource or compile a DSC Configuration, the DSC Resource is created with the parameterless constructor and then each specified property is set on the instance.

Note

When you define a constructor, it should only throw an exception if the DSC Resource isn't valid on the system for some reason. For example a class-based DSC Resource that only works on Windows might throw an exception if created on a non-Windows system.

For more information about constructors, see about_Classes

Defining a default constructor

Class-based DSC Resources require that the class implements either no constructors or one with this signature:

<DscResourceClassName>() {
    # Implementation here
}

If there's no required logic to perform, like setting defaults based on the operating system, you don't need to write any code in the constructor. The following example shows a minimal definition for the default constructor:

MyDscResource() {}

You can add code to the constructor to set default values for properties of your class. For example, you may want to set a value based on the target operating system:

MyDscResource() {
    if ($IsWindows) {
        $this.Format = 'JSON'
    } else {
        $this.Format = 'YAML'
    }
}

Note

Remember, property validation in DSC happens after the constructor is used, not during. Don't try to validate any properties in the constructor.

Defining a custom constructor

DSC only calls the parameterless constructor. However, you can define other constructors with parameters if the class is used in other circumstances, such as by functions in your module.

For more information on defining constructors, see about_Classes

Best practices

This is a non-exhaustive list of best practices for authoring high quality class-based DSC Resources that are idempotent, safe, and maintainable. It supplements the DSC Resource authoring checklist, which defines more general guidance for authoring DSC Resources. The practices in this section are specific to class-based DSC Resources.

Use validation attributes

If you can validate a property with one or more of the validation attributes, do so. This validation is checked before any method is called with Invoke-DscResource and when you compile a DSC Configuration. Raising an error earlier reduces the chances that the DSC Resource will fail in an unpredictable way when enforcing state.

The errors raised by the built-in validation attributes are also clear about which property had an invalid value and how the value was invalid.

Note

Only use validation attributes to ensure the user input for a property is valid, not to see whether the property is in the correct state. That's what the Test() method is for.

For example, don't validate that a Path value exists already unless it's required to exist for the rest of the DSC Resource's logic to work. If the DSC Resource creates a file at that location, don't validate its existence on the property declaration.

If you're using any complex properties, be sure to apply validation attributes to those subproperties as well. Those subproperties are validated at the same time as their parent property.

Use enums instead of ValidateSet

If a property of the class-based DSC Resource has a list of valid values it accepts, define it as an enum property instead of using the ValidateSet attribute.

This gives users a better error message and makes maintenance easier if you use those values anywhere else in your module. Instead of having to update every cmdlet or check for that property, you can update the enum definition.

Remember that enums are value type properties. They default to 0 when an instance of the class-based DSC Resource is created. If you set an explicit default value for an enum property, that enum can't be unmanaged. If a user doesn't specify the property, the DSC Resource behaves the same as if the user had explicitly specified the default value.

To support unmanaged enum properties, make sure not to define a label for the 0 value. By default, if you don't specify the integer value for any labels when using the enum statement, the first declared label has a value of 0. Instead, define the first label with a value of 1 (or any higher value).

For example:

enum OptionalSetting {
    FirstOption = 1
    SecondOption
    ThirdOption
}

Then, in your Test() and Set() methods, you can ignore the enum property if the value is 0. In Test(), if the enum property is 0, ignore the current state of the DSC Resource on the system. Don't report the DSC Resource as out of desired state if the current state is a valid value. In Set(), make sure your logic for modifying system state ignores the enum property if it's 0.

Note

This method for implementing enum properties that can be unmanaged only works when the underlying value of the enum labels doesn't matter or when 0 isn't a valid underlying value.

Explicitly define default values for value type properties

When authoring a class-based DSC Resource, ensure that your properties specify correct default values for every value type property. You'll need to ensure your default values stay in sync with those of the component your DSC Resource manages.

Document the behavior of these properties for your users. Make sure that your documentation explains that these properties are set to their default values if the user doesn't specify them when using the DSC Resource.

Ensure the DSC Resource respects unmanaged reference type properties

Because reference type properties initialize to $null, unlike value type properties, a DSC Resource can distinguish between whether a reference type property was specified or not. Make sure that your DSC Resources ignore unmanaged reference type properties in the Test() and Set() methods.

In Test(), if the reference type property is $null, ignore the current state of the DSC Resource on the system. Don't report the DSC Resource as out of desired state if the current state isn't $null.

In Set(), make sure your logic for modifying the system state ignores any reference type properties that are $null.

Use a custom validation attribute instead of ValidateScript

For properties that require more complex validation, consider defining your own attribute that inherits from the ValidateArgumentsAttribute class or one of its derivatives.

This gives you much more control over the validation of the property and the messaging for when a specified value fails validation.

For example, this definition of the ValidateHttpsUrl attribute ensures that a specified value is a valid HTTPS URL.

using namespace System.Management.Automation

class ValidateHttps : ValidateArgumentsAttribute {
    [void]  Validate([object]$Url, [EngineIntrinsics]$engineIntrinsics) {
        [uri]$Uri = $Url

        if($Uri.Scheme -ne 'https') {
            $Message = @(
                "Specified value '$Url' is not a valid HTTPS URL."
                "Specify an absolute URL that begins with 'https://'."
            ) -join ' '

            throw [System.ArgumentException]::new($Message)
        }
    }
}

When applied to a property, the DSC Resource raises a useful error message:

$Parameters = @{
    Name       = 'MyDscResource'
    ModuleName = 'MyDscResources'
    Method     = 'Get'
    Property   = @{
      Url = 'http://contoso.com/updater'
    }
}

Invoke-DscResource @Parameters -ErrorAction Stop
Invoke-DscClassBasedResource: Exception setting "Url": "Specified value
'http://contoso.com/updater'  is not a valid HTTPS URL. Specify an absolute
URL that begins with 'https://'."

Note

Due to limitations in DSC, you can't define and use a custom validation attribute in the same module as the class-based DSC Resource. Instead, you need to define your custom validation attribute in a separate .psm1 file, add it to the main module's manifest in the NestedModules setting, and specify a using statement in the file your class-based DSC Resource is defined in.

For example, if you defined the ValidateHttps attribute in Validators.psm1, you would need to have it in your module manifest:

{
    NestedModules = @(
        'Validators.psm1'
    )
}

And at the top of your module file where you define your class-based DSC Resource:

using module ./Validators.psm1

In this example Validators.psm1 is in the same folder as the module where the class-based DSC Resource is defined. If the files are in different folders, you need to specify the relative path to the module that defines the validation attribute.

Use complex properties instead of hashtables

If your DSC Resource has a property with known subproperties, create a class for it and define those subproperties on that class. This provides a more discoverable surface for the settings and allows you to apply validation on subproperties. For more information, see complex properties.

Add a Validation method if properties are interdependent

You may have a DSC Resource that has properties that must be combined to be validated. These properties can't be validated by a validation attribute. Add a validation method to your class-based DSC Resource and call it at the beginning of your Get(), Test(), and Set() methods.

For example, if your DSC Resource for managing a configuration file has the Path and Extension properties you might need to validate:

  • That the extension of a file specified as the Path either matches the value of Extension or Extension isn't specified
  • That Extension is specified if the value of Path is a folder instead of a file

Your validation method should have an unambiguous name, not return any output, and throw if the instance fails validation.

[void] ValidatePath() {
    $ExtensionSpecified = ![string]::IsNullOrEmpty($this.Extension)
    $PathExtension = Split-Path -Path $this.Path -Extension

    if (
        $PathExtension -and
        $ExtensionSpecified -and
        ($PathExtension -ne $this.Extension)
    ) {
        $Message = @(
            "Specified Path '$($this.Path)' has an extension ('$PathExtension')"
            "which doesn't match the value of the Extension property"
            "'$($this.Extension)'. When specifying the Extension property with"
            "the Path property as a specific file, the extension of Path's"
            "value must be the same as the value of Extension or Extension must"
            "not be specified."
        ) -join ' '

        throw [System.ArgumentException]::new($Message)
    } elseif (!$ExtensionSpecified) {
        $Message = @(
            "Specified Path '$($this.Path)' has no extension and the Extension"
            "property wasn't specified. When the value of Path is a folder, the"
            "Extension property is mandatory. Specify a value for Extension."
        ) -join ' '

        throw [System.ArgumentException]::new($Message)
    }
}

If you need to validate several different parameters or groups of parameters in this way, define a generic validation method that calls the others. Use that method in Get(), Test(), and Set().

Extract shared code into methods or functions

If your methods are lengthy with complex logic or reuse the same code between them, create a helper method or function and move the code there instead. This makes testing and maintaining your DSC Resource easier. It also makes your DSC Resource's methods easier to read and understand.