在 C# 中编写 PowerShell 模块时,如何存储模块状态

本文关键字:存储模块 状态 模块 PowerShell | 更新日期: 2023-09-27 18:09:39

我正在用C#编写一个连接到数据库的PowerShell模块。该模块具有可用于查询数据库的Get-MyDatabaseRecord cmdlet。如果变量 $MyCredentials 中有 PSCredential 对象,则可以按如下所示调用 cmdlet:

PS C:'> Get-MyDatabaseRecord -Credential $MyCredentials -Id 3

MyRecordId    : 3
MyRecordValue : test_value

问题是,每次调用Get-MyDatabaseRecord时都必须指定Credential参数既乏味又低效。如果只调用一个 cmdlet 连接到数据库,然后调用另一个 cmdlet 获取记录,那就更好了:

PS C:'> Connect-MyDatabase -Credential $MyCredentials
PS C:'> Get-MyDatabaseRecord -Id 3

MyRecordId    : 3
MyRecordValue : test_value

为了实现这一点,Connect-MyDatabase cmdlet 必须将数据库连接对象存储在某个位置,以便Get-MyDatabaseRecord cmdlet 可以获取该对象。我应该怎么做?

我想到的想法

使用静态变量

我可以在某处定义一个静态变量来包含数据库连接:

static class ModuleState
{
    internal static IDbConnection CurrentConnection { get; set; }
}

但是,全局可变状态通常是一个坏主意。这会以某种方式引起问题,还是这是一个很好的解决方案?

(问题的一个示例是,如果多个 PowerShell 会话以某种方式共享程序集的同一实例。然后,所有会话都会无意中共享一个CurrentConnection属性。但我不知道这是否真的可能。

使用 PowerShell 模块会话状态

MSDN 页面"Windows PowerShell 会话状态"讨论了称为会话状态的东西。该页面说"会话状态数据"包含"会话状态变量信息",但没有详细说明这些信息是什么或如何访问它。

该页面还说SessionState类可用于访问会话状态数据。此类包含一个名为 PSVariable 的属性,类型为 PSVariableIntrinsics

但是,我对此有两个问题。第一个问题是访问 SessionState 属性需要我从 PSCmdlet 继承而不是Cmdlet ,我不确定我是否要这样做。

第二个问题是我不知道如何将变量设为私有。这是我正在尝试的代码:

const int TestVariableDefault = 10;
const string TestVariableName = "TestVariable";
int TestVariable
{
    get
    {
        return (int)SessionState.PSVariable.GetValue(TestVariableName,
            TestVariableDefault);
    }
    set
    {
        PSVariable testVariable = new PSVariable(TestVariableName, value,
            ScopedItemOptions.Private);
        SessionState.PSVariable.Set(testVariable);
    }
}

TestVariable属性就像我期望的那样。但是尽管我使用的是ScopedItemOptions.Private,我仍然可以通过输入$TestVariable在提示符下访问此变量,并且该变量列在Get-Variable的输出中。我希望我的变量对用户隐藏。

在 C# 中编写 PowerShell 模块时,如何存储模块状态

一种方法是使用输出连接对象的 cmdlet 或函数。此对象可以只是 PSCredential 对象,也可以包含凭据和其他信息(如连接字符串(。现在将其保存在变量中,可以继续执行此操作,但也可以使用$PSDefaultParamterValues来存储此值并将其传递给模块中的所有相应 cmdlet。

我从未编写过 C# 模块,但我在 PS 中做了类似的事情:

function Set-DefaultCredential
{
    param
    (
        [PSCredential]
        $Credential
    )
    $ModuleName = (Get-Item -Path $PSScriptRoot).Parent.Name
    $Module = Get-Module -Name $ModuleName
    $Commands = $Module.ExportedCommands.GetEnumerator()  | Select-Object -ExpandProperty value | Select-Object -ExpandProperty name
    foreach ($Command in $Commands)
    {
        $Global:PSDefaultParameterValues["$Command`:Credential"] = $Credential
    }
}

此代码使用 $PSDefaultParameterValues auto 变量将你传入的凭据设置为我的模块的任何导出命令的默认凭据。当然,你的逻辑可能不一样,但它可能会告诉你方法。

我也有类似的想法,但从来没有需要构建一个完整而干净的实现。 存储状态的一种似乎适合我的连接的方法是将该状态封装在 PSDrive 中。 此类驱动器已集成到会话状态中。

文档在这方面并不总是很明显,但它涉及编写一个派生自 DriveCmdletProvider 的类,该类具有对凭据管理的内置支持。 我记得,NewDrive 方法可以返回从具有其他成员(包括私有或内部成员(的PSDriveInfo派生的自定义类,以存储所需的内容。

然后,您可以使用New-PSDriveRemove-PSDrive建立/断开具有驱动器名称的连接连接。 DriveCmdletProvider提供的动态参数允许您自定义参数以New-PSDrive您的具体情况。

有关详细信息,请参阅此处的 MSDN。

我想最简单的方法是在ps中创建一个高级函数并使用$script:mycred

但是,如果您需要坚持使用纯 c# 并支持多个运行空间,则可以创建运行空间 ID 和标记的字典

public static class TokenCollection {
    public static readonly Dictionary<Guid,PSObject> Tokens = new Dictionary<Guid,PSObject>();
}

然后,使用令牌将当前运行空间 guid 添加到其中

Guid runspId = Guid.Empty;
using (var runsp = PowerShell.Create(RunspaceMode.CurrentRunspace))
{
    runspId = runsp.Runspace.InstanceId;
    TokenCollection.Add(runspId, token);
}

获取这样的令牌

PSObject token = null;
if (TokenCollection.Tokens.ContainsKey(runspaceId))
{
    token = TokenCollection.Tokens[runspId];
}