From 8b19c836c03f79e26588a54a2c07784bfe534ffd Mon Sep 17 00:00:00 2001 From: "Jason.Helmick" Date: Mon, 15 Dec 2025 11:54:57 -0800 Subject: [PATCH 1/2] Added rfc PSDSC Class based contract --- rfc/draft/rfcxxxx.md | 649 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 649 insertions(+) create mode 100644 rfc/draft/rfcxxxx.md diff --git a/rfc/draft/rfcxxxx.md b/rfc/draft/rfcxxxx.md new file mode 100644 index 000000000..d4f0bb623 --- /dev/null +++ b/rfc/draft/rfcxxxx.md @@ -0,0 +1,649 @@ +--- +RFC: RFCNNNN # WG will set the number after submission +Author: jahelmic # <@GitHubUserName> +Sponsor: michaeltlombardi # <@GitHubUserName> +Status: Draft # +SupercededBy: null # +Version: 1.0 # . +Area: DSC # +CommentsDue: null # +--- + +# Class-based PSDSC resource contract for DSC v3 + +This RFC defines a contract for PowerShell **class-based** PSDSC resources so that a single +implementation can: + +- Continue to work with **PSDSC v1/v2**, and +- Participate fully in **DSC v3 semantics** via the PSDSC adapter, + +without requiring a hard dependency on a Microsoft-shipped "DSC types" or RDK module. + +The RFC focuses on: + +- The **method signatures** and shapes DSC v3 cares about for PowerShell class-based resources. +- The **expected return structures** (results, messages, and streams). +- How this contract **aligns with JSON schema and manifest generation**, so that tooling (RDK, + Sampler, analyzers) can build on it. + +## Motivation + +> As a DSC resource author with existing PSDSC class-based resources, +> I want to augment those resources to participate in DSC v3 semantics, +> so that I can support both PSDSC v1/v2 and DSC v3 without maintaining separate codebases. + +Additional motivations: + +- Many resources already implemented as **class-based PSDSC resources** have active users and are + expensive to rewrite. +- DSC v3 introduces richer semantics and tooling: + - Structured results from `Test`, `Set`, and `Get` (not just a "Reason" string). + - Better "diff" semantics (for latest-version / package-like scenarios). + - JSON-schema-based validation and manifest-driven discovery. +- Resource authors should be able to: + - Incrementally add **V3-only capabilities** (e.g., richer test results, schema) to existing + class-based resources. + - Avoid taking a **mandatory dependency** on a Microsoft-shipped types or RDK module. +- The DSC community (including RDK / Sampler users) needs a **clear, documented contract** so that: + - Static analysis is feasible, + - Schema/manifest generation is consistent, + - ScriptAnalyzer rules can be updated coherently for V1/V2/V3 resources. + +## Proposed experience + +This section describes how the contract feels for a resource author and for DSC v3 consumers. + +### Authoring a class-based resource that works for PSDSC v1/v2 and DSC v3 + +A resource author today might have: + +```powershell +[DscResource()] +class ChocolateyPackage { + [DscProperty(Key)] + [string] $Name + + [DscProperty(Mandatory)] + [ValidateSet('Present', 'Absent')] + [string] $Ensure = 'Present' + + [ChocolateyPackage] Get() { ... } + [void] Set() { ... } + [bool] Test() { ... } +} +``` + +With this RFC, the author can introduce: + +```powershell +class ChocolateyPackage { + static [System.Tuple[bool, ChocolateyPackage, String[]]] Test( + [ChocolateyPackage]$instance + ) { + return Test-ChocolateyPackageResource -Instance $instance + } + + static [System.Tuple[ChocolateyPackage, String[]]] Set( + [ChocolateyPackage]$instance + ) { + Set-ChocolateyPackageResource -Instance $instance + } + + static [ChocolateyPackage] Get( + [ChocolateyPackage]$instance + ) { + return Get-ChocolateyPackageResource -Instance $instance + } + + static [void] Delete( + [ChocolateyPackage]$instance + ) { + Remove-ChocolateyPackageResource -Instance $instance + } + + static [ChocolateyPackage[]] Export( + [ChocolateyPackage]$filteringInstance + ) { + return Export-ChocolateyPackageResource -FilteringInstance $filteringInstance + } +} +``` + +Key changes: + +- All DSC v3-relevant methods are **static** and _optional_. If the class doesn't implement a static + method for an operation, DSC can use the PSDSC instance method for that operation. +- A single class supports **both PSDSC and DSC v3**. +- Authors may optionally return richer structured data for DSC v3. +- No mandatory dependency on Microsoft-owned types. + +### Using the resource in DSC v3 + +Example configuration: + +```yaml +resources: +- type: Contoso.DSC/ChocolateyPackage + name: InstallGit + properties: + Name: git +``` + +DSC v3 (via the PSDSC adapter): + +- Validates JSON against the generated schema. +- Calls `Test`, `Get`, `Set` with class-based resource instances. +- Accepts both simple and structured returns. +- Emits structured messages and differences. + +Resource consumers see **consistent behavior**, regardless of whether the implementation is native +DSC v3 or an adapted PSDSC class-based resource. + +## Specification + +> [!NOTE] +> Some aspects are deliberately scoped as "MVP" so that a pilot implementation (e.g., Chocolatey +> resources) can validate the design. Where details are not finalized, they are explicitly called +> out. + +### Resource class shape + +A DSC v3-compliant class-based resource MUST: + +- Declare a schema class representing the resource instance. +- Use **static methods** for DSC v3 interaction. +- Accept the schema class instance as the parameter to all methods. + +Skeleton: + +```powershell +[DscResource()] +class { + [DscProperty(Key)] + [string] $Name + + [DscProperty(Mandatory)] + [ValidateSet('Present', 'Absent')] + [string] $Ensure = 'Present' + + static [] Get([]$instance) {} + static [] Set([]$instance) {} + static [] Test([]$instance) {} + + # optional + static [] Export(...) + static [void] Delete(...) + static [] Schema(...) +} +``` + +### DSC operation method selection + +If a class has the `[DscResource()]` attribute, DSC and the adapter know that the resource class +implements the traditional PSDSC resource methods `Get()`, `Set()`, and `Test()`. + +When selecting the method to use for an operation, the adapter: + +1. Checks for the existance of a DSC static method for that operation. +1. If the resource class implements a static method for the operation, DSC invokes that method. +1. If the class doesn't implement a static method for the operation and the operation is part of + the PSDSC resource API, DSC uses the appropriate PSDSC instance method. +1. If the class doesn't implement a static method or instance method for the operation, the resource + can't be used for that operation and DSC raises an error. + +> [!NOTE] +> In this model, we _can_ support classes defined for DSC that don't have the `[DscResource()]` +> attribute and thus may not have the PSDSC instance methods. Supporting these classes is out of +> scope for the MVP. + +### Method signatures (MVP) + +For the MVP, this RFC proposes the following new method signatures. Each section defines method +signatures for a different DSC resource operation. In this proposal, we use the `Tuple` type for +structured return data. This enables static analysis and implementation without any dependencies +for defined types. + +Future revisions of this RFC may: + +- Introduce strongly-typed result classes (e.g., `DscTestResult`) that map to the same shape. +- Define a shared types module that authors can optionally reference. + +#### Get operation method + +Signature: + +```pwsh +static [] Get([]$instance) +``` + +The `get` operation must always return the actual state of the instance with all discoverable +properties populated. + +#### Set operation method + +Signatures: + +- No return data (DSC invokes `get` after `set` to generate after state and + changed properties): + + ```pwsh + static [void] Set([]$instance) + ``` + +- Return state only (DSC generates the changed properties arrray): + + ```pwsh + static [] Set([]$instance) + ``` + +- Return state and changed properties (DSC uses the result without processing): + + ```pwsh + static [System.Tuple[, String[]]] Set([]$instance) + ``` + +- To indicate that the resource supports `whatIf` mode operations as well as `actual`, the class + should define a method signature that expects a boolean parameter after the instance parameter: + + ```pwsh + # No return data + static [void] Set([]$instance, [bool]$whatIf) + # state return kind + static [] Set([]$instance, [bool]$whatIf) + # stateAndDiff return kind + static [System.Tuple[, String[]]] Set([]$instance, [bool]$whatIf) + ``` + +The `set` operation may use one of three return types: + +- The `[void]` return type maps to the same behavior and handling as the PSDSC `Set()` instance + method. +- The `[]` return type maps to the DSC `state` return kind for a command resource. +- The `[System.Tuple[, String[]]]` return type maps to the DSC `stateAndDiff` + return kind for a command resource. + +The `set` operation may support `whatIf` mode invocations. In this mode, the resource doesn't change the system. Instead, it reports _how_ it would modify the system. The return data for this +operation is the _expected_ final state and changed properties. The return type for the what-if method _must_ be the same as the actual method signature, such as: + +```pwsh +static [System.Tuple[ChocolateyPackage, String[]] Set( + [ChocolateyPackage]$instance, + [bool]$whatIf +) { + # Implementation +} +static [System.Tuple[ChocolateyPackage, String[]] Set( + [ChocolateyPackage]$instance +) { + [ChocolateyPackage]::Set($instance, $false) +} +``` + +#### Test operation method + +Signatures: + +- Return state only (DSC generates the differing properties array): + + ```pwsh + static [System.Tuple[bool, ]] Test([] $instance) + ``` + +- Return state and differing properties (DSC uses the result without processing): + + ```pwsh + static [System.Tuple[bool, , String[]]] Test([] $instance) + ``` + +The `test` operation may use one of two return types: + +- The `[System.Tuple[bool, ]]` return type maps to the DSC `state` return kind for + a command resource. Instead of requiring the class to define the `InDesiredState` read-only + property, DSC expects the resource to return the boolean value _and_ the actual state of the + resource. The adapter munges the result for DSC. +- The `[System.Tuple[bool, , String[]]]` return type maps to the DSC `stateAndDiff` + return kind for a command resource. + +#### Export operation method + +Signatures: + +- Non-filtering export (resource returns every discovered instance): + + ```pwsh + static [[]] Export() + ``` + +- Filtered export (resource uses the input instance to limit the return data): + + ```pwsh + static [[]] Export([]$filteringInstance) + ``` + +The return type for the `export` operation is always an array of instances of the resource class. + +The export functionality depends on which method signatures are implemented: + +- If the class implements both signatures, it supports filtered and unfiltered exports. +- If the class implements only the parameterless signature, it doesn't support filtered exports. +- If the class implements only the signature with a filtering instance, it doesn't support + unfiltered exports. +- If the class doesn't implement either signature, it doesn't support the `export` operation. + +#### Delete operation method + +Signature: + +```pwsh +static [void] Delete([]$instance) +``` + +In the current data model for DSC, the `delete` method returns no data. Only messages and +execution status (success or failure) are reported back to the engine. + +#### Schema method + +Signatures: + +```pwsh +static [string] Schema() +``` + +DSC expects the resource to return a string representation of the resource instance JSON Schema. +The output must validate against the resource instance meta schema at the following JSON pointer +URI: + +```text +https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/manifest.schema.json#/properties/embedded +``` + +If the resource doesn't implement this method, DSC generates a JSON Schema by inspecting the +resource class itself. + +> [!NOTE] +> Currently, DSC doesn't distinguish between the instance schema and the filtering instance +> schema. Issue #1232 proposes a model for separately validating resource properties for the +> `export` operation. Until that proposal is implemented, the only schema we can emit for a +> resource is the resource instance schema. +> +> In the future, we may add a second method, like `FilteringSchema()`, to account for this. + +### Attributes and metadata + +Some information cannot be derived from method signatures alone. This RFC proposes a small set of +**optional attributes** that can be applied to the class, properties and methods. + +These attributes MAY live in a shared types/module (e.g., RDK or `Microsoft.DSC.Types`). DSC v3 +MUST treat this module as an **optional dependency**: + +- If present, the adapter and tooling can leverage the attributes. +- If absent, equivalent metadata MAY be provided via structured return types or manifest entries. + +> [!NOTE] +> When considering alternatives to attributes, it would be _possible_ to retrieve this information +> with a set of new metadata methods or static properties. We could accept hashtables for those +> methods or properties to minimize the number of items to check, but that would then require +> validating the keys and data types. Each of the following sections has a collapsible details +> block that enumerates the possible metadata static properties, but in general using attributes is +> preferable for this purpose. + +#### `[DscResourceClass()]` attribute + +The `[DscResourceClass()]` attribute annotates the class itself to define metadata about the class: + +- `[DscResourceClass(DscVersion = '')]` - Indicates which version of DSC the resource + was developed with and to use for the `$schema` in the generated manifest. Values must match + the following regex: + + ```regex + ^v(?\d+)\.(?\d+).(?\d+)$ + ``` + + When this option isn't specified, the default value is `v3`. +- `[DscResourceClass(Type = '')]` - Defines the type name for the + resource. Values must be a valid resource type name, like `Contoso.Chocolatey/Package`. When + this option isn't specified, the default is `/`. +- `[DscResourceClass(Version = '')]` - Defines the semantic version for the + resource. Values must be a string that parses as a valid semantic version, like `1.2.3`. When + this option isn't specified, the default is the module version. +- `[DscResourceClass(Description = '')]` - Defines a short description for the + resource, surfaced in the `dsc resource list` command. No default value. +- `[DscResourceClass(Tags = ('', ..., ''))]` - Defines the tags for the resource. + +
Metadata static properties + +```pwsh +# Single method for all data, all fields optional: +static [hashtable] $ResourceMetadata = { + @{ + DscVersion = '' + Type = '' + Version = '' + Description = '' + Tags = @('', ..., '') + } +} +# Individual properties +static [string]$DscVersion = '' +static [string]$Type = '' +static [string]$Version = '' +static [string]$Description = '' +static [string[]]$Tags = @('', ..., '') +``` + +
+ +#### `[DscResourceProperty()]` attribute + +The `[DscResourceProperty()]` attribute enables authors to annotate their resource properties with DSC semantics: + +- `[DscResourceProperty(Canonical)]` - indicates that the property is a canonical DSC resource + property. This attribute is only valid on properties that have the same name. For example. the following is valid, annotating the `_exist` canonical property: + + ```pwsh + [DscResourceProperty(Canonical)] + [bool]$Exist = $false + ``` + + And the following snippet would be invalid, because `_ensure` isn't a canonical property: + + ```pwsh + [DscResourceProperty(Canonical)] + [string]$Ensure + ``` + + > [!NOTE] + > Ideally, we would have a way for the resource to use either the shorthand, + > `[DscResourceProperty(Canonical)]` or specify the name of the canonical property to help + > with property name conflicts, especially given the existence of canonical properties like + > `_name`, which may conflict with the ergonomic design of the resource (like a chocolatey + > package name). + > + > That would make the following definitions valid: + > + > ```pwsh + > [DscResourceProperty(Canonical)] + > [bool]$Exist = $false + > + > [DscResourceProperty(Canonical='_name')] + > [string]$InstanceName + > ``` + +- `[DscResourceProperty(ReadOnly)]` - indicates that the property is read-only and can be + returned from the resource but is never used as input _to_ the resource. +- `[DscResourceProperty(WriteOnly)]` - indicates that the property is write-only and can be + passed to the resource as input but is never returned in the output data. +- `[DscResourceProperty(Sensitive)]` - inidcates that the property is sensitive and should be + redacted from messaging and output. Only valid on properties that have a string, object, or + enum type. +- `[DscResourceProperty(Key)]` - indicates that the property uniquely identifies an instance of + the resource. +- `[DscResourceProperty(Required)]` - indicates that the property is mandatory for non-export + operations. + +The `[DscResourceProperty()]` can inherit values when the class defines the `[DscProperty()]` +attribute on the same property: + +- `[DscProperty(Key)]` - maps to `[DscResourceProperty(Key)]`. +- `[DscProperty(Mandatory)]` - maps to `[DscResourceProperty(Required)]`. +- `[DscProperty(NotConfigurable)]` - maps to `[DscResourceProperty(ReadOnly)]`. + +
Metadata static properties + +```pwsh +# Single property for all data, each key a different property, all fields optional: +static [hashtable] $ResourcePropertyMetadata = @{ + @{ + = @{ + ReadOnly = $false + WriteOnly = $false + Sensitive = $false + Key = $false + Required = $false + } + } +} +# Per property metadata, all fields optional: +static [hashtable]$Metadata = @{ + ReadOnly = $false + WriteOnly = $false + Sensitive = $false + Key = $false + Required = $false +} +# Individual properties for each resource property and option: +static [bool]$ReadOnly = $false +static [bool]$WriteOnly = $false +static [bool]$Sensitive = $false +static [bool]$Key = $false +static [bool]$Required = $false +``` + +
+ +#### `[DscResourceSet()]` attribute + +The `[DscResourceSet()]` attribute defines handling for the `set` method. Must be attached to a +static set method signature. If the resource defines a signature that indicates support for +`whatIf` mode, the attribute must be on that method. + +- `[DscResourceSet(implementsPretest)]` - Indicates that the resource is implemented to check + whether it needs to change system state before making any changes. This maps to the + `set.implementsPretest` resource manifest field. When this option isn't specified, the + default is `false`. +- `[DscResourceSet(handlesExist)]` - Indicates that the resource directly handles the `_exist` + canonical property. This maps to the `set.handlesExist` resource manifest field. When this + option isn't specified, the default is `false`. + +
Metadata static properties + +```pwsh +# Single property for all data, all fields optional: +static [hashtable] $SetOperation = @{ + @{ + ImplementsPretest = $false + HandlesExist = $false + } +} +# Per option metadata +static [bool] $SetOperationImplementsPretest = $false +static [bool] $SetOperationHandlesExist = $false +``` + +
+ +### JSON schema and manifest alignment + +The class contract MUST align with a JSON schema and manifest model: + +- **JSON schema**: + + - Represents properties, types, constraints, and read-only / write-only / sensitive flags. + - Ideally generated at **build time** from the PowerShell class (e.g., using `System.Text.Json`). + - May be embedded in a manifest or file alongside the resource module. + +- **Manifest**: + + - Describes which methods are implemented and what capabilities the resource supports (e.g., + supports `Export`, has rich `Test` results, etc.). + - Enables faster discovery and avoids heavy runtime analysis. + +This RFC does **not** fully define the JSON schema format or manifest schema, but it requires: + +- The class-based contract to expose enough information to: + + - Generate JSON schema with correct property naming and constraints. + - Generate a manifest that allows the adapter to skip expensive reflection where possible. + +> OPEN: +> +> - Naming conventions for JSON properties (camelCase vs PascalCase). +> - How to annotate canonical properties and avoid conflicts with WMI/LCM constraints (e.g., +> `__Name`). +> - Minimum set of fields a manifest must contain to support this contract. + +### PSDSC v2 adapter considerations + +This RFC assumes a PSDSC-based adapter for DSC v3 that: + +- Can load class-based PSDSC resources and recognize the proposed contract. +- May be shipped: + - As part of the PSDSC module, or + - As a separate adapter module with a dependency on PSDSC. + +> OPEN: +> +> - Final shipping model (in-module vs separate module) and versioning strategy. +> - How to clearly communicate to users what changed when PSDSC or the adapter is updated. + +## Alternate Proposals and Considerations + +### Functions as the primary contract + +An alternative approach was to make top-level functions the DSC v3 contract surface, using the +class only for schema: + +- Pros: + - Familiar for PowerShell users who prefer functions over classes. +- Cons: + - Requires DSC to reason about a more complex combination of functions and classes. + - Static analysis and manifest generation are simpler with everything on the class. + - Harder to express the contract as a single analyzable unit. + +This RFC proposes **static class methods** as the primary contract, with authors free to delegate to functions internally. + +### Mandatory shared types module + +Another alternative was to require all resources to depend on a shared types module (for result types, attributes, etc.): + +- Pros: + - Strong typing and IntelliSense for result objects and attributes. + - Clear place to evolve shared patterns. +- Cons: + - Introduces "dependency hell" for resource authors and consumers. + - Complicates versioning and servicing. + - Not necessary for basic functionality; generic structured returns are sufficient. + +This RFC opts for: + +- Generic structured forms (hash tables / objects) as the **baseline**. +- Optional shared types for authors who want richer tooling. + +### RDK on the critical path + +The working group explicitly does **not** want the Resource Development Kit (RDK) on the critical path: + +- RDK should be able to build on this contract once defined. +- The contract and adapter behavior must stand on their own. +- Community and Sampler-based tooling can implement schema/manifest generation independently. + +## Related work items + +- Issue: "Define method signatures for PSDSC resource classes" (link TBD) + + Describes the need to clarify what methods and signatures DSC v3 should look for on class-based resources. + +- Future (potential separate RFCs): + - PSDSC v2 adapter for DSC v3 (shipping model and behavior). + - JSON schema/manifest specification for DSC v3 resources. + - ScriptAnalyzer rule set for V1/V2/V3 DSC resources, including class-based patterns. From 67b9f29acd4fbe4ca38458f8be9cc08009eba757 Mon Sep 17 00:00:00 2001 From: Mikey Lombardi Date: Thu, 29 Jan 2026 18:02:25 -0600 Subject: [PATCH 2/2] (Fixup) Amend RFC based on WG feedback This change: 1. Gives the RFC its appropriate number. 1. Clarifies the semantics of DSC operations and differences with PSDSC 1. Clarifies the reasons pattern and how the combination of messages and DSC test result obviates the pattern. 1. Removes the sections about attributes and DevX 1. Adds a section clearly defining related concerns that are explicitly out of scope for the RFC with reasoning. 1. Clarifies behavior for when the resource doesn't define an explicit JSON Schema. 1. Scaffolds, but does not define examples demonstrating the effective behavior of the contract with a fictional resource. --- rfc/draft/rfc0001.md | 1284 ++++++++++++++++++++++++++++++++++++++++++ rfc/draft/rfcxxxx.md | 649 --------------------- 2 files changed, 1284 insertions(+), 649 deletions(-) create mode 100644 rfc/draft/rfc0001.md delete mode 100644 rfc/draft/rfcxxxx.md diff --git a/rfc/draft/rfc0001.md b/rfc/draft/rfc0001.md new file mode 100644 index 000000000..0a30a74f0 --- /dev/null +++ b/rfc/draft/rfc0001.md @@ -0,0 +1,1284 @@ +--- +RFC: RFC0001 # WG will set the number after submission +Author: jahelmic # <@GitHubUserName> +Sponsor: michaeltlombardi # <@GitHubUserName> +Status: Draft # +SupercededBy: null # +Version: 1.0 # . +Area: DSC # +CommentsDue: null # +--- + +# Class-based PSDSC resource contract for DSC v3 + +This RFC defines a contract for PowerShell **class-based** PSDSC resources so that a single +implementation can: + +- Continue to work with **PSDSC v1/v2**, and +- Participate fully in **DSC v3 semantics** via the PSDSC adapter, + +without requiring a hard dependency on a Microsoft-shipped "DSC types" or RDK module. + +The RFC focuses on: + +- The **method signatures** and shapes DSC v3 cares about for PowerShell class-based resources. +- The **expected return structures** (results, messages, and streams). +- How this contract **aligns with JSON schema and manifest generation**, so that tooling (RDK, + Sampler, analyzers) can build on it. + +## Motivation + +> As a DSC resource author with existing PSDSC class-based resources, I want to augment those +> resources to participate in DSC v3 semantics, so that I can support both PSDSC v1/v2 and DSC v3 +> without maintaining separate codebases. + +Additional motivations: + +- Many resources already implemented as **class-based PSDSC resources** have active users and are + expensive to rewrite. +- DSC v3 introduces richer semantics and tooling: + - Structured results from `Test`, `Set`, and `Get` (not just a "Reason" string). + - Better "diff" semantics (for latest-version / package-like scenarios). + - JSON-schema-based validation and manifest-driven discovery. +- Resource authors should be able to: + - Incrementally add **V3-only capabilities** (e.g., richer test results, schema) to existing + class-based resources. + - Avoid taking a **mandatory dependency** on a Microsoft-shipped types or RDK module. +- The DSC community (including RDK / Sampler users) needs a **clear, documented contract** so that: + - Static analysis is feasible, + - Schema/manifest generation is consistent, + - ScriptAnalyzer rules can be updated coherently for V1/V2/V3 resources. + +### DSC operation semantics + +In PSDSC, the only operations defined were: + +- `Get` to return an instance of the resource representing its actual current state. For + class-based resources, this mapped to the method signature: + + ```powershell + [] Get() {} + ``` + + The semantics of this operation are identical for both DSC and PSDSC. + +- `Test` to return a boolean value representing whether the current state of a resource instance is + correct for a given desired state. For class-based resources, this mapped to the method + signature: + + ```powershell + [bool] Test() {} + ``` + + In DSC, the `test` operation indicates not only _whether_ the resource instance is in the desired + state but which properties are non-compliant. The result an end user sees for a specific instance + looks like the following YAML snippet for a non-compliant instance: + + ```yaml + desiredState: + path: MyPackage + _exist: true + version: 1.2.3 + actualState: + name: MyPackage + _exist: true + version: 1.1.0 + inDesiredState: false + differingProperties: + - version + ``` + + Moreover, what DSC expects a resource to return is never just a boolean result: + + 1. A DSC resource that doesn't define the test operation itself can rely on DSC's synthetic + testing feature, which invokes the `get` operation for a resource and then compares the + desired state against the actual state to determine whether the instance is compliant and + which properties (if any) aren't in the desired state. + 1. A DSC resource that implements the `test` operation explicitly must return an instance of the + resource representing the actual state of the instance with the canonical `_inDesiredState` + read-only property populated with a boolean indicating whether the instance is compliant. + + The resource must return that data as a JSON Line. The resource may also emit a second JSON + Line containing a JSON array where every item in the array is a string matching the name of a + property that isn't in the desired state. + + If the resource doesn't return the second JSON Line as an array of differing properties, DSC + synthesizes the array itself by comparing the returned actual state and given desired state. + + The current implementation for the PSDSC adapter is to invoke the `Test` method for the resource + to determine whether the resource is in the desired state and compose that information with the + actual state of the resource by invoking the `Get` operation. + +- `Set` to idempotently enforce the resource instance to match the desired state. For class-based + resources, this mapped to the method signature: + + ```powershell + [void] Set() {} + ``` + + In DSC, the `set` operation returns more useful data to the user, showing them which properties + were modified and how they were changed. The result an end user sees for a specific instance + looks like the following YAML snippet for an instance that required changes: + + ```yaml + beforeState: + path: MyPackage + _exist: true + version: 1.1.0 + afterState: + name: MyPackage + _exist: true + version: 1.2.3 + changedProperties: + - version + ``` + + If the resource is implemented not to return any data from the `set` operation, DSC invokes the + `get` operation after the `set` operation concludes and uses that data to populate the + `afterState` map and the `changedProperties` array. + + If the resource is implemented to return data from the `set` operation, it _must_ return an + instance of the resource representing the actual state of the instance after the `set` operation. + + The resource must emit that data as a JSON Line. The resource may also emit a second JSON Line + containing a JSON array where every item in the array is a string representing the name of a + property that the resource modified to match the desired state. + + If the resource doesn't return the second JSON Line as an array of changed properties, DSC + synthesizes the array itself by comparing the returned actual state and given desired state. + +In addition to the differing semantics for the `Test` and `Set` operation return data, DSC supports +new operations that have no equivalence in PSDSC: + +- The `delete` operation removes an instance of the resource from a system. In the current + implementation, the `delete` operation returns no data. +- The `export` operation enumerates every instance of the resource on a system. Optionally, the + resource may support filtered `export` operations, where the user supplies a filtering instance + of the resource and only matching instances are returned. Resources may _require_ a filtering + instance in cases where enumerating every instance on a system is incoherent or may cause + critical performance issues, such as enumerating every file and folder on the system recursively. + +The contract for class-based PSDSC resources in DSC must account for these differences in semantics +to enable PSDSC resource authors to fully participate in the richer semantics of DSC. + +### Reasons pattern in PSDSC + +Due to the limited return data and operation semantics in PSDSC, many resources adopted a pattern +where they defined a read-only property named `Reasons` for the class-based DSC Resource. In +particular, this pattern was mandatory for resources to fully participate in the semantics of Azure +Machine Configuration. + +This required the resource author to: + +1. Define a class representing a _reason_ for why the resource is non-compliant. The class must + define the `Code` and `Phrase` properties with type `[string]`. The class must not define any + other properties. + + Because of type conflicts, resource authors were expected to redefine this class in their own + PSDSC module with a sufficiently distinct name to avoid conflicts across modules. +1. Add the `Reasons` property to their PSDSC resource as a non-configurable property that accepts + an array of instances of the reason class. + +An example of this pattern is shown in the following (incomplete) snippet: + +```powershell +class MyModuleReasons { + [DscProperty()] [string] $Code + [DscProperty()] [string] $Phrase +} + +[DscResource()] +class Package { + [DscProperty(NotConfigurable)] [MyModuleReasons[]] $Reasons +} +``` + +Implementing the reasons pattern required a resource author to populate compliance information in +the `Get` operation, since neither `Test` nor `Set` returned an instance of the resource. This +effectively shifts the logic for testing a resource instance from the `Test` method into the `Get` +method. + +This pattern arose due to limitations in the data that could be returned from a PSDSC resource. In +DSC, by contrast: + +- The `test` and `set` operations provide richer semantics for the operation results. The test + result data indicates whether the instance is in the desired state _and_ provides a list of + non-compliant properties along with both the desired state and actual state data. The set result + shows which properties the resource modified as well as the before and after state. + +- DSC supports resources emitting arbitrary messages during an operation which can serve to enhance + the level of detail a caller receives for that operation. + + For example, a DSC resource could emit messages for every non-compliant property that indicates + how and why that property is non-compliant. This information is bubbled up to the user but + doesn't affect the actual result object for the instance. + +- DSC supports a pattern for resources to emit additional metadata. The reasons pattern could be + incorporated into this idiom if emitting additional messages is insufficient. + +## Proposed experience + +This section describes how the contract feels for a resource author and for DSC v3 consumers. + +### Explicitly out of scope concerns + +Before describing the contract for defining a PowerShell class with methods for DSC, note that the +following concerns are out-of-scope for this RFC, though the authors admit that these concerns are +_related_, they should be discussed in separate RFCs: + +- Defining a DSC resource as a PowerShell class without the `[DscResource()]` attribute is + explicitly out of scope for this PR. There are concerns around discovering and surfacing such + resources from a PowerShell module that require their own RFC. + + The contracts for operation methods defined in this RFC are also a prerequisite for any such + RFC. + +- DSC introduces the concept of _canonical resource properties_. Every canonical resource property + has a standard JSON Schema and has a property name that begins with an underscore (`_`), like the + `_exist` canonical property. The purpose of canonical properties is to provide a set of properties + with specific semantics that the DSC engine and higher order tools may rely on. + + There are some concerns around canonical properties that need to be addressed for DSC resources + implemented as PowerShell classes, including but not limited to: + + - The non-idiomatic status of naming a PowerShell class property with a leading underscore. + - The automatic detection of whether a resource defines canonical properties without inspecting + the resource's JSON Schema. + - Detecting and enforcing correctness for the definition of a canonical property on a PowerShell + class. + + All of these concerns are out of scope for this RFC. + +- DSC expects resources to emit messages to the `stderr` stream as JSON Lines containing an object + with a single key-value pair. The key defines the level of the message, such as `trace` or `debug` + while the value defines the actual message. + + How these messages are emitted from a class and made available to DSC from the PowerShell adapter + is a separate concern from the method signatures for resource operations and enabling resources + implemented as PowerShell classes to participate in the operational semantics of DSC. This concern + is being addressed separately. + +- Static analysis of the PowerShell class to generate a JSON Schema for the DSC resource is out of + scope for this RFC and being addressed separately. + +- Generation of reference documentation from the AST of the PowerShell class is out of scope for + this RFC. Any such proposal: + + 1. Is dependent on the RFC for defining the data model for reference documentation of a resource, + which isn't filed yet. + 1. Would probably depend heavily on enhanced parsing and semantics for comment-based help, which + will itself merit an RFC. + +- Enhanced developer experience features, like a base class, reusable attributes, and generic return + types are all out of scope for this RFC. Any design for those enhancements to the developer + experience necessarily depend on this RFC. + +### Authoring a class-based resource that works for PSDSC v1/v2 and DSC v3 + +> [!NOTE] +> This RFC uses a hypothetical resource named `SoftwarePackage` in example code snippets. The +> resource has three properties: +> +> - `Name` - the name of the software package. +> - `Version` - the semantic version of the software package. +> - `Exist` - whether the software package should be installed. +> +> While it is more typical for PSDSC resources to use the `Ensure` property as a convention, the +> design for migrating PSDSC resources from defining `Ensure` to using the `_exist` canonical +> property is out of scope. For the purposes of this RFC, the `SoftwarePackage` PSDSC resource +> was already using the `Exist` boolean property so we can ellide that conversation, deferring it +> to a future RFC about canonical properties for class-based resources. +> +> In the example snippets for this RFC, the implementation for the methods is either elided or +> has the method simply wrapping a function call. This is both for brevity and because the RFC +> authors expect this to be the primary way that PowerShell developers implement class-based +> resources based on experience in the community. This is **not** a requirement for the method +> signatures. Static analysis of a PowerShell class can inspect the signatures without having any +> opinion about the implementation details _within_ those methods. + +A resource author today might have: + +```powershell +[DscResource()] +class SoftwarePackage { + [DscProperty(Key)] + [string] $Name + + [DscProperty()] + [string] $Version + + [DscProperty()] + [bool] $Exist = $true + + [SoftwarePackage] Get() { + # Elided implementation + } + [void] Set() { + # Elided implementation + } + [bool] Test() { + # Elided implementation + } +} +``` + +With this RFC, the author can introduce: + +```powershell +[DscResource()] +class SoftwarePackage { + # DSC methods + static [string] InstanceJsonSchema() { + return @{ + type = 'object' + required = @('name') + properties = @{ + name = @{ type = 'string' } + version = @{ type = 'string' } + _exist = @{ '$ref' = 'https://aka.ms/dsc/schemas/v3/resource/properties/exist.json' } + } + } + } + + static [System.Tuple[bool, SoftwarePackage, String[]]] Test( + [SoftwarePackage]$instance + ) { + return Test-SoftwarePackageResource -Instance $instance + } + + static [System.Tuple[SoftwarePackage, String[]]] Set( + [SoftwarePackage]$instance + ) { + Set-SoftwarePackageResource -Instance $instance + } + + static [SoftwarePackage] Get( + [SoftwarePackage]$instance + ) { + return Get-SoftwarePackageResource -Instance $instance + } + + static [void] Delete( + [SoftwarePackage]$instance + ) { + Remove-SoftwarePackageResource -Instance $instance + } + + static [SoftwarePackage[]] Export( + [SoftwarePackage]$filteringInstance + ) { + return Export-SoftwarePackageResource + } + + static [SoftwarePackage[]] Export( + [SoftwarePackage]$filteringInstance + ) { + if ($null -eq $filteringInstance) { + throw 'Invalid operation' + } + + return Export-SoftwarePackageResource -FilteringInstance $filteringInstance + } + + # PSDSC methods + [SoftwarePackage] Get() { + return Get-SoftwarePackageResource -Instance $this + } + + [bool] Test() { + return Test-SoftwarePackageResource -Instance $this | + Select-Object -ExpandProperty Item1 + } + + [void] Set() { + Set-SoftwarePackageResource -Instance $this + } +} +``` + +Key changes: + +- All DSC v3-relevant methods are **static** and _optional_. If the class doesn't implement a + static method for an operation, DSC can use the PSDSC instance method for that operation. +- A single class supports **both PSDSC and DSC v3**. +- Authors may optionally return richer structured data for DSC v3. +- No mandatory dependency on Microsoft-owned types. + +### Using the resource in DSC v3 + +Example configuration: + +```yaml +resources: +- type: Contoso.DSC/SoftwarePackage + name: InstallGit + properties: + Name: git +``` + +DSC v3 (via the PSDSC adapter): + +- Validates JSON against the schema returned by the `InstanceJsonSchema()` method. +- Calls `Test`, `Get`, `Set` with class-based resource instances. +- Accepts both simple and structured returns. +- Emits structured messages and differences. + +Resource consumers see **consistent behavior**, regardless of whether the implementation is native +DSC or an adapted PSDSC class-based resource. + +## Specification + +### Resource class shape + +A PowerShell class-based resource compatible with DSC _must_ define a PowerShell class that is also +a valid PSDSC resource. This requires the class to: + +- Have the `[DscResource()]` attribute +- Define at least one property with the `[DscProperty(Key)]` attribute. +- Define the PSDSC instance methods `Get`, `Set`, and `Test` with the corect signatures. + +To enhance the class for participating in the semantics of DSC, the author may also implement +static methods on the class that adhere to the signatures and contracts defined in this RFC. + +Class skeleton (methods only): + +```powershell +[DscResource()] +class { + # PSDSC (instance) methods + [] Get() {} + [bool] Test() {} + [void] Set() {} + # DSC (static) methods + static [] Get([]$instance) {} + static [] Set([]$instance) {} + static [] Test([]$instance) {} + static [[]] Export() + static [[]] Export([]$filteringInstance) {} + static [void] Delete(...) + static [] InstanceSchema(...) +} +``` + +> ![NOTE] +> With the exception of ``, all types with the wrapping angle brackets, like +> `[]` are placeholders that are defined later in this RFC. +> +> `` always refers to the name of the implementing PowerShell class, like +> `ExampleResource` in the following snippet: +> +> ```powershell +> class ExampleResource { +> static [ExampleResource] Get([ExampleResource]$instance) {} +> } +> ``` + +> [!IMPORTANT] +> _Every_ static method signature is optional. When the class doesn't define a given static method, +> the PowerShell adapter for DSC handles the resource like a classic PSDSC resource. + +### DSC operation method selection + +If a class has the `[DscResource()]` attribute, DSC and the adapter know that the resource class +implements the traditional PSDSC resource methods `Get()`, `Set()`, and `Test()`. + +When selecting the method to use for an operation, the adapter: + +1. Checks for the existance of a DSC static method for that operation. +1. If the resource class implements a static method for the operation, DSC invokes that method. +1. If the class doesn't implement a static method for the operation and the operation is part of + the PSDSC resource API, DSC uses the appropriate PSDSC instance method. +1. If the class doesn't implement a static method or instance method for the operation, the + resource can't be used for that operation and DSC raises an error. + +> [!NOTE] In this model, we _can_ support classes defined for DSC that don't have the +> `[DscResource()]` attribute and thus may not have the PSDSC instance methods. Supporting these +> classes is out of scope for this RFC and should be addressed in a future RFC. + +### Method signatures + +This RFC proposes the following new method signatures. Each section defines method signatures for a +different DSC resource operation. In this proposal, we use the `Tuple` type for structured return +data. This enables static analysis and implementation without any dependencies for defined types. + +Future RFCs may: + +- Introduce strongly-typed result classes (e.g., `DscTestResult`) that map to the same shape. +- Define a shared types module that authors can optionally reference. + +#### Get operation method + +Signature: + +```pwsh +static [] Get([]$instance) +``` + +The `get` operation must always return the actual state of the instance with all discoverable +properties populated. There are no meaningful differences in the semantics for the `get` operation +between DSC and PSDSC. + +#### Set operation method + +##### DSC Context for set operations + +There are meaningful differences between the semantics of the `set` operation between DSC and +PSDSC: + +1. For PSDSC, resources aren't expected to return any data. In contrast, the result for a `set` + operation against a specific resource instance in DSC takes the following shape: + + ```yaml + beforeState + name: foo + version: 1.2.3 + _exist: true + afterState + name: foo + version: 1.3.0 + _exist: true + changedProperties: + - version + ``` + +1. In DSC, a non-adapted resource manifest defines the following fields that influence how and + when DSC invokes the operation for the resource and what DSC expects the resource to return: + + - The `set.return` field defines the output DSC should expect for the resource. When the + manifest omits this field, DSC synthesizes the result object by invoking the `get` operation + for the resource after the `set` operation finishes. + + - The `set.implementsPreTest` field indicates whether DSC should invoke the `test` operation to + determine whether to invoke the `set` operation. + + When the manifest omits this field or defines it as `false`, DSC invokes `test` for the + resource. DSC only invokes the `set` operation for the resource when the test result indicates + that the resource isn't in the desired state. + + When the manifest defines this field as `true`, DSC invokes the `set` operation for the + resource without invoking `test` first. Functionally, this manifest field explicitly indicates + that the resource's `Set` operation is idempotent. + + - The `set.handlesExist` field indicates whether DSC can invoke the `set` operation to delete + an instance of the resource. This only applies to resources that have the `_exist` canonical + property and define the `delete` operation. + + If the manifest doesn't define this field or defines it as `false` and _does_ define the + `delete` method, DSC invokes the `delete` method whenever a user invokes the `set` operation + for the resource with the `_exist` property defined as `false` in the desired state. + + If the manifest defines this field as `true`, DSC invokes the `set` operation for the resource + when the caller invokes the set operation, like with the `dsc resource set` or + `dsc config set` commands. It does so even when the `_exist` property is defined as `false` in + the desired state. + + This setting does _not_ affect whether DSC invokes the `set` operation when the caller + explicitly invokes the delete operation, like with the `dsc resource delete` command. + +1. DSC supports invoking the `set` operation for a resource in `whatIf` mode. By default, DSC is + able to synthesize how a resource will change the system by invoking the `test` operation for + the resource and converting the result to show how the `set` operation will modify the system. + + However, a synthetic what-if is not always sufficient for showing how a resource will modify the + system when the `set` operation is invoked. + + Consider an example where a package resource supports defining a version range, not just + specific versions. If the version range specifies that the resource must be between versions + `1.2.0` and `1.5.0`, the test operation will report that the resource is in the desired state + when the current version is `1.2.3`. Depending on the implementation of the resource, invoking + the `set` operation may upgrade the package to version `1.3.0`. To better show the difference, + compare the following result snippets between the synthesized changes and the actual changes: + + ```yaml + # synthetic what-if result + beforeState: + name: foo + version: 1.2.3 + _exist: true + afterState: + name: foo + version: 1.2.3 + _exist: true + changedProperties: [] + --- + # Implemented what-if result + beforeState: + name: foo + version: 1.2.3 + _exist: true + afterState: + name: foo + version: 1.3.0 + _exist: true + changedProperties: + - version + ``` + + To support resources that need to implement their own logic to correctly indicate how the + resource will modify system state, resource authors can define the `whatIf` field in the + resource manifest. Then, if a user invokes the `set` operation for the resource in `whatIf` + mode, DSC invokes the resource using that information instead of the actual `set` definition. + In all respects except for the `command` and `args` sub-fields, DSC requires the `set` and + `whatIf` sections of the manifest to be identical. + +For the purposes of this RFC: + +1. The return data and inferring the effective value of the `set.return` manifest field can be + accomplished with a normal class method signature. Supporting the definition of resources to + match those manifest values with different method signatures is in scope. +1. Support for explicitly implemented `whatIf` mode can also be inferred from a normal class method + signature and is in scope. +1. Instead of further complicating the class with additional methods, this RFC sets the contract + for the `set.implementsPretest` field to be `false`. DSC (and the adapter) will always invoke + the `test` method to determine whether to actually invoke the `set` method. For more information + about this decision, see + [Alternate proposals and considerations](#alternate-proposals-and-considerations). +1. Instead of further complicating the class with additional methods, this RFC sets the contract + for the `set.handlesExist` field to be `true`. This is because the PSDSC `Set()` instance method + contract already requires PSDSC resources to handle creating, updating, and deleting instances + that can be created and deleted. For more information about this decision, see + [Alternate proposals and considerations](#alternate-proposals-and-considerations). + +##### Proposed signatures for set + +- No return data (DSC invokes `get` after `set` to generate after state and changed properties): + + ```pwsh + static [void] Set([]$instance) + ``` + + This maps to a non-adapted DSC resource that omits the `set.returns` field in its manifest. + +- Return state only (DSC generates the changed properties arrray): + + ```pwsh + static [] Set([]$instance) + ``` + + This maps to a non-adapted DSC resource that defines the `set.returns` field in its manifest as + `state`. + +- Return state and changed properties (DSC uses the result without processing): + + ```pwsh + static [System.Tuple[, String[]]] Set([]$instance) + ``` + + This maps to a non-adapted DSC resource that defines the `set.returns` field in its manifest as + `stateAndDiff`. + +- To indicate that the resource supports `whatIf` mode operations as well as `actual`, the class + should define a method signature that expects a boolean parameter after the instance parameter: + + ```pwsh + # No return data + static [void] Set([]$instance, [bool]$whatIf) + # state return kind + static [] Set([]$instance, [bool]$whatIf) + # stateAndDiff return kind + static [System.Tuple[, String[]]] Set([]$instance, [bool]$whatIf) + ``` + +The `set` operation may use one of three return types: + +- The `[void]` return type maps to the same behavior and handling as the PSDSC `Set()` instance + method and a command resource that omits the `set.return` field in its manifest. + + In this case, the DSC engine invokes the `get` operation for the resource after `set` completes + to construct the `afterState` and `changedProperties` fields of the set result. + +- The `[]` return type maps to the DSC `state` value for the `set.return` manifest + field of a command resource. + + In this case, the DSC engine populates the `afterState` field of the set result with this return + data. It compares the `beforeState` and `afterState` fields property-by-property, performing a + strict equivalence check (case sensitively). Any properties with non-equal values in the + `beforeState` and `afterState` fields are inserted into the array for the `changedProperties` + field of the result. + +- The `[System.Tuple[, String[]]]` return type maps to the DSC `stateAndDiff` value + for the `set.return` manifest field of a command resource. + + In this case, the DSC engine populates the `afterState` and `changedProperties` fields of the set + result with the return data. DSC doesn't munge the output. + +> [!NOTE] +> In all cases, DSC _always_ validates any returned state data against the defined JSON Schema for +> the resource instance. This is intended to ensure that resources are matching their own JSON +> Schema in their return data and helps resource authors catch mistakes early in their development +> lifecycle, since acceptance tests will immediately catch this kind of violation. + +The `set` operation may support `whatIf` mode invocations. In this mode, the resource doesn't +change the system. Instead, it reports _how_ it would modify the system. The return data for this +operation is the _expected_ final state and changed properties. The return type for the what-if +method _must_ be the same as any defined `set` method signature without the `[bool]$whatIf` +parameter, such as: + +```pwsh +static [System.Tuple[SoftwarePackage, String[]]] Set( + [SoftwarePackage]$instance, + [bool]$whatIf +) { + # Implementation +} +static [System.Tuple[SoftwarePackage, String[]]] Set( + [SoftwarePackage]$instance +) { + [SoftwarePackage]::Set($instance, $false) +} +``` + +If the two methods have different return values, static analysis should consider the implementation +to be invalid. For example, the following implementation should cause static analysis to identify +the resource implementation as invalid: + +```pwsh +static [SoftwarePackage] Set( + [SoftwarePackage]$instance, + [bool]$whatIf +) { + # Implementation +} + +static [System.Tuple[SoftwarePackage, String[]]] Set( + [SoftwarePackage]$instance +) {} +``` + +#### Test operation method + +##### DSC Context for test operations + +There are meaningful differences between the semantics of the `set` operation between DSC and +PSDSC: + +1. For PSDSC, resources are expected to return a simple boolean result indicating whether the + resource is in the desired state. For DSC, the result for a `test` operation against a specific + resource instance takes the following shape: + + ```yaml + desiredState + name: foo + version: '[1.3.0,2.0.0)' # Nuget version requirement + _exist: true + actualState + name: foo + version: 1.2.3 # actual semantic version + _exist: true + inDesiredState: false + differingProperties: + - version + ``` + +1. In DSC, implementing the `test` operation is entirely optional, while it's mandatory for PSDSC. + + When a DSC resource doesn't implement the test operation (as indicated by not defining the + `test` section in the resource manifest), DSC performs a synthetic test for the resource + instance by invoking the `get` operation to populate the `actualState` field for the result. DSC + then compares every property defined in the `desiredState` against that same property in + `actualState`. The comparison is a strict equivalence check (case-sensitive, order insensitive). + Any properties with differing values between `desiredState` and `actualState` are inserted into + the `differingProperties` array field of the result. + + For non-adapted resources that implement the `test` operation, DSC _requires_ the resource to + define the `_inDesiredState` canonical resource property and return a boolean value for that + property in the return data for the `test` operation. DSC hoists that return value into the + `inDesiredState` field of the result object. + + If the non-adapted resource defines the `test.return` field of its manifest as `stateAndDiff`, + DSC expects the resource to also return the array of property names that aren't in the desired + state as a JSON Line after the JSON Line containing the actual state of the resource (which must + include the `_inDesiredState` canonical property). + +For the purposes of this RFC: + +- The `test` operation is mandatory, since any class with the `[DscResource()]` attribute _must_ + implement the `Test()` instance method. Defining one of the proposed static method signatures + simply enables the resource to provide better information to users as enabled by the semantics of + DSC. + +- The contract of the adapter can obviate the need to define an `_inDesiredState` class property + and include it in the resource instance JSON Schema. This RFC proposes signatures that send the + boolean value as part of the return data to the adapter to avoid defining an awkward class + property to simplify the resource's configurable API. This is particularly relevant when + considering the probability that resource authors developing classes for DSC resources will want + their resources to function idiomatically with both DSC and PSDSC. + + The adapter can correctly handle any required munging when returning the JSON for the `test` + operation to DSC. Handling the existence of the `_inDesiredState` canonical property in terms of + JSON Schema should be deferred to a future RFC about handling canonical properties for + class-based resources. + +##### Proposed signatures for test + +- Return state only (DSC generates the differing properties array): + + ```pwsh + static [System.Tuple[bool, ]] Test([] $instance) + ``` + + This maps to a non-adapted DSC resource that omits the `test.return` field in its manifest or + defines it as `state`. + +- Return state and differing properties (DSC uses the result without processing): + + ```pwsh + static [System.Tuple[bool, , String[]]] Test([] $instance) + ``` + + This maps to a non-adapted DSC resource that defines the `test.return` field in its manifest as + `stateAndDiff`. + +The `test` operation may use one of two return types: + +- The `[System.Tuple[bool, ]]` return type maps to the DSC `state` value of the + `test.return` field in the manifest of a non-adapted resource. Instead of requiring the class to + define the `InDesiredState` read-only property, DSC expects the resource to return the boolean + value _and_ the actual state of the resource. The adapter munges the result for DSC. + + When a resource uses the `state` return kind, DSC populates the `actualState` field of the result + object with the returned data. DSC then compares every property defined in the `desiredState` + against that same property in `actualState`. The comparison is a strict equivalence check + (case-sensitive, order insensitive). Any properties with differing values between `desiredState` + and `actualState` are inserted into the `differingProperties` array field of the result. + +- The `[System.Tuple[bool, , String[]]]` return type maps to the DSC `stateAndDiff` + return kind for a command resource. + + When a resource uses the `stateAndDiff` return kind, DSC populates the `actualState` field of the + result with the returned data. DSC populates the `differingProperties` field of the result with + the returned array. + +#### Export operation method + +##### DSC Context for export operations + +PSDSC has no API or semantics for discovering and returning every instance of a resource on a +system. DSC introduces the `export` operation to enable users and higher order tools to query for +every instance of a resource with a single command. DSC also supports filtered export operations, +where the user passes a filtering instance to limit the return data for the operation. + +Regardless of whether the results are filtered, DSC expects the resource to emit a JSON Line +containing the actual state of the discovered instances. + +For the purposes of this RFC: + +- Currently, DSC doesn't distinguish between a JSON Schema that validates the properties of an + actual resource instance from the JSON Schema validating a filter for export. This is being + addressed separately and is out of scope for this RFC. +- Class-based resources should be able to support filtered export and/or unfiltered export. For + some resources, _only_ filtered exports make sense, like a resource that manages files, to avoid + accidentally enumerating every file on the system. For other resources, filtered exports may not + make sense, such as a resource for managing the system timezone which will only ever return a + single instance. + + Choosing whether to support filtered and/or unfiltered exports should be up to the resource + author and discoverable from the method signatures on the PowerShell class. + +##### Proposed signatures for export + +- Non-filtering export (resource returns every discovered instance): + + ```pwsh + static [[]] Export() + ``` + +- Filtered export (resource uses the input instance to limit the return data): + + ```pwsh + static [[]] Export([]$filteringInstance) + ``` + +The return type for the `export` operation is always an array of instances of the resource class. + +The export functionality depends on which method signatures are implemented: + +- If the class implements both signatures, it supports filtered and unfiltered exports. +- If the class implements only the parameterless signature, it doesn't support filtered exports. +- If the class implements only the signature with a filtering instance, it doesn't support + unfiltered exports. +- If the class doesn't implement either signature, it doesn't support the `export` operation. + +#### Delete operation method + +##### DSC context for delete operations + +PSDSC has no explicit API or semantics for deleting an instance of a resource on a system. DSC +introduces the `delete` operation to simplify: + +1. Removing resource instances for users, who can use the `dsc resource delete` command without + having to explicitly specify `_exist` as `false` in the input JSON, which they would have to + do for the `set` operation. + +1. Implementing resources for authors, who can define implement the `set` operation to create and + update instances of the resources and only handle deleting instances in the `delete` operation, + simplifying the code logic for `set`. + +The `delete` operation is only intended for resources that define the `_exist` canonical property. +As noted in [DSC Context for set operations](#dsc-context-for-set-operations), DSC resource authors +have some choice in whether the `set` operation must handle deleting instances. + +For the purposes of this RFC: + +- Implementing the static `Delete()` method is entirely optional. DSC will only ever invoke this + method when a user specifically invokes the `delete` operation, as with the `dsc resource delete` + command. Even if the resource defines the `_exist` canonical property and the `Delete()` method, + DSC will still invoke the `Set()` method for all `set()` operations. + +##### Proposed signatures for delete + +```pwsh +static [void] Delete([]$instance) +``` + +In the current data model for DSC, the `delete` method returns no data. Only messages and execution +status (success or failure) are reported back to the engine. While there are ongoing discussions +about extending the data model to support optionally returning data from the `delete` operation, +those concerns are out of scope for this RFC. + +If the data model is updated in the future then this RFC should be similarly amended to clarify +how a class-based resource can participate in those semantics by defining additional method +signatures. + +#### InstanceSchema method + +##### DSC context for JSON Schemas + +In PSDSC, the schema for the properties a user could specify to configure a resource was defined +either by a MOF file file or by the properties of the PowerShell class with the `[DscProperty()]` +attribute. + +In DSC, the schema is defined as a JSON Schema either emitted from a command or embedded in the +resource manifest, with embedded schemas being the preferred option. + +There are several other meaningful differences between what could be expressed in the schema for +a PSDSC resource and a DSC resource: + +- PSDSC resource properties could be annotated as being: + + - _Key_ properties, which uniquely identify an instance of the resource together. This prevented + PSDSC configuration authors from defining conflicting instances. + + DSC does not yet have a way to represent key properties in the JSON Schema. This is planned + work. + - _Mandatory_ properties, which the resource always requires the user to explicitly define. + + In JSON Schema and DSC, this is represented with the `required` keyword, which takes an array of + property names that are mandatory. + - _NotConfigurable_ properties, which the resource could return but the user couldn't enforce. An + example of such a property is the `LastWriteTime` for a file. + + In DSC, properties that define the `readOnly` JSON Schema keyword have the same behavior. + +Additionally, DSC supports defining the JSON Schema for a property with the `writeOnly` keyword, +which indicates that a user can send the property to the resource but the resource will never +return it. This is used conventionally in DSC resources for passing secrets to a resource and for +defining directives that change the behavior of a resource but can't be represented in the actual +state of a resource. An example of this sort of property is the `_purge` canonical property. + +For the purposes of this RFC: + +- How the adapter can infer a JSON Schema from the class definition is explicitly out of scope. + Currently, the PowerShell adapter for PSDSC resources skips validation. This is an ongoing design + concern that is being addressed separately. +- Programmatically managing the differences in what can be represented in the PSDSC schema and the + DSC JSON Schema is out of scope. If a class-based resource provides a JSON Schema to DSC, DSC + will use that to validate the data that a user supplies. +- Defining a different JSON Schema to validate filtering instances of the resource for the `export` + operation is out of scope. The potential conflict for a strictly defined JSON Schema that + validates a configurable instance of the resource and a more lax JSON Schema that validates a + filtering instance is a known design problem that is being addressed separately. + + The only concession to this known problem that the RFC makes is to use a more specific name for + the proposed method than simply `Schema`. + +##### Proposed signatures for emitting a JSON Schema + +Signatures: + +```pwsh +static [string] InstanceJsonSchema() +``` + +DSC expects the resource to return a string representation of the resource instance JSON Schema. +The output must validate against the resource instance meta schema at the following JSON pointer +URI: + +```text +https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/manifest.schema.json#/properties/embedded +``` + +In the current implementation of DSC, no pre-validation is performed for the resource properties +of adapted PSDSC resources. If the class implements this method, DSC and the adapter will validate +the user-supplied data with the JSON Schema emitted by the method. + +As noted above, generating a JSON Schema from the properties of the class is out of scope for this +RFC. + +While programmatically reconciling the differences in what can be represented in JSON Schema and +how PSDSC interprets class properties with the `[DscProperty()]` is out of scope for this PR, here +we make a few recommendations to resource authors: + +1. Annotate every property relevant to using the class as a DSC resource with the + `[DscProperty()]` attribute as normal. _Do not_ define properties in the JSON Schema that + aren't annotated with the attribute. + +1. When defining write-only properties, which are only supported by DSC (not PSDSC): + + - Clearly document the behavior of those properties for PSDSC users. Ensure that PSDSC users + understand that they can define a value for those properties but will _never_ get a non-null + value for them from the `Get()` method. + + - If the property type is a value type, like `[bool]` or `[int]`, define the type as nullable + (`[System.Nullable[bool]]` or `[System.Nullable[int]]`) + + - Always explicitly set the value for write-only properties to `$null` on the instance that you + are returning for a DSC or PSDSC operation. + +1. Define the property names in Pascal case as normal, like `[string] $RequiredVersion` and not + `[string] $requiredVersion`. + + The casing that you define in the JSON Schema will be the casing that DSC expects and uses to + validate input, but neither the class itself nor PSDSC care about casing. +1. When defining a canonical property, define the property name in PowerShell _with_ the leading + underscore and correct casing, like `[bool] $_exist = $true` instead of `[bool] $Exist = $true`. + + While a future RFC (or enhancement to this RFC) may provide better programmatic translation of + the names for canonical properties, this will make the class immediately usable with the engine + semantics for canonical properties. + +### Adapter considerations + +This RFC assumes a PSDSC-aware adapter for DSC that: + +- Can load class-based PSDSC resources and recognize the proposed contract. +- May be shipped: + - As part of the PSDSC module, or + - As a separate adapter module with a dependency on PSDSC. + +The adapter will also be responsible for the following behaviors: + +1. Inserting the `_inDesiredState` canonical property into the emitted JSON Schema. This property + isn't represented in the PowerShell class as a property and is _built into_ the contract for + class-based resources. +1. Converting the return data for resource operations into JSON Lines for DSC. +1. Indicating the resource capabilities to DSC based on static analysis. +1. Capturing PowerShell stream messages from a resource and emitting them as the correctly + structured JSON Line to stderr. This is already being addressed separately. + +> OPEN: +> +> - Final shipping model (in-module vs separate module) and versioning strategy. - How to clearly +> communicate to users what changed when PSDSC or the adapter is updated. + +## Detailed examples + +The following snippet defines a resource adhering to this contract. Each of the following examples +shows how the resource itself behaves and how DSC leverages the contract with the resource through +the adapter. + +```powershell +[DscResource()] +class SoftwarePackage { + #region Properties + [DscProperty(Key)] + [string] $Name + + [DscProperty()] + [string] $Version + + [DscProperty()] + [bool] $_exist = $true + #endregion Properties + #region DSC methods + static [string] InstanceJsonSchema() { + return @{ + type = 'object' + required = @('name') + properties = @{ + name = @{ type = 'string' } + version = @{ type = 'string' } + _exist = @{ '$ref' = 'https://aka.ms/dsc/schemas/v3/resource/properties/exist.json' } + } + } + } + + static [System.Tuple[bool, SoftwarePackage, String[]]] Test( + [SoftwarePackage]$instance + ) { + return Test-SoftwarePackageResource -Instance $instance + } + + static [System.Tuple[SoftwarePackage, String[]]] Set( + [SoftwarePackage]$instance + ) { + Set-SoftwarePackageResource -Instance $instance + } + + static [SoftwarePackage] Get( + [SoftwarePackage]$instance + ) { + return Get-SoftwarePackageResource -Instance $instance + } + + static [void] Delete( + [SoftwarePackage]$instance + ) { + Remove-SoftwarePackageResource -Instance $instance + } + + static [SoftwarePackage[]] Export( + [SoftwarePackage]$filteringInstance + ) { + return Export-SoftwarePackageResource + } + + static [SoftwarePackage[]] Export( + [SoftwarePackage]$filteringInstance + ) { + if ($null -eq $filteringInstance) { + throw 'Invalid operation' + } + + return Export-SoftwarePackageResource -FilteringInstance $filteringInstance + } + #endregion DSC methods + #region PSDSC methods + [SoftwarePackage] Get() { + return Get-SoftwarePackageResource -Instance $this + } + + [bool] Test() { + return Test-SoftwarePackageResource -Instance $this | + Select-Object -ExpandProperty Item1 + } + + [void] Set() { + Set-SoftwarePackageResource -Instance $this + } + #endregion PSDSC methods +} +``` + +> [!NOTE] +> The snippet doesn't define the functions these methods call. The output in the examples is +> effectively mocking the behavior of those functions to limit the cognitive load for reviewing +> the examples in this RFC. + +### Example: Validating input + +todo + +### Example: Getting current state + +todo + +### Example: Testing desired state + +todo + +### Example: Enforcing desired state + +todo + +### Example: Deleting an instance + +todo + +### Example: Exporting instances + +todo + +## Alternate Proposals and Considerations + +### Functions as the primary contract + +An alternative approach was to make top-level functions the DSC v3 contract surface, using the +class only for schema: + +- Pros: + - Familiar for PowerShell users who prefer functions over classes. +- Cons: + - Requires DSC to reason about a more complex combination of functions and classes. + - Static analysis and manifest generation are simpler with everything on the class. + - Harder to express the contract as a single analyzable unit. + +This RFC proposes **static class methods** as the primary contract, with authors free to delegate +to functions internally. + +### Mandatory shared types module + +Another alternative was to require all resources to depend on a shared types module (for result +types, attributes, etc.): + +- Pros: + - Strong typing and IntelliSense for result objects and attributes. + - Clear place to evolve shared patterns. +- Cons: + - Introduces "dependency hell" for resource authors and consumers. + - Complicates versioning and servicing. + - Not necessary for basic functionality; generic structured returns are sufficient. + +This RFC opts for: + +- Generic structured forms (tuples) to represent resource output. + +Defined types can be addressed in the future with a separate RFC. + +### Resource metadata and manifest equivalence + +Omitted from the current iteration of this proposal, but not _explicitly_ out of scope for this RFC: + +- Enabling greater flexibility in defining the resource, such as choosing the equivalent value for + `set.handlesExist`. For the purposes of this RFC, the authors have chosen to define the contract + in a way that requires as little additional work and consideration for resource authors who are + defining PSDSC resources but want to take advantage of DSC semantics. + + Options like `handlesExist` and `implementsPretest` are valid considerations for DSC that are + entirely ignored by PSDSC. Instead of leading resource authors to develop resources that behave + differently in terms of "what happens to the system when I invoke the `set` operation for this + resource" depending on whether the resource is invoked through DSC (and the adapter) or PSDSC, + this RFC prefers consistency for the end user regardless of invoking tool. + + Further, the idiomatic way to represent these options would be with attributes on the static + `Set()` method. Given that this RFC was explicitly intended to avoid requiring resources to be + aware of any new types, no idiomatic proposal could be made. + + In the future, when DSC supports resources implemented as PowerShell classes that are _not_ + compatible with PSDSC, _those_ classes (and the RFC defining their contract) should support the + enhanced flexibility. + +- Defining static metadata for the resource as represented by the `description`, `author`, `tags`, + and other fields in a DSC resource manifest. + + While we _could_ define these as static properties, the static properties for PowerShell classes + are _not_ immutable. If the static property for a class is modified in a runspace it will return + the new value for that property whenever you access that property. In practice, the authors + think that this is a minimal concern, since DSC creates a new PowerShell process whenever it + invokes the adapter, but for test and other integration purposes it can be a problem. + + On the other hand, defining them as static methods that just return a string or array of strings + also seems a little strange. It is more idiomatic to represent this information with attributes + or by parsing comments. However, both of those approaches require much more work than defining + simple method contracts, and so are considered out of scope for this RFC. + +### RDK on the critical path + +The working group explicitly does **not** want the Resource Development Kit (RDK) on the critical +path: + +- RDK should be able to build on this contract once defined. +- The contract and adapter behavior must stand on their own. +- Community and Sampler-based tooling can implement schema/manifest generation independently. + +## Related work items + +- Issue: "Define method signatures for PSDSC resource classes" (link TBD) + + Describes the need to clarify what methods and signatures DSC v3 should look for on class-based + resources. + +- Future (potential separate RFCs): + - PSDSC v2 adapter for DSC v3 (shipping model and behavior). + - JSON schema/manifest specification for DSC v3 resources. + - ScriptAnalyzer rule set for V1/V2/V3 DSC resources, including class-based patterns. diff --git a/rfc/draft/rfcxxxx.md b/rfc/draft/rfcxxxx.md deleted file mode 100644 index d4f0bb623..000000000 --- a/rfc/draft/rfcxxxx.md +++ /dev/null @@ -1,649 +0,0 @@ ---- -RFC: RFCNNNN # WG will set the number after submission -Author: jahelmic # <@GitHubUserName> -Sponsor: michaeltlombardi # <@GitHubUserName> -Status: Draft # -SupercededBy: null # -Version: 1.0 # . -Area: DSC # -CommentsDue: null # ---- - -# Class-based PSDSC resource contract for DSC v3 - -This RFC defines a contract for PowerShell **class-based** PSDSC resources so that a single -implementation can: - -- Continue to work with **PSDSC v1/v2**, and -- Participate fully in **DSC v3 semantics** via the PSDSC adapter, - -without requiring a hard dependency on a Microsoft-shipped "DSC types" or RDK module. - -The RFC focuses on: - -- The **method signatures** and shapes DSC v3 cares about for PowerShell class-based resources. -- The **expected return structures** (results, messages, and streams). -- How this contract **aligns with JSON schema and manifest generation**, so that tooling (RDK, - Sampler, analyzers) can build on it. - -## Motivation - -> As a DSC resource author with existing PSDSC class-based resources, -> I want to augment those resources to participate in DSC v3 semantics, -> so that I can support both PSDSC v1/v2 and DSC v3 without maintaining separate codebases. - -Additional motivations: - -- Many resources already implemented as **class-based PSDSC resources** have active users and are - expensive to rewrite. -- DSC v3 introduces richer semantics and tooling: - - Structured results from `Test`, `Set`, and `Get` (not just a "Reason" string). - - Better "diff" semantics (for latest-version / package-like scenarios). - - JSON-schema-based validation and manifest-driven discovery. -- Resource authors should be able to: - - Incrementally add **V3-only capabilities** (e.g., richer test results, schema) to existing - class-based resources. - - Avoid taking a **mandatory dependency** on a Microsoft-shipped types or RDK module. -- The DSC community (including RDK / Sampler users) needs a **clear, documented contract** so that: - - Static analysis is feasible, - - Schema/manifest generation is consistent, - - ScriptAnalyzer rules can be updated coherently for V1/V2/V3 resources. - -## Proposed experience - -This section describes how the contract feels for a resource author and for DSC v3 consumers. - -### Authoring a class-based resource that works for PSDSC v1/v2 and DSC v3 - -A resource author today might have: - -```powershell -[DscResource()] -class ChocolateyPackage { - [DscProperty(Key)] - [string] $Name - - [DscProperty(Mandatory)] - [ValidateSet('Present', 'Absent')] - [string] $Ensure = 'Present' - - [ChocolateyPackage] Get() { ... } - [void] Set() { ... } - [bool] Test() { ... } -} -``` - -With this RFC, the author can introduce: - -```powershell -class ChocolateyPackage { - static [System.Tuple[bool, ChocolateyPackage, String[]]] Test( - [ChocolateyPackage]$instance - ) { - return Test-ChocolateyPackageResource -Instance $instance - } - - static [System.Tuple[ChocolateyPackage, String[]]] Set( - [ChocolateyPackage]$instance - ) { - Set-ChocolateyPackageResource -Instance $instance - } - - static [ChocolateyPackage] Get( - [ChocolateyPackage]$instance - ) { - return Get-ChocolateyPackageResource -Instance $instance - } - - static [void] Delete( - [ChocolateyPackage]$instance - ) { - Remove-ChocolateyPackageResource -Instance $instance - } - - static [ChocolateyPackage[]] Export( - [ChocolateyPackage]$filteringInstance - ) { - return Export-ChocolateyPackageResource -FilteringInstance $filteringInstance - } -} -``` - -Key changes: - -- All DSC v3-relevant methods are **static** and _optional_. If the class doesn't implement a static - method for an operation, DSC can use the PSDSC instance method for that operation. -- A single class supports **both PSDSC and DSC v3**. -- Authors may optionally return richer structured data for DSC v3. -- No mandatory dependency on Microsoft-owned types. - -### Using the resource in DSC v3 - -Example configuration: - -```yaml -resources: -- type: Contoso.DSC/ChocolateyPackage - name: InstallGit - properties: - Name: git -``` - -DSC v3 (via the PSDSC adapter): - -- Validates JSON against the generated schema. -- Calls `Test`, `Get`, `Set` with class-based resource instances. -- Accepts both simple and structured returns. -- Emits structured messages and differences. - -Resource consumers see **consistent behavior**, regardless of whether the implementation is native -DSC v3 or an adapted PSDSC class-based resource. - -## Specification - -> [!NOTE] -> Some aspects are deliberately scoped as "MVP" so that a pilot implementation (e.g., Chocolatey -> resources) can validate the design. Where details are not finalized, they are explicitly called -> out. - -### Resource class shape - -A DSC v3-compliant class-based resource MUST: - -- Declare a schema class representing the resource instance. -- Use **static methods** for DSC v3 interaction. -- Accept the schema class instance as the parameter to all methods. - -Skeleton: - -```powershell -[DscResource()] -class { - [DscProperty(Key)] - [string] $Name - - [DscProperty(Mandatory)] - [ValidateSet('Present', 'Absent')] - [string] $Ensure = 'Present' - - static [] Get([]$instance) {} - static [] Set([]$instance) {} - static [] Test([]$instance) {} - - # optional - static [] Export(...) - static [void] Delete(...) - static [] Schema(...) -} -``` - -### DSC operation method selection - -If a class has the `[DscResource()]` attribute, DSC and the adapter know that the resource class -implements the traditional PSDSC resource methods `Get()`, `Set()`, and `Test()`. - -When selecting the method to use for an operation, the adapter: - -1. Checks for the existance of a DSC static method for that operation. -1. If the resource class implements a static method for the operation, DSC invokes that method. -1. If the class doesn't implement a static method for the operation and the operation is part of - the PSDSC resource API, DSC uses the appropriate PSDSC instance method. -1. If the class doesn't implement a static method or instance method for the operation, the resource - can't be used for that operation and DSC raises an error. - -> [!NOTE] -> In this model, we _can_ support classes defined for DSC that don't have the `[DscResource()]` -> attribute and thus may not have the PSDSC instance methods. Supporting these classes is out of -> scope for the MVP. - -### Method signatures (MVP) - -For the MVP, this RFC proposes the following new method signatures. Each section defines method -signatures for a different DSC resource operation. In this proposal, we use the `Tuple` type for -structured return data. This enables static analysis and implementation without any dependencies -for defined types. - -Future revisions of this RFC may: - -- Introduce strongly-typed result classes (e.g., `DscTestResult`) that map to the same shape. -- Define a shared types module that authors can optionally reference. - -#### Get operation method - -Signature: - -```pwsh -static [] Get([]$instance) -``` - -The `get` operation must always return the actual state of the instance with all discoverable -properties populated. - -#### Set operation method - -Signatures: - -- No return data (DSC invokes `get` after `set` to generate after state and - changed properties): - - ```pwsh - static [void] Set([]$instance) - ``` - -- Return state only (DSC generates the changed properties arrray): - - ```pwsh - static [] Set([]$instance) - ``` - -- Return state and changed properties (DSC uses the result without processing): - - ```pwsh - static [System.Tuple[, String[]]] Set([]$instance) - ``` - -- To indicate that the resource supports `whatIf` mode operations as well as `actual`, the class - should define a method signature that expects a boolean parameter after the instance parameter: - - ```pwsh - # No return data - static [void] Set([]$instance, [bool]$whatIf) - # state return kind - static [] Set([]$instance, [bool]$whatIf) - # stateAndDiff return kind - static [System.Tuple[, String[]]] Set([]$instance, [bool]$whatIf) - ``` - -The `set` operation may use one of three return types: - -- The `[void]` return type maps to the same behavior and handling as the PSDSC `Set()` instance - method. -- The `[]` return type maps to the DSC `state` return kind for a command resource. -- The `[System.Tuple[, String[]]]` return type maps to the DSC `stateAndDiff` - return kind for a command resource. - -The `set` operation may support `whatIf` mode invocations. In this mode, the resource doesn't change the system. Instead, it reports _how_ it would modify the system. The return data for this -operation is the _expected_ final state and changed properties. The return type for the what-if method _must_ be the same as the actual method signature, such as: - -```pwsh -static [System.Tuple[ChocolateyPackage, String[]] Set( - [ChocolateyPackage]$instance, - [bool]$whatIf -) { - # Implementation -} -static [System.Tuple[ChocolateyPackage, String[]] Set( - [ChocolateyPackage]$instance -) { - [ChocolateyPackage]::Set($instance, $false) -} -``` - -#### Test operation method - -Signatures: - -- Return state only (DSC generates the differing properties array): - - ```pwsh - static [System.Tuple[bool, ]] Test([] $instance) - ``` - -- Return state and differing properties (DSC uses the result without processing): - - ```pwsh - static [System.Tuple[bool, , String[]]] Test([] $instance) - ``` - -The `test` operation may use one of two return types: - -- The `[System.Tuple[bool, ]]` return type maps to the DSC `state` return kind for - a command resource. Instead of requiring the class to define the `InDesiredState` read-only - property, DSC expects the resource to return the boolean value _and_ the actual state of the - resource. The adapter munges the result for DSC. -- The `[System.Tuple[bool, , String[]]]` return type maps to the DSC `stateAndDiff` - return kind for a command resource. - -#### Export operation method - -Signatures: - -- Non-filtering export (resource returns every discovered instance): - - ```pwsh - static [[]] Export() - ``` - -- Filtered export (resource uses the input instance to limit the return data): - - ```pwsh - static [[]] Export([]$filteringInstance) - ``` - -The return type for the `export` operation is always an array of instances of the resource class. - -The export functionality depends on which method signatures are implemented: - -- If the class implements both signatures, it supports filtered and unfiltered exports. -- If the class implements only the parameterless signature, it doesn't support filtered exports. -- If the class implements only the signature with a filtering instance, it doesn't support - unfiltered exports. -- If the class doesn't implement either signature, it doesn't support the `export` operation. - -#### Delete operation method - -Signature: - -```pwsh -static [void] Delete([]$instance) -``` - -In the current data model for DSC, the `delete` method returns no data. Only messages and -execution status (success or failure) are reported back to the engine. - -#### Schema method - -Signatures: - -```pwsh -static [string] Schema() -``` - -DSC expects the resource to return a string representation of the resource instance JSON Schema. -The output must validate against the resource instance meta schema at the following JSON pointer -URI: - -```text -https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/manifest.schema.json#/properties/embedded -``` - -If the resource doesn't implement this method, DSC generates a JSON Schema by inspecting the -resource class itself. - -> [!NOTE] -> Currently, DSC doesn't distinguish between the instance schema and the filtering instance -> schema. Issue #1232 proposes a model for separately validating resource properties for the -> `export` operation. Until that proposal is implemented, the only schema we can emit for a -> resource is the resource instance schema. -> -> In the future, we may add a second method, like `FilteringSchema()`, to account for this. - -### Attributes and metadata - -Some information cannot be derived from method signatures alone. This RFC proposes a small set of -**optional attributes** that can be applied to the class, properties and methods. - -These attributes MAY live in a shared types/module (e.g., RDK or `Microsoft.DSC.Types`). DSC v3 -MUST treat this module as an **optional dependency**: - -- If present, the adapter and tooling can leverage the attributes. -- If absent, equivalent metadata MAY be provided via structured return types or manifest entries. - -> [!NOTE] -> When considering alternatives to attributes, it would be _possible_ to retrieve this information -> with a set of new metadata methods or static properties. We could accept hashtables for those -> methods or properties to minimize the number of items to check, but that would then require -> validating the keys and data types. Each of the following sections has a collapsible details -> block that enumerates the possible metadata static properties, but in general using attributes is -> preferable for this purpose. - -#### `[DscResourceClass()]` attribute - -The `[DscResourceClass()]` attribute annotates the class itself to define metadata about the class: - -- `[DscResourceClass(DscVersion = '')]` - Indicates which version of DSC the resource - was developed with and to use for the `$schema` in the generated manifest. Values must match - the following regex: - - ```regex - ^v(?\d+)\.(?\d+).(?\d+)$ - ``` - - When this option isn't specified, the default value is `v3`. -- `[DscResourceClass(Type = '')]` - Defines the type name for the - resource. Values must be a valid resource type name, like `Contoso.Chocolatey/Package`. When - this option isn't specified, the default is `/`. -- `[DscResourceClass(Version = '')]` - Defines the semantic version for the - resource. Values must be a string that parses as a valid semantic version, like `1.2.3`. When - this option isn't specified, the default is the module version. -- `[DscResourceClass(Description = '')]` - Defines a short description for the - resource, surfaced in the `dsc resource list` command. No default value. -- `[DscResourceClass(Tags = ('', ..., ''))]` - Defines the tags for the resource. - -
Metadata static properties - -```pwsh -# Single method for all data, all fields optional: -static [hashtable] $ResourceMetadata = { - @{ - DscVersion = '' - Type = '' - Version = '' - Description = '' - Tags = @('', ..., '') - } -} -# Individual properties -static [string]$DscVersion = '' -static [string]$Type = '' -static [string]$Version = '' -static [string]$Description = '' -static [string[]]$Tags = @('', ..., '') -``` - -
- -#### `[DscResourceProperty()]` attribute - -The `[DscResourceProperty()]` attribute enables authors to annotate their resource properties with DSC semantics: - -- `[DscResourceProperty(Canonical)]` - indicates that the property is a canonical DSC resource - property. This attribute is only valid on properties that have the same name. For example. the following is valid, annotating the `_exist` canonical property: - - ```pwsh - [DscResourceProperty(Canonical)] - [bool]$Exist = $false - ``` - - And the following snippet would be invalid, because `_ensure` isn't a canonical property: - - ```pwsh - [DscResourceProperty(Canonical)] - [string]$Ensure - ``` - - > [!NOTE] - > Ideally, we would have a way for the resource to use either the shorthand, - > `[DscResourceProperty(Canonical)]` or specify the name of the canonical property to help - > with property name conflicts, especially given the existence of canonical properties like - > `_name`, which may conflict with the ergonomic design of the resource (like a chocolatey - > package name). - > - > That would make the following definitions valid: - > - > ```pwsh - > [DscResourceProperty(Canonical)] - > [bool]$Exist = $false - > - > [DscResourceProperty(Canonical='_name')] - > [string]$InstanceName - > ``` - -- `[DscResourceProperty(ReadOnly)]` - indicates that the property is read-only and can be - returned from the resource but is never used as input _to_ the resource. -- `[DscResourceProperty(WriteOnly)]` - indicates that the property is write-only and can be - passed to the resource as input but is never returned in the output data. -- `[DscResourceProperty(Sensitive)]` - inidcates that the property is sensitive and should be - redacted from messaging and output. Only valid on properties that have a string, object, or - enum type. -- `[DscResourceProperty(Key)]` - indicates that the property uniquely identifies an instance of - the resource. -- `[DscResourceProperty(Required)]` - indicates that the property is mandatory for non-export - operations. - -The `[DscResourceProperty()]` can inherit values when the class defines the `[DscProperty()]` -attribute on the same property: - -- `[DscProperty(Key)]` - maps to `[DscResourceProperty(Key)]`. -- `[DscProperty(Mandatory)]` - maps to `[DscResourceProperty(Required)]`. -- `[DscProperty(NotConfigurable)]` - maps to `[DscResourceProperty(ReadOnly)]`. - -
Metadata static properties - -```pwsh -# Single property for all data, each key a different property, all fields optional: -static [hashtable] $ResourcePropertyMetadata = @{ - @{ - = @{ - ReadOnly = $false - WriteOnly = $false - Sensitive = $false - Key = $false - Required = $false - } - } -} -# Per property metadata, all fields optional: -static [hashtable]$Metadata = @{ - ReadOnly = $false - WriteOnly = $false - Sensitive = $false - Key = $false - Required = $false -} -# Individual properties for each resource property and option: -static [bool]$ReadOnly = $false -static [bool]$WriteOnly = $false -static [bool]$Sensitive = $false -static [bool]$Key = $false -static [bool]$Required = $false -``` - -
- -#### `[DscResourceSet()]` attribute - -The `[DscResourceSet()]` attribute defines handling for the `set` method. Must be attached to a -static set method signature. If the resource defines a signature that indicates support for -`whatIf` mode, the attribute must be on that method. - -- `[DscResourceSet(implementsPretest)]` - Indicates that the resource is implemented to check - whether it needs to change system state before making any changes. This maps to the - `set.implementsPretest` resource manifest field. When this option isn't specified, the - default is `false`. -- `[DscResourceSet(handlesExist)]` - Indicates that the resource directly handles the `_exist` - canonical property. This maps to the `set.handlesExist` resource manifest field. When this - option isn't specified, the default is `false`. - -
Metadata static properties - -```pwsh -# Single property for all data, all fields optional: -static [hashtable] $SetOperation = @{ - @{ - ImplementsPretest = $false - HandlesExist = $false - } -} -# Per option metadata -static [bool] $SetOperationImplementsPretest = $false -static [bool] $SetOperationHandlesExist = $false -``` - -
- -### JSON schema and manifest alignment - -The class contract MUST align with a JSON schema and manifest model: - -- **JSON schema**: - - - Represents properties, types, constraints, and read-only / write-only / sensitive flags. - - Ideally generated at **build time** from the PowerShell class (e.g., using `System.Text.Json`). - - May be embedded in a manifest or file alongside the resource module. - -- **Manifest**: - - - Describes which methods are implemented and what capabilities the resource supports (e.g., - supports `Export`, has rich `Test` results, etc.). - - Enables faster discovery and avoids heavy runtime analysis. - -This RFC does **not** fully define the JSON schema format or manifest schema, but it requires: - -- The class-based contract to expose enough information to: - - - Generate JSON schema with correct property naming and constraints. - - Generate a manifest that allows the adapter to skip expensive reflection where possible. - -> OPEN: -> -> - Naming conventions for JSON properties (camelCase vs PascalCase). -> - How to annotate canonical properties and avoid conflicts with WMI/LCM constraints (e.g., -> `__Name`). -> - Minimum set of fields a manifest must contain to support this contract. - -### PSDSC v2 adapter considerations - -This RFC assumes a PSDSC-based adapter for DSC v3 that: - -- Can load class-based PSDSC resources and recognize the proposed contract. -- May be shipped: - - As part of the PSDSC module, or - - As a separate adapter module with a dependency on PSDSC. - -> OPEN: -> -> - Final shipping model (in-module vs separate module) and versioning strategy. -> - How to clearly communicate to users what changed when PSDSC or the adapter is updated. - -## Alternate Proposals and Considerations - -### Functions as the primary contract - -An alternative approach was to make top-level functions the DSC v3 contract surface, using the -class only for schema: - -- Pros: - - Familiar for PowerShell users who prefer functions over classes. -- Cons: - - Requires DSC to reason about a more complex combination of functions and classes. - - Static analysis and manifest generation are simpler with everything on the class. - - Harder to express the contract as a single analyzable unit. - -This RFC proposes **static class methods** as the primary contract, with authors free to delegate to functions internally. - -### Mandatory shared types module - -Another alternative was to require all resources to depend on a shared types module (for result types, attributes, etc.): - -- Pros: - - Strong typing and IntelliSense for result objects and attributes. - - Clear place to evolve shared patterns. -- Cons: - - Introduces "dependency hell" for resource authors and consumers. - - Complicates versioning and servicing. - - Not necessary for basic functionality; generic structured returns are sufficient. - -This RFC opts for: - -- Generic structured forms (hash tables / objects) as the **baseline**. -- Optional shared types for authors who want richer tooling. - -### RDK on the critical path - -The working group explicitly does **not** want the Resource Development Kit (RDK) on the critical path: - -- RDK should be able to build on this contract once defined. -- The contract and adapter behavior must stand on their own. -- Community and Sampler-based tooling can implement schema/manifest generation independently. - -## Related work items - -- Issue: "Define method signatures for PSDSC resource classes" (link TBD) - - Describes the need to clarify what methods and signatures DSC v3 should look for on class-based resources. - -- Future (potential separate RFCs): - - PSDSC v2 adapter for DSC v3 (shipping model and behavior). - - JSON schema/manifest specification for DSC v3 resources. - - ScriptAnalyzer rule set for V1/V2/V3 DSC resources, including class-based patterns.