Skip to content

Commit da6465d

Browse files
committed
(#3539) Add design documentation for C# cmdlets
Given we use some slightly custom bits in Chocolatey.PowerShell that will alter how contributors are expected to work with the cmdlets in this project compared to how more bare-bones projects handle C# cmdlets, this document outlines some of the more notable changes as well as the common design practices we will be using here.
1 parent 89a5634 commit da6465d

File tree

1 file changed

+100
-0
lines changed

1 file changed

+100
-0
lines changed
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Contributing Guidelines for `Chocolatey.PowerShell`
2+
3+
This document outlines some guidelines and design practices followed in the `Chocolatey.PowerShell` project.
4+
It also highlights any important differences from how cmdlets are implemented here compared to the standard patterns for PowerShell cmdlets.
5+
6+
## Naming conventions
7+
8+
Cmdlet classes should be named `VerbNounCommand` and placed in the `Commands` folder and `Chocolatey.PowerShell.Commands` namespace.
9+
10+
## Inherit from `ChocolateyCmdlet` and **not** `PSCmdlet` or `Cmdlet`
11+
12+
`ChocolateyCmdlet` affords some additional helper methods and establishes patterns which can be easily reused across all Chocolatey cmdlets.
13+
Note that you will still need to apply the standard `[Cmdlet(Verb, Noun)]` attribute on all cmdlet classes for PowerShell to recognise them.
14+
15+
### Overrides
16+
17+
Note that unlike `PSCmdlet`, `ChocolateyCmdlet` requires cmdlets to override the methods `Begin()`, `Process()`, and `End()`. These correspond to `begin {}`, `process {}`, and `end {}` blocks in a PowerShell function.
18+
19+
If unsure, follow these guidelines:
20+
- Commands that do pipeline processing (declaring a parameter with `[Parameter(ValueFromPipeline = true)]`) will need to handle that input in `Process()`
21+
- Commands not using pipeline input often handle the bulk of their processing in `End()`
22+
- If there is any setup that may need to be handled in the command prior to processing any pipeline input, that can go in `Begin()`.
23+
24+
### Logging
25+
26+
Cmdlets inheriting from `ChocolateyCmdlet` will log their parameter values to debug logs when called by default. If any parameters may contain sensitive information, override the `Logging` property and set it to `false` to disable this behaviour.
27+
28+
### Output
29+
30+
By default, `ChocolateyCmdlet`'s `WriteObject()` method will enumerate collections when outputting them, similar to how PowerShell's `Write-Output` works by default. If you need to disable this, use the `WriteObject `
31+
32+
### Helpers
33+
34+
`ChocolateyCmdlet` provides some helper methods for common operations that might be needed for many cmdlets.
35+
Some of these (and many more) are available on the `PSHelper` class.
36+
37+
## Place core logic in helper classes
38+
39+
For more general-purpose PowerShell helpers, add methods to the `PSHelper` so that these can remain in a centralised place.
40+
41+
For other more task-specific helpers:
42+
- If there is a relevant helper class already present in `src/Chocolatey.PowerShell/Helpers`, add any needed methods to it and have the cmdlet call that method.
43+
- If there is not already a relevant helper class, add a new one into this folder (and the `Chocolatey.PowerShell.Helpers` namespace).
44+
45+
Unlike in PowerShell functions, C# cmdlets cannot directly call into each other as easily (there is no supported way to instantiate one cmdlet from another in the C# PowerShell API without starting a new subshell and pipeline, which is excessively expensive).
46+
As such, we need to ensure we leave methods that may need to be shared _not_ on the classes inheriting from `Cmdlet`/`PSCmdlet`/`ChocolateyCmdlet`.
47+
To work around this, the majority of the core logic of a cmdlet should be placed in a helper class, so it can be easily called from other cmdlets (or helper classes) that may need to reuse the logic.
48+
49+
> :info: **Example**
50+
>
51+
> Take the commands `Install-ChocolateyPackage` and `Install-ChocolateyInstallPackage` for example.
52+
> `Install-ChocolateyPackage` needs to call `Install-ChocolateyInstallPackage` after downloading its installation files.
53+
> To facilitate this, we can put the core logic of `Install-ChocolateyInstallPackage` into a helper class, and the actual `InstallChocolateyInstallPackageCommand` class we would write would only define parameters, then call into the helper class.
54+
> Then, when we write the `InstallChocolateyPackageCommand`, it can download files and then call into the same helper class to run the other cmdlet's logic seamlessly.
55+
56+
## Add `ShouldProcess` support where applicable
57+
58+
As a _very_ brief and reductive overview, if a cmdlet is making changes to the user's machine, it should implement `ShouldProcess` support.
59+
This enables the built-in PowerShell functionality for `-WhatIf` and `-Confirm` parameters on the cmdlet.
60+
61+
### Implementing `ShouldProcess`
62+
63+
To implement `ShouldProcess` correctly, the following conditions must be met:
64+
65+
1. The cmdlet class should be decorated with `[Cmdlet(SupportsShouldProcess = true)]` (in other words: add `SupportsShouldProcess = true` to the existing property declarations in the `Cmdlet` attribute)
66+
2. In any logic paths where we might be making changes to the user's machine (installing, uninstalling, modifying the registry, making changes to the user or machine environment variables, creating or deleting non-temporary files, running external applications, and so on) we need to be wrapping those code paths in an `if` check like so:
67+
```csharp
68+
if (ShouldProcess("target item (path, object, env var name, description of what is being actually modified)", "description of the action to be performed"))
69+
{
70+
// Code to run that does the described action.
71+
}
72+
```
73+
74+
> :memo: **Note**
75+
>
76+
> If this check takes place in a helper class rather than the main cmdlet class, remember that the `ShouldProcess` method only exists on the `PSCmdlet` base class.
77+
> To call `ShouldProcess` from a helper class, a `PSCmdlet` parameter can be passed to the method, and cmdlets calling into it can simply pass `this` for that parameter value: `HelperClass.MethodName(this)`
78+
> Then in the helper class, it can be called on the parameter, similar to this: `cmdlet.ShouldProcess(...)`
79+
80+
Ideally these checks should take place at the deepest or most narrow point that is sensible in the code path, bypassing _only_ the code which actually makes changes to the system where possible.
81+
This pattern enables us to use `-WhatIf` to verify code paths when invoked from a cmdlet, without risking making any permanent changes to the testing environment.
82+
83+
For more information on ShouldProcess, see [Everything you wanted to know about ShouldProcess](https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-shouldprocess?view=powershell-7.4).
84+
85+
## No dependency injection
86+
87+
In most areas of the Chocolatey CLI codebase, we use a SimpleInjector framework for dependency injection.
88+
This is not available in the Chocolatey.PowerShell library, as it is intentionally decoupled from the other CLI projects and should never depend on any of the other CLI projects.
89+
90+
Additionally, since all cmdlets are initialised by the PowerShell runtime, we cannot use a dependency injection framework to make things work.
91+
As a result, many of the helpers will be a little more bare-bones and not make as heavy use of interfaces as other code areas in this repository.
92+
93+
## Deprecating old command names
94+
95+
For deprecating an old command name, we have the following process:
96+
1. Add an alias to the `chocolateyInstaller.psm1` file in the `chocolatey.resources` project, pointing to the new command name.
97+
2. Rename the command and its class appropriately (cmdlet classes are named after the command's name, with a `Command` suffix, so `Get-FileHash` would be `GetFileHashCommand`).
98+
3. Add an entry to the `_deprecatedCommandNames` list/dictionary provided on `ChocolateyCmdlet`, listing the old command name and the new.
99+
4. File an issue targeted at the next major version to remove the alias, completing the deprecation cycle.
100+
1. Issues should also be added to the Package Validator and its associated extension repositories to add warning rules for package maintainers to be made aware of the impending removal of the old command name.

0 commit comments

Comments
 (0)