Skip to content

Proposal: Define method signatures for PSDSC resource classes #860

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
michaeltlombardi opened this issue Jun 3, 2025 · 8 comments
Open
Labels
Issue-Enhancement The issue is a feature or idea Needs Triage

Comments

@michaeltlombardi
Copy link
Collaborator

Summary of the new feature / enhancement

As an author implementing DSC resources in PowerShell, I want to be able to implement a PowerShell class that can fully participate in the semantics of both DSC and PSDSC.

Currently, implementing a PSDSC resource requires the following method signatures, which map to the operations in PSDSC:

  • [<ResourceClassTypeName>] Get()

    Returns a single instance of the class representing the actual current state.

  • [bool] Test()

    Returns $true if the instance is in the desired state and $false otherwise.

  • [void] Set()

    Enforces the desired state for the instance.

These method signatures are checked by the parser when a PowerShell class has the [DscResource()] attribute. The parser also checks:

  • Whether the class defines at least one string or enum property with the [DscProperty(Key)] attribute.
  • Whether the class has a default constructor (if it defines a non-default constructor, it must also explicitly define a default constructor)

For DSC, we have added support for the export operation with the following method signature:

static [<ResourceClassTypeName>[]] Export()

However, we don't currently support:

  • The delete operation,
  • Invoking the set operation in what-if mode,
  • Returning specific information for how a resource is out of the desired state when the Test() method returns $false,
  • Returning the final state of an instance, optionally with the list of changed properties, for the set operation.

I propose that we consider and define method signatures in support of the semantics of DSC. When we implement the PowerShell resource development kit (RDK), we can provide a bridge between classic implementations for PSDSC and DSC-compliant semantics through a base class.

Proposed technical implementation details (optional)

I propose supporting the following method signatures for PowerShell classes.

Test operation method signatures

  • Result object (requires dependency)

    static [DscResourceTestResult] Test(
      [<ResourceClassTypeName>]$instance
    )

    This signature assumes the availability of the Microsoft.PowerShell.Dsc module to provide the [DscResourceTestResult], which would have a definition like:

    class DscResourceTestResult {
      [DscResourceInstance] $ActualState
      [bool]                $InDesiredState
      [string[]]            $DifferingProperties
    }
    class DscResourceInstance {
      hidden [ValidateIsDscResourceClass()] [object] $value
    }

    Which we could use to provide the equivalent of the state and stateAndDiff outputs without requiring the resource to define an InDesiredState property. The DscResourceInstance class is just a lightweight type to ensure that the value is always an instance of a DSC resource.

  • Tuples (no dependency)

    # State
    static [System.Tuple[<ResourceClassTypeName>, bool]] Test(
      [<ResourceClassTypeName>]$instance
    )
    # State and diff
    static [System.Tuple[<ResourceClassTypeName>, bool, string[]]] Test(
      [<ResourceClassTypeName>]$instance
    )

    This option is more complex for the author, but doesn't require a dependency.

Set operation method signatures

Here we could enable resource authors to support what-if mode with less overhead. If the class doesn't implement the signature with the whatIf parameter, the resource doesn't support invoking the set operation in what-if mode. If the resource does support what-if mode, authors would only need to implement their code in that method. The other method could just invoke the what-if mode method with whatIf as $false.

  • Result object (requires dependency)

    static [DscResourceSetResult] Set(
      [<ResourceClassTypeName>]$instance,
      [bool]$whatIf
    )
    static [DscResourceSetResult] Set(
      [<ResourceClassTypeName>]$instance
    )}

    This signature assumes the availability of the Microsoft.PowerShell.Dsc module to provide the [DscResourceSetResult], which would have a definition like:

    class DscResourceTestResult {
      [DscResourceInstance] $AfterState
      [string[]]            $ChangedProperties
    }
    class DscResourceInstance {
      hidden [ValidateIsDscResourceClass()] [object] $value
    }

    Which we could use to provide the equivalent of the state and stateAndDiff outputs. The DscResourceInstance class is just a lightweight type to ensure that the value is always an instance of a DSC resource.

  • Tuples (no dependency)

    # State
    static [<ResourceClassTypeName>] Set(
      [<ResourceClassTypeName>]$instance
    )
    static [<ResourceClassTypeName>] Set(
      [<ResourceClassTypeName>]$instance,
      [bool]$whatIf
    )
    # State and diff
    static [System.Tuple[<ResourceClassTypeName>, string[]]] Set(
      [<ResourceClassTypeName>]$instance
    )
    static [System.Tuple[<ResourceClassTypeName>, string[]]] Set(
      [<ResourceClassTypeName>]$instance,
      [bool]$whatIf
    )

    This option is more complex for the author, but doesn't require a dependency.

Delete operation method signatures

For resources that support the delete operation, we could expect the following signatures:

static [void] Delete(
    [<ResourceClassTypeName>]$instance
)
static [void] Delete(
  [<ResourceClassTypeName>]$instance,
  [bool]$whatIf
)

As with the set operation methods, this would enable authors to implement what-if support with
little overhead.

Export operation method signatures

For resources that support the export operation, we could expect the following signatures:

static [<ResourceClassTypeName>[]] Export(
  [<ResourceClassTypeName>]$filteringInstance
)

static [<ResourceClassTypeName>[]] Export()

Here the resource author could choose whether to support export with filters, similar to supporting
what-if modes for the set and delete operations. We could infer from static analysis whether the
resource supports filtering.

Get operation method signature

static [<ResourceClassTypeName>] Get(
  [<ResourceClassTypeName>]$instance
)

This static method returns the actual state for the provided instance. This is nearly identical to the PSDSC signature, except that it's static (and thus requires passing the instance instead of invoking the method on an instance.

This is primarily to bring the method signature in line with the other proposed signatures.

Example classes

The following example classes show the methods together.

  • Without dependencies

    [DscResource()]
    class TSToysCliSettings {
      [DscProperty(Key)]
      [ValidateSet("machine", "user")]
      [string]
      $SettingsScope
    
      [DscProperty()]
      [System.Nullable[bool]]
      $UpdateAutomatically
    
      [DscProperty()]
      [ValidateRange(1, 90)]
      [System.Nullable[int]]
      $UpdateFrequency
    
      [DscProperty()]
      [bool]$Exist = $true
    
      #region    DSC static methods
      static [TSToysCliSettings] Get([TSToysCliSettings]$instance) {
        # elided for brevity
      }
      static [System.Tuple[TSToysCliSettings, bool]] Test([TSToysCliSettings]$instance) {
        # elided for brevity
      }
      static [TSToysCliSettings] Set([TSToysCliSettings]$instance) {
        [TSToysCliSettings]::Set($instance, $false)
      }
      static [TSToysCliSettings] Set([TSToysCliSettings]$instance, [bool]$whatIf) {
        # elided for brevity
      }
      static [void] Delete([TSToysCliSettings]$instance) {
        [TSToysCliSettings]::Delete($instance, $false)
      }
      static [void] Delete([TSToysCliSettings]$instance, [bool]$whatIf) {
        # elided for brevity
      }
      static [TSToysCliSettings[]] Export() {
        [TSToysCliSettings]::Export($null)
      }
      static [TSToysCliSettings[]] Export([TSToysCliSettings]$filteringInstance) {
        # elided for brevity
      }
      #endregion DSC static methods
      #region    PSDSC instance methods
      [TSToysCliSettings] Get() {
        return [TSToysCliSettings]::Get($this)
      }
      [bool] Test() {
        return [TSToysCliSettings]::Test($this).Item2
      }
    
      [void] Set() {
        [TSToysCliSettings]::Set($this)
      }
      #endregion PSDSC instance methods
    }
  • With dependency (no base class)

    [DscResource()]
    class TSToysCliSettings {
      [DscProperty(Key)]
      [ValidateSet("machine", "user")]
      [string]
      $SettingsScope
    
      [DscProperty()]
      [System.Nullable[bool]]
      $UpdateAutomatically
    
      [DscProperty()]
      [ValidateRange(1, 90)]
      [System.Nullable[int]]
      $UpdateFrequency
    
      [DscProperty()]
      [bool]$Exist = $true
    
      #region    DSC static methods
      static [TSToysCliSettings] Get([TSToysCliSettings]$instance) {
        # elided for brevity
      }
      static [DscResourceTestResult] Test([TSToysCliSettings]$instance) {
        # elided for brevity
      }
      static [DscResourceSetResult] Set([TSToysCliSettings]$instance) {
        [TSToysCliSettings]::Set($instance, $false)
      }
      static [DscResourceSetResult] Set([TSToysCliSettings]$instance, [bool]$whatIf) {
        # elided for brevity
      }
      static [void] Delete([TSToysCliSettings]$instance) {
        [TSToysCliSettings]::Delete($instance, $false)
      }
      static [void] Delete([TSToysCliSettings]$instance, [bool]$whatIf) {
        # elided for brevity
      }
      static [TSToysCliSettings[]] Export() {
        [TSToysCliSettings]::Export($null)
      }
      static [TSToysCliSettings[]] Export([TSToysCliSettings]$filteringInstance) {
        # elided for brevity
      }
      #endregion DSC static methods
      #region    PSDSC instance methods
      [TSToysCliSettings] Get() {
        return [TSToysCliSettings]::Get($this)
      }
      [bool] Test() {
        return [TSToysCliSettings]::Test($this).InDesiredState
      }
    
      [void] Set() {
        [TSToysCliSettings]::Set($this)
      }
      #endregion PSDSC instance methods
    }
  • With dependency and base class

    class TSToysCliSettings : DscResourceBaseClass {
      [DscProperty(Key)]
      [ValidateSet("machine", "user")]
      [string]
      $SettingsScope
    
      [DscProperty()]
      [System.Nullable[bool]]
      $UpdateAutomatically
    
      [DscProperty()]
      [ValidateRange(1, 90)]
      [System.Nullable[int]]
      $UpdateFrequency
    
      [DscProperty()]
      [bool]$Exist = $true
    
      #region    DSC static methods
      static [TSToysCliSettings] Get([TSToysCliSettings]$instance) {
        # elided for brevity
      }
      static [DscResourceTestResult] Test([TSToysCliSettings]$instance) {
        # elided for brevity
      }
      static [DscResourceSetResult] Set([TSToysCliSettings]$instance, [bool]$whatIf) {
        # elided for brevity
      }
      static [void] Delete([TSToysCliSettings]$instance, [bool]$whatIf) {
        # elided for brevity
      }
      static [TSToysCliSettings[]] Export([TSToysCliSettings]$filteringInstance) {
        # elided for brevity
      }
      #endregion DSC static methods
      
      <#
        The base class would provide friendly handling for:
    
        - Invoking the set and delete operations without implementing the non-what-if overloads
          in the derived class.
        - Invoking the export operation without implementing the non-filtering overload in the
          derived class.
        - Invoking the PSDSC instance methods without needing to implement them in the derived class.
      #>
    }

Additional thoughts

We could certainly support both the friendly result types and the tuples for method output - it
would complicate the implementation for the adapter/module, but it would enable authors to choose
the implementation that best suits their needs.

All of this would be much friendlier with the RDK, where we could iteratively enhance the DevX for
resource authors writing PowerShell.

Separating the DSC methods from the PSDSC instance methods would also ensure we can adjust/enhance the supported definitions as the capabilities and expectations for DSC evolve without affecting the PSDSC functionality.

@ThomasNieto
Copy link

We should really be using defined interfaces so that the user and adapter doesn't have to parse or discover method signatures.

@michaeltlombardi
Copy link
Collaborator Author

We should really be using defined interfaces so that the user and adapter doesn't have to parse or discover method signatures.

That's one option, for sure - it will require defining the interfaces in C# (since we can't do so in PowerShell).

Other options, which I've been considering for the RDK, include:

  • Defining a base class, to ease implementation (and provide a lot of default functionality).
  • Defining VS Code snippets and scaffolding cmdlets, to simplify creating new resource classes and methods.
  • Defining PSScriptAnalyzer rules, reusable pester tests, and analysis cmdlets (e.g. Test-DscResourceClass) to provide feedback usable locally and in CI.

My main (but not insurmountable) reluctance to requiring an interface is that my experience with the community shows that contributions to C# modules are at least an order of magnitude less frequent than for modules implemented in PowerShell.

My secondary reluctance to relying on an interface is the need to define several of them - because in DSC, not every resource needs to implement every method overload. I think this isn't a huge problem, but it's worth noting.

@Borgquite
Copy link

Might be missing a memo, but don't we use Ensure = Absent for deleting resources at present?

@Gijsreyn
Copy link
Contributor

@Borgquite
Copy link

Thanks. Why do we need the 'delete' operation as well as _exist = false; don't they do the same thing?

@Gijsreyn
Copy link
Contributor

I can explain it in my words, but I think the explanation by Steve on: #290 explains it perfectly :)

@Borgquite
Copy link

@Gijsreyn Ah I see, to make module authoring easier. That seems to make sense so long as 'delete' remains an optional operation.

@michaeltlombardi
Copy link
Collaborator Author

That seems to make sense so long as 'delete' remains an optional operation.

Most DSC operations are now optional. For practical purposes, most resources only need to implement the get and set operations to enable using the resource in configuration documents.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Issue-Enhancement The issue is a feature or idea Needs Triage
Projects
Status: Todo
Development

No branches or pull requests

4 participants