Sophie Swett
Sophie Swett

Reputation: 3398

When writing a PowerShell module in C#, how do I store module state?

I'm writing a PowerShell module in C# that connects to a database. The module has a Get-MyDatabaseRecord cmdlet which can be used to query the database. If you have a PSCredential object in the variable $MyCredentials, you can call the cmdlet like so:

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


MyRecordId    : 3
MyRecordValue : test_value

The problem is, having to specify the Credential parameter every time that you call Get-MyDatabaseRecord is tedious and inefficient. It would be better if you could just call one cmdlet to connect to the database, and then another to get the record:

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


MyRecordId    : 3
MyRecordValue : test_value

In order for that to be possible, the Connect-MyDatabase cmdlet has to store the database connection object somewhere so that the Get-MyDatabaseRecord cmdlet can obtain that object. How should I do this?

Ideas I've thought of

Use a static variable

I could just define a static variable somewhere to contain the database connection:

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

However, global mutable state is usually a bad idea. Could this cause problems somehow, or is this a good solution?

(One example of a problem would be if multiple PowerShell sessions somehow shared the same instance of my assembly. Then all of the sessions would inadvertently share a single CurrentConnection property. But I don't know if this is actually possible.)

Use PowerShell module session state

The MSDN page "Windows PowerShell Session State" talks about something called session state. The page says that "session-state data" contains "session-state variable information", but it doesn't go into detail about what this information is or how to access it.

The page also says that the SessionState class can be used to access session-state data. This class contains a property called PSVariable, of type PSVariableIntrinsics.

However, I have two problems with this. The first problem is that accessing the SessionState property requires me to inherit from PSCmdlet instead of Cmdlet, and I'm not sure if I want to do that.

The second problem is that I can't figure out how to make the variable private. Here's the code that I'm trying:

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);
    }
}

The TestVariable property works just as I would expect. But despite the fact that I'm using ScopedItemOptions.Private, I can still access this variable at the prompt by typing in $TestVariable, and the variable is listed in the output of Get-Variable. I want my variable to be hidden from the user.

Upvotes: 13

Views: 2296

Answers (3)

Matt McNabb
Matt McNabb

Reputation: 362

One approach would be to use a cmdlet or function that outputs a connection object. This object could be simply the PSCredential object, or it could contain the credential and other information like a connection string. You're saving this in a variable now and you can continue to do this, but you can also use $PSDefaultParamterValues to store this value and pass it to all the appropriate cmdlets in the module.

I've never written a C# module but I've done something similar in 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
    }
}

This code sets the credential you've passed in as the default for any of the exported commands of my module using the $PSDefaultParameterValues automatic variable. Of course your logic may not be the same but it might show you the approach.

Upvotes: 6

Max
Max

Reputation: 77

I suppose the easiest way is to just make an advanced function in ps and use $script:mycred

But if u need to stick to pure c# and support multiple runspaces, it may be possible to make a dictionary of runspace ids and tokens

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

You then add your current runspace guid inside with your token

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

Get a token like this

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

Upvotes: 1

Burt_Harris
Burt_Harris

Reputation: 6874

I've had similar thoughts, but never had the need to build a full and clean implementation. One approach to storing state that seemed appropriate for my connections was to encapsulate that state in a PSDrive. The such drives are integrated into session state.

The docs aren't always obvious on this, but it involves writing a class that derives from DriveCmdletProvider, which has built-in support for credential management. As I remember it, the NewDrive method can return a custom class derived from PSDriveInfo with additional members, including private or internal members, to store what you need.

You can then use New-PSDrive and Remove-PSDrive to establish/break connections connection with a drive name. Dynamic parameters provided by the DriveCmdletProvider let you customize parameters to New-PSDrive for your specifics.

See MSDN here for details.

Upvotes: 1

Related Questions