Connect to Microsoft Exchange PowerShell within C#

MadBoy picture MadBoy · Mar 26, 2016 · Viewed 7.9k times · Source

I'm trying to connect to remote powershell from C# .NET WinForms app. My goal is to create my own version of Microsoft PowerShell ISE. So i need a way to execute PowerShell Scripts from my app on Remote Machines. I've created couple of methods and tested it on local machine from my app. If I don't use WSManConnectionInfo and use using (Runspace remoteRunspace = RunspaceFactory.CreateRunspace()) i can execute scripts locally as if it was true powershell (little scripts, usage of variables, output data using ft, fl, do a lot of other things I usually do with powershell. Problem starts when I add WSManConnectionInfo and point it to my Exchange Server instead of using local connection. It seems it's able to execute basic stuff like "get-mailbox" but as soon as i try to pipe things, use some scripting capabilities like $variables it breaks saying it's unsupported.

Similarly I have to disable powershell.AddCommand("out-string"); when not using it locally.

An unhandled exception of type 'System.Management.Automation.RemoteException' occurred in System.Management.Automation.dll.

Additional information: The term 'Out-String' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

The very same error doesn't appear if I don't force remote connection but simply do it locally. It seems like the SchemaUri is making it very strict to only execute basic commands. I saw other examples where people where using very direct information such us:

powershell.AddCommand("Get-Users");
powershell.AddParameter("ResultSize", count);

But with that approach I would have to define a lot of possible options and I don't want to go thru defining parameters and other stuff. I simply would like to load "script" and execute it just like in PowerShell window.. Here's an example of what I use now.

    public static WSManConnectionInfo PowerShellConnectionInformation(string serverUrl, PSCredential psCredentials)
    {
        var connectionInfo = new WSManConnectionInfo(new Uri(serverUrl), "http://schemas.microsoft.com/powershell/Microsoft.Exchange", psCredentials);
        //var connectionInfo = new WSManConnectionInfo(new Uri(serverUrl), "http://schemas.microsoft.com/powershell", psCredentials);
        connectionInfo.AuthenticationMechanism = AuthenticationMechanism.Basic;
        connectionInfo.SkipCACheck = true;
        connectionInfo.SkipCNCheck = true;
        connectionInfo.SkipRevocationCheck = true;
        connectionInfo.MaximumConnectionRedirectionCount = 5;
        connectionInfo.OperationTimeout = 150000;
        return connectionInfo;
    }
    public static PSCredential SecurePassword(string login, string password)
    {
        SecureString ssLoginPassword = new SecureString();
        foreach (char x in password) { ssLoginPassword.AppendChar(x); }
        return new PSCredential(login, ssLoginPassword);
    }
    public static string RunScriptPs(WSManConnectionInfo connectionInfo, string scriptText)
    {
        StringBuilder stringBuilder = new StringBuilder();
        // Create a remote runspace using the connection information.
        //using (Runspace remoteRunspace = RunspaceFactory.CreateRunspace())
        using (Runspace remoteRunspace = RunspaceFactory.CreateRunspace(connectionInfo))
        {
            // Establish the connection by calling the Open() method to open the runspace. 
            // The OpenTimeout value set previously will be applied while establishing 
            // the connection. Establishing a remote connection involves sending and 
            // receiving some data, so the OperationTimeout will also play a role in this process.
            remoteRunspace.Open();
            // Create a PowerShell object to run commands in the remote runspace.
            using (PowerShell powershell = PowerShell.Create())
            {
                powershell.Runspace = remoteRunspace;
                powershell.AddScript(scriptText);
                //powershell.AddCommand("out-string");
                powershell.Commands.Commands[0].MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output);
                Collection<PSObject> results = powershell.Invoke();
            
                foreach (PSObject result in results) {
                        stringBuilder.AppendLine(result.ToString());
                }

            }
            // Close the connection. Call the Close() method to close the remote 
            // runspace. The Dispose() method (called by using primitive) will call 
            // the Close() method if it is not already called.
            remoteRunspace.Close();
        }

        // convert the script result into a single string
        return stringBuilder.ToString();
    }

Any advice on why this is happening and workaround how to get it to behave the same way? I've seen a lot of blogs like this but defining every simple command doesn't make sense to me. I also saw an option to create local connection and then execute remote connection within that but that's gotta be last resort since it relies on multiple other factors.

Answer

Aziz Kabyshev picture Aziz Kabyshev · Mar 30, 2016

Check https://blogs.msdn.microsoft.com/akashb/2010/03/25/how-to-migrating-exchange-2007-powershell-managed-code-to-work-with-exchange-2010/:

The management experience given by Exchange 2010 through PowerShell has been moved all the way from Local to Remote. [...] Only exchange cmdlets will work in this remoting scenario, you will not be able to run most of the powershell cmdlets. [...] Yes, this does mean that you will not be able to run cmdlets like Where-Object and .PS1 scripts in the Remote Runspace.

Is that a limitation? I don’t think so. We can very easily get around it by create a new Session and Importing it.


So you'll need to do something like this:

PSCredential creds = new PSCredential(userName, securePassword);
System.Uri uri = new Uri("http://Exchange-Server/powershell?serializationLevel=Full");

Runspace runspace = RunspaceFactory.CreateRunspace();

PowerShell powershell = PowerShell.Create();
PSCommand command = new PSCommand();
command.AddCommand("New-PSSession");
command.AddParameter("ConfigurationName", "Microsoft.Exchange");
command.AddParameter("ConnectionUri", uri);
command.AddParameter("Credential", creds);
command.AddParameter("Authentication", "Default");
powershell.Commands = command;
runspace.Open(); powershell.Runspace = runspace;
Collection<PSSession> result = powershell.Invoke<PSSession>();

powershell = PowerShell.Create();
command = new PSCommand();
command.AddCommand("Set-Variable");
command.AddParameter("Name", "ra");
command.AddParameter("Value", result[0]);
powershell.Commands = command;
powershell.Runspace = runspace;
powershell.Invoke();

powershell = PowerShell.Create();
command = new PSCommand();
command.AddScript("Import-PSSession -Session $ra");
powershell.Commands = command;
powershell.Runspace = runspace;
powershell.Invoke();

# now you can use remote PS like it's local one