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

ValidateSet with class does not work #19676

Closed
5 tasks done
MarcelVersteeg opened this issue May 18, 2023 · 14 comments
Closed
5 tasks done

ValidateSet with class does not work #19676

MarcelVersteeg opened this issue May 18, 2023 · 14 comments
Labels
Needs-Triage The issue is new and needs to be triaged by a work group. Resolution-No Activity Issue has had no activity for 6 months or more

Comments

@MarcelVersteeg
Copy link

Prerequisites

Steps to reproduce

Create two files in a directory.

test.ps1

using module './testvalidate.psm1'

param
(
    [Parameter(Mandatory)]
    [ValidateSet([TestValidate])]
    [string]$foo
)

testvalidate.psm1

class TestValidate : System.Management.Automation.IValidateSetValuesGenerator
{
    [String[]] GetValidValues()
    {
        $values = @()
        $values += 'bar'
        $values += 'baz'
        return $values
    }
}

Expected behavior

cmdlet test.ps1 at command pipeline position 1
Supply values for the following parameters:
foo:

Actual behavior

InvalidOperation: E:\Software development\Royalty Calculator\trunk.powershell\test.ps1:6
Line |
   6 |      [ValidateSet([TestValidate])]
     |      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Unable to find type [TestValidate].

Error details

Exception             :
    Type        : System.Management.Automation.RuntimeException
    ErrorRecord :
        Exception             :
            Type    : System.Management.Automation.ParentContainsErrorRecordException
            Message : Unable to find type [TestValidate].
            HResult : -2146233087
        TargetObject          : [ValidateSet([TestValidate])]
        CategoryInfo          : InvalidOperation: ([ValidateSet([TestValidate])]:AttributeAst) [], ParentContainsErrorRecordException
        FullyQualifiedErrorId : TypeNotFound
        InvocationInfo        :
            ScriptLineNumber : 6
            OffsetInLine     : 5
            HistoryId        : -1
            ScriptName       : E:\Software development\Royalty Calculator\trunk.powershell\test.ps1
            Line             : [ValidateSet([TestValidate])]

            PositionMessage  : At E:\Software development\Royalty Calculator\trunk.powershell\test.ps1:6 char:5
                               +     [ValidateSet([TestValidate])]
                               +     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            PSScriptRoot     : E:\Software development\Royalty Calculator\trunk.powershell
            PSCommandPath    : E:\Software development\Royalty Calculator\trunk.powershell\test.ps1
            CommandOrigin    : Internal
        ScriptStackTrace      : at <ScriptBlock>, <No file>: line 1
    TargetSite  :
        Name          : NewValidateSetAttribute
        DeclaringType : System.Management.Automation.Language.Compiler, System.Management.Automation, Version=7.3.4.500, Culture=neutral, PublicKeyToken=31bf3856ad364e35
        MemberType    : Method
        Module        : System.Management.Automation.dll
    Message     : Unable to find type [TestValidate].
    Data        : System.Collections.ListDictionaryInternal
    Source      : System.Management.Automation
    HResult     : -2146233087
    StackTrace  :
   at System.Management.Automation.Language.Compiler.NewValidateSetAttribute(AttributeAst ast)
   at System.Management.Automation.Language.Compiler.GetAttribute(AttributeAst attributeAst)
   at System.Management.Automation.Language.Compiler.GetRuntimeDefinedParameter(ParameterAst parameterAst, Boolean& customParameterSet, Boolean& usesCmdletBinding)
   at System.Management.Automation.Language.Compiler.GetParameterMetaData(ReadOnlyCollection`1 parameters, Boolean automaticPositions, Boolean& usesCmdletBinding)
   at System.Management.Automation.CompiledScriptBlockData.InitializeMetadata()
   at System.Management.Automation.CompiledScriptBlockData.Compile(Boolean optimized)
   at System.Management.Automation.PSScriptCmdlet..ctor(ScriptBlock scriptBlock, Boolean useNewScope, Boolean fromScriptFile, ExecutionContext context)
   at System.Management.Automation.CommandProcessor.Init(IScriptCommandInfo scriptCommandInfo)
   at System.Management.Automation.CommandDiscovery.GetScriptAsCmdletProcessor(IScriptCommandInfo scriptCommandInfo, ExecutionContext context, Boolean useNewScope, Boolean fromScriptFile, SessionStateInternal sessionState)
   at System.Management.Automation.CommandDiscovery.CreateCommandProcessorForScript(ExternalScriptInfo scriptInfo, ExecutionContext context, Boolean useNewScope, SessionStateInternal sessionState)
   at System.Management.Automation.CommandDiscovery.CreateScriptProcessorForSingleShell(ExternalScriptInfo scriptInfo, ExecutionContext context, Boolean useLocalScope, SessionStateInternal sessionState)
   at System.Management.Automation.CommandDiscovery.LookupCommandProcessor(CommandInfo commandInfo, CommandOrigin commandOrigin, Nullable`1 useLocalScope, SessionStateInternal sessionState)
   at System.Management.Automation.CommandDiscovery.LookupCommandProcessor(String commandName, CommandOrigin commandOrigin, Nullable`1 useLocalScope)
   at System.Management.Automation.ExecutionContext.CreateCommand(String command, Boolean dotSource)
   at System.Management.Automation.PipelineOps.AddCommand(PipelineProcessor pipe, CommandParameterInternal[] commandElements, CommandBaseAst commandBaseAst, CommandRedirection[] redirections, ExecutionContext context)
   at System.Management.Automation.PipelineOps.InvokePipeline(Object input, Boolean ignoreInput, CommandParameterInternal[][] pipeElements, CommandBaseAst[] pipeElementAsts, CommandRedirection[][] commandRedirections, FunctionContext funcContext)
   at System.Management.Automation.Interpreter.ActionCallInstruction`6.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
TargetObject          : [ValidateSet([TestValidate])]
CategoryInfo          : InvalidOperation: ([ValidateSet([TestValidate])]:AttributeAst) [], RuntimeException
FullyQualifiedErrorId : TypeNotFound
InvocationInfo        :
    ScriptLineNumber : 6
    OffsetInLine     : 5
    HistoryId        : -1
    ScriptName       : E:\Software development\Royalty Calculator\trunk.powershell\test.ps1
    Line             : [ValidateSet([TestValidate])]

    PositionMessage  : At E:\Software development\Royalty Calculator\trunk.powershell\test.ps1:6 char:5
                       +     [ValidateSet([TestValidate])]
                       +     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    PSScriptRoot     : E:\Software development\Royalty Calculator\trunk.powershell
    PSCommandPath    : E:\Software development\Royalty Calculator\trunk.powershell\test.ps1
    CommandOrigin    : Internal
ScriptStackTrace      : at <ScriptBlock>, <No file>: line 1

Environment data

Name                           Value
----                           -----
PSVersion                      7.3.4
PSEdition                      Core
GitCommitId                    7.3.4
OS                             Microsoft Windows 10.0.19045
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Visuals

No response

@MarcelVersteeg MarcelVersteeg added the Needs-Triage The issue is new and needs to be triaged by a work group. label May 18, 2023
@rhubarb-geek-nz
Copy link

rhubarb-geek-nz commented May 19, 2023

I would expect that testvalidate.psm1 would need to be in an installed module.

The rules for param include that it must be the first line in a script, so I expect that includes before using or import

But can be after comments and "#!/usr/bin/env pwsh" ...

@MarcelVersteeg
Copy link
Author

@rhubarb-geek-nz That could indeed be the issue, because when I give the command using module ./testvalidate.psm1 on the powershell prompt and then run my script, it runs correctly (with the expected results).

It would be nice that the using module can also be handled before the param declaration, preventing the need to actually install the used module. I actually do not want to have the module with the class definition installed, but still use the class definition for the [ValidateSet] attribute.

I guess this will need to be a feature request then.

@rhubarb-geek-nz
Copy link

You might be able solve the problem by having a function which has the the [ValidateSet] attributes for its parameters. So you define the parameters for the script at the top as typeless but with appropriate defaults, load your module then validate the parameters by calling your function which you define after loading the module.

@MarcelVersteeg
Copy link
Author

@rhubarb-geek-nz But then I will loose the tab-completion on that parameter, right?

@rhubarb-geek-nz
Copy link

I would expect so. For tab completion to work I expect it needs to have access to all the classes to do the value matching. If all the command line interpreter does is read the param stanza then it won't have enough information. I don't expect that tab-completion is done by actually running the script.

about_Functions_Argument_Completion

Can you not do something like their example?

Param(
    [ValidateSet('hello', 'world')]
    [string]$Message
)

$Message = 'bye'

The auto-completion offered both 'hello' and 'world' as options

This also errors when you try and assign 'bye' to $Message

@MarcelVersteeg
Copy link
Author

@rhubarb-geek-nz No, I cannot use a static set, as the validation set is actually dynamic, based on configuration files that might change. I do not want to copy the values from those files into the script due to maintenance. I just used the simple example to show what is going wrong.

@mklement0
Copy link
Contributor

mklement0 commented May 19, 2023

Your symptom is a variation of a long-standing, fundamental problem that appears to be a distinct bug:

  • Any types referenced in a param(...) block (or class definition) must already have been loaded into the session at parse time,

  • using module normally does provide parse-time access to classes and enums defined in the referenced module, but this seemingly only works in the body of a script, and unexpectedly not inside a param(...) block.


As an aside: Note that neither using module nor using assembly can currently provide parse-time access to types from binary assemblies. The - long-standing - plain is to fix this, to allow class definitions to use .NET types loaded via using module and using assembly statements, but this hasn't happened yet, presumably because the implementation is non-trivial; see the following for a technical discussion:

There's also a meta issue tracking all class-related problems:

@rhubarb-geek-nz
Copy link

rhubarb-geek-nz commented May 19, 2023

Are the configuration values so dynamic that they could change from run to run, or only from deployment to deployment?

Could you write a tool to update your script before deployment with the current set of values?

Could you load your class in the PowerShell profile?

@MarcelVersteeg
Copy link
Author

MarcelVersteeg commented May 19, 2023

Are the configuration values so dynamic that they could change from run to run, or only from deployment to deployment?

They might change from run to run

Could you write a tool to update your script before deployment with the current set of values?

That would be possible, but that is also something to forget as a user. So for me, and also from a user perspective, this is really a no-go.

Could you load your class in the PowerShell profile?

Of course that is possible, but that would mean that all shortcuts to PowerShell need to be updated on the machines that the users are using (even including Linux machines). Not a very nice scenario.

@mklement0
Copy link
Contributor

mklement0 commented May 19, 2023

There is a workaround, but it is somewhat cumbersome:
It relies on the fact that referencing types in script blocks used in parameter attributes happens at runtime, so the imported type is then already available:

using module ./testvalidate.psm1

param(
    [Parameter(Mandatory)]
    [ValidateScript({ 
      if ($_ -notin [TestValidate]::new().GetValidValues()) { throw "Not a valid value: $_" }
      $true
    })]
    [ArgumentCompleter({ 
      param($command, $param, $wordToComplete)
      [TestValidate]::new().GetValidValues() -like "$wordToComplete*"
    })]
    [string]$foo
)

The unfortunate side effect of using ArgumentCompleter with a script block (rather than with an IArgumentCompleter-implementing class) is that if what the user typed doesn't match any of the valid values, the default file-name completion behavior kicks in; see:

Copy link
Contributor

This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

@microsoft-github-policy-service microsoft-github-policy-service bot added the Resolution-No Activity Issue has had no activity for 6 months or more label Nov 17, 2023
Copy link
Contributor

This issue has been marked as "No Activity" as there has been no activity for 6 months. It has been closed for housekeeping purposes.

@mklement0

This comment was marked as resolved.

@totkeks
Copy link

totkeks commented Mar 25, 2024

After trying my luck in the PowerShell discord (https://discord.com/channels/180528040881815552/1221837959922192455) I found this issue through the long list of class issues.

I see the issue is closed, but I don't understand why, because the issue itself still exists.

I have the same issue with my GitManagement module that uses IValidateSetValuesGenerator to validate against the actual git repositories checked out on my PC, which of course is a dynamic set and not a static set.

It also fails when using the using module method to import the classes into the .ps1 files that use them in the ValidateSet.

With the help of the PowerShell discord, I also tried to switch to dot sourcing the classes as .ps1 (instead of .psm1) files in the root module, but this also fails, because of #4114

I will try out your workaround next.

Edit: Workaround works. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs-Triage The issue is new and needs to be triaged by a work group. Resolution-No Activity Issue has had no activity for 6 months or more
Projects
None yet
Development

No branches or pull requests

4 participants