Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Question] Advanced functions | Parameter binding by PSTypeName #107

Open
OCram85 opened this issue Apr 18, 2018 · 14 comments
Open

[Question] Advanced functions | Parameter binding by PSTypeName #107

OCram85 opened this issue Apr 18, 2018 · 14 comments

Comments

@OCram85
Copy link

OCram85 commented Apr 18, 2018

Hi guys,

I recently discovered the possibility to bind a parameter by a custom type name. The common approach seems to be using [PSCustomObject] as parameter type if you don't use classes.

Just for clarify a little example:

function Get-MyAwesomeCustomType {
    [CmdletBinding()]
    [OutputType('ModuleName.Context.Identifier')]
    param()
    $returnObj = [PSCustomObject]@{
        somePropertyKey = 'awesome value'
        foo             = 'bar'
    }
    $returnObj.PsObject.TypeNames.Insert(0, 'ModuleName.Context.Identifier')
    Write-Output $returnObj
}

function Invoke-AwesomeStuff {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [PSTypeName('ModuleName.Context.Identifier')]$InputObj
    )

    Write-Host $InputObj.foo
}

My questions here are:

  • Does this scenario of using PSTypeName for parameter binding respect the best practice and common used style guide?
  • It seems to fill the gap for using the PSCustomObjects and still be able to bind it properly. So why is this so rare to find?
  • Is this a newer feature? Was this introduced in v4 or v5?
@vexx32
Copy link
Contributor

vexx32 commented Apr 20, 2018

Duplicating the relevant part of my post from your thread on the PS forum so I can follow this discussion also. :)

I think that in most cases, it may be a better idea to use a class. However, there are probably cases that make that very difficult or impossible (classes in PS are unfortunately still very limited). In those cases, this would be a boon.

The primary issue with this is that a type name means essentially nothing. It can affect the object's display modes and default display set if you've done that with it, but that's about it.

This isn't really validating anything, honestly. It's not hard to tack on a type name to any custom object, so this doesn't do what input validation is meant to do — avoid having to deal with the messiness that comes with manually validating. You'll still have to verify that the object's you're given have the right properties and so forth.

So... In a pinch, this is quite nifty. But when you have the choice, use a proper class.

@OCram85
Copy link
Author

OCram85 commented Apr 24, 2018

Well I was looking for a way filling gap of using PSCustomObjects with custom TypeNames and using them as function parameter.
I thought this would be very useful to ensure users provide the correct object with ist required properties. I'm fully aware this doesn't check any properties - If this is available in v4 it's still better than just using PSCustomObjects without any validation. - At least in my opinion ^^

@pauby
Copy link
Contributor

pauby commented Apr 24, 2018

I thought this would be very useful to ensure users provide the correct object with ist required properties

@OCram85 that is exactly what I use them for. As has been said it doesn't do any property validation but it does make sure the caller is passing the correct object (which could be the output from another function of yours). So that is a validation in itself.

@OCram85
Copy link
Author

OCram85 commented Apr 27, 2018

@pauby: That's exactly what I wanted to achieve. But I'm really wondering why this is used so rarely and also not documented ^^

@pauby
Copy link
Contributor

pauby commented Apr 27, 2018

Totally agree. I tend to use it for things like configuration data that I need to be sure is in a specific format. But I'm really not sure why it's not used more readily. Its not difficult to do so that's not a stumbling block.

As you said the documentation for it is lacking too. I stumbled across it while looking for something else. Its had me wondering if there isn't some horrible consequences of using it that everybody knows except me!

It might make a good lightning talk. Hmm.

@MartinSGill
Copy link

Wouldn't best practice nowadays be to use a class instead? Seems a lot clearer and simpler to me.

class ContextIdentifier {
    $SomePropertyKey
    $foo
}

function Get-MyAwesomeCustomType {
    [CmdletBinding()]
    [OutputType([ContextIdentifier])]
    param()
    $returnObj = [ContextIdentifier]@{
        SomePropertyKey = 'awesome value'
        Foo             = 'bar'
    }
    Write-Output $returnObj
}

function Invoke-AwesomeStuff {
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [ContextIdentifier]$InputObj
    )

    Write-Host $InputObj.foo
}

@vexx32
Copy link
Contributor

vexx32 commented Sep 21, 2018

Assuming what you need can be crafted in a PS class, sure. Otherwise you may need to mess with inlined C# and Add-Type

For simpler objects, I don't see any particular need for a class. For anything decently complex, or especially if you want to be able to work neatly with multiple types of input (which can be done with well-designed constructor overloads), you'll find it much easier to use a class.

Also note that your hashtable->class object conversion is only possible on an object where the following two conditions are met:

  1. The object has a public, parameterless (default) constructor available.
  2. The properties you're trying to set are public, or have public setters.

Setters are really only implemented in C#, but if you're implementing an interface that works with getters and setters, it's still something you need to be aware of, even in PS classes.

I'm looking at potentially improving that bit of parser magic in PS Core, but I'm not yet sure how it'll work, precisely.

@MartinSGill
Copy link

MartinSGill commented Sep 21, 2018

I felt the main point of the post was type safety for custom objects in a module. The generic PSCustomObject gives you no real safety, hence the workaround with $returnObj.PsObject.TypeNames.Insert(0, 'ModuleName.Context.Identifier') and [PSTypeName('ModuleName.Context.Identifier')]

Anything you can do with PSCustomObject you can do with class, surely; or have I missed a trick somewhere?

For more complex scenarios wouldn't it be better to just write it in C# and compile it?

@vexx32
Copy link
Contributor

vexx32 commented Sep 21, 2018

The difficulty with using a purely custom class is that oftentimes since these are not exported in most cases from a module (the only way I've found to export them from a module by default is having them in a separate .ps1 script that is referenced in ScriptsToProcess in the PS1 file) there is little to no autocomplete or support for building a proper object from outside the module, at the console prompt, in order to use it to its fullest potential.

Unfamiliar types for parameters pose a difficulty in that users may be unsure exactly what kind of input to give -- and answers to that have to be given in documentation on a per-function basis, because there is (as mentioned above) limited support for exploring custom types that a module introduces in and of itself.

@Jaykul
Copy link
Member

Jaykul commented Sep 21, 2018

One specific use case for PSTypeNames is when you're adding things to existing objects.

For example, I was working last night on a function which has two parameter sets. One of them has a -Passthru switch. If you -Passthru, there is an $InputObject which is accepted from the pipeline, and instead of creating a new object, I use Add-Member to add two properties to the existing object (for the curious, the objects are usually ErrorRecords or Breakpoints, or Pester CodeCoverage misses, and I'm adding to them the SourceFile and SourceLineNumber to map from compiled psm1 lines to source ps1 lines).

In the passthru case, I insert a PSTypeName so that the object, in addition to still being whatever it was before, is now also one of my objects.

This cannot be done with a PowerShell custom class, because I don't want to limit what you can pass through, nor encapsulate what you pass through in a container with a OriginalObject property.

We are using the PSTypeName restriction as a weak stand-in for "looks like a duck" here, and it would be better is if I could write something like:

[Parameter()]
[ValidateProperties(@{ SourceFile = [string]; SourceLineNumber = [int]})]

@Jaykul
Copy link
Member

Jaykul commented Sep 21, 2018

P.S. @vexx32 the only correct way to define classes and have them exported correctly (in such a way that they live in your module and are scoped correctly) is to put directly them IN the main PSM1

Then, when you want to use them, write: using module YourModule (instead of Import-Module?)

@vexx32
Copy link
Contributor

vexx32 commented Sep 21, 2018

As you mention, that does not import custom types when you import the module. Granted, you may not always want to, but say for example you have a custom class to simplify parameter validation / conversions. Suddenly there's an arbitrary (hopefully well-named, but we all know that's not that likely) type name when you Get-Help a function, and the discoverability ends there.

If you happen to not know about using module (which isn't that well documented either), you're stuck with trying whatever seems sensible as input until something hopefully works, and you can't instantiate an object of that type to check it out, since it isn't useable outside the module scope.

Custom types are fantastic for parameter validation, but with this barrier to discovery, I'm not a fan of them whatsoever.

@Jaykul
Copy link
Member

Jaykul commented Sep 21, 2018

Yep, I should modify my recommendation, because I mispoke earlier. Clearly complex types aren't the best practice ...

  1. The best practice for parameters is to use simple value types (strings, numbers, boolean switches).
  2. The best practice for pipeline output is to use strongly typed objects defined with either .Net or PowerShell Classes
  3. The next best thing for pipeline output is to always inject a module-qualified PSTypeName into any custom PSObjects (this is necessary for formatting rules as well as pipelining).

If you need a lot of parameters and can easily get their values from another command, you will want to pipeline the object. In this case, the best way is clearly to ensure it's properties are simple value types, and then accept those simple values as parameters, using ValueFromPipelineByPropertyName to allow piping the object in to bind all the parameters at once.

However, it is obviously acceptable to use custom types as a parameter. In particular, it's common to do the many parameters thing I mentioned and also have an InputObject which accepts the rich object (for when you're not able to pipe it directly). It's also common to use custom type parameters for heavy objects, like and object that's holding an open network connection, or for objects that represent the result of a lot of work.

In those cases, the best practice is going to be to accept strongly typed objects defined with either .Net or PowerShell Classes, and the next best would be to use a PSTypeName restriction.

As additional suggestions:

- Avoid using custom types unless there is an obvious function for getting them
- Avoid using custom types as (mandatory) parameters or as the only way to pass information. This complicates learning to use your commands, and complicates your examples and help

I'm sure I'm missing a few restrictions, and obviously there are always exceptions to the rules ...

@vexx32
Copy link
Contributor

vexx32 commented Sep 21, 2018

Fully agree with that recommendation. Wanna copy-paste that into the relevant section in the guide? 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants