Web References
First, here are some helpful links to get started with invoking the management shell from C# code:
Creating Applications that Use the Default Host
http://msdn2.microsoft.com/en-us/library/ms714671.aspx
Mstehle's Introduction to Exchange Powershell Automation
Part 1: http://blogs.msdn.com/mstehle/archive/2007/01/25/fyi-introduction-to-exchange-powershell-automation-part-1.aspx
Part 2: http://blogs.msdn.com/mstehle/archive/2007/01/25/outbox-introduction-to-exchange-powershell-automation-part-2.aspx
Sample Code: Calling Exchange cmdlets from .Net code
http://www.viveksharma.com/techlog/2006/07/27/sample-code-calling-exchange-cmdlets-from-net-code
Caveats
- Creating the necessary Runspace and RunspaceInvoke instances will take 2-3 seconds to get setup in a compiled application.
- It can take 6-9 seconds to execute the first Exchange command on a computer with only the Exchange 2007 management tools installed. You may also receive the following errors in the event log:
Event Type: Error
Event Source: MSExchange ADAccess
Event Category: General
Event ID: 2152
Date: 3/26/2007
Time: 7:38:52 PM
User: N/A
Computer: HASTY
Description:
Process eIDAD.exe (PID=2984). A remote procedure call (RPC) request to the Microsoft Exchange Active Directory Topology service failed with error 1753 (Error 6d9 from HrGetServersForRole). Make sure that the Remote Procedure Call (RPC) service is running. In addition, make sure that the network ports that are used by RPC are not blocked by a firewall.
Event Type: Error
Event Source: MSExchange Common
Event Category: Devices
Event ID: 106
Date: 3/26/2007
Time: 7:38:57 PM
User: N/A
Computer: HASTY
Description:
Performance counter updating error. Counter name is Latest Exchange Topology Discovery Time in Seconds, category name is MSExchange Topology. Optional code: 2.
- Invoking the Runspace and adding the Exchange management snap-in does not give you the same environment as opening the Exchange Management Shell. One of the most striking differences is that the $AdminSessionADSettings environment settings are not available. By default, all Exchange management cmdlets will search the entire forest. You can best replicate this environment by starting a normal PowerShell instance and add the Microsoft.Exchange.Management.Powershell.Admin snap-in. (Run “Add-PSSnapin -Name:’Microsoft.Exchange.Management.PowerShell.Admin’ ”).
- To compile your code you must add a reference for Systems.Management.Automation. By default this is added to the global assembly cache when PowerShell is installed. The best way to add this DLL as a reference is to manually edit the .csproj file associated with the project and add the following line to the references itemgroup.
<reference include="System.Management.Automation">
Adding the reference in this manner will ensure that you are always using the appropriate DLL whether you are running on a 32 or 64 bit machine.
Birth of the wrapper class
My first attempt at integrating recipient management into our identity management provisioning code worked well but I paid quite a time penalty when invoking the shell and issuing commands to update/set attributes on each account. Updating a single account was not too bad (~1 second per account) after the Runspace was completely setup and all the Exchange DLLs were loaded. However, when updating all 60,000+ accounts the time penalty added up and became an unacceptable bottleneck. To prevent this, I implemented logic to check if each attribute needed to be updated before invoking the command to set the attribute. This logic removed the bottleneck and very soon the code was running just as quickly as before; except for the time needed to create the Runspace. These optimizations made the Runspace unnecessary for most instances when an update was run on a single account and no Exchange attributes were changed. Thus the wrapper class was setup to store a Runspace that would only be initialized if a command was invoked.
Notes About the Wrapper Class
- The wrapper class is a singleton and must me instantiated as such. Because the class is a singleton, only one Runspace will be created no matter how many times the class is instantiated.
- The Runspace is only created once the first command is invoked. The same is true with RunspaceInvoke.
- The wrapper class combines methods from both the Runspace and RunspaceInvoke classes.
The Wrapper Class
using System;
using System.IO;
using System.Text;
using System.Reflection;
using System.Diagnostics;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Management.Automation;
using System.Management.Automation.Host;
using System.Management.Automation.Runspaces;
namespace EMSDemo.Utility
{
public sealed class ExchangeManagementShellWrapper
{
#region Variable Declaration
private RunspaceConfiguration rc; //RunspaceConfiguration
private Runspace r; //Runspace
private RunspaceInvoke ri; //RunspaceInvoke
private static readonly ExchangeManagementShellWrapper instance = new ExchangeManagementShellWrapper(); //Singleton instance
#endregion
private ExchangeManagementShellWrapper(){}
public static ExchangeManagementShellWrapper Instance
{
get
{
return instance;
}
}
private void InitializeRunspace()
{
//Setup Runspace environment for Exchange Management Shell
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(AssemblyResolver);
//Create RunspaceConfiguration
rc = RunspaceConfiguration.Create();
//Add PSSnapIn for Exchange 2007 and check for warnings
PSSnapInException warning;
PSSnapInInfo info = rc.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out warning);
if (warning != null)
{
// A warning is not expected, but if one is detected
// write the warning and return.
System.Console.Write(warning.Message);
return;
}
//Create and open runspace
r = RunspaceFactory.CreateRunspace(rc);
r.Open();
}
private void InitializeRunspaceInvoke()
{
//Create runspace if it has not already been created
if (r == null) { InitializeRunspace(); }
//Create RunspaceInvoke
ri = new RunspaceInvoke(r);
}
public ICollection<PSObject> RunspaceInvoke(string EMSCommand)
{
//Create RunspaceInvoke if it has not already been created
if (ri == null) { InitializeRunspaceInvoke(); }
//Invoke the commmand and return the results
return ri.Invoke(EMSCommand);
}
public ICollection<PSObject> RunspaceInvoke(string EMSCommand, IEnumerable input)
{
//Create RunspaceInvoke if it has not already been created
if (ri == null) { InitializeRunspaceInvoke(); }
//Invoke the commmand and return the results
return ri.Invoke(EMSCommand, input);
}
public ICollection<PSObject> RunspaceInvoke(string EMSCommand, out IList errors)
{
//Invoke the RunspaceInvoke method with a null input
return RunspaceInvoke(EMSCommand, null, out errors);
}
public ICollection<PSObject> RunspaceInvoke(string EMSCommand, IEnumerable input, out IList errors)
{
//Create RunspaceInvoke if it has not already been created
if (ri == null) { InitializeRunspaceInvoke(); }
//Invoke the commmand and return the results and errors
return ri.Invoke(EMSCommand, input, out errors);
}
public ICollection<PSObject> PipelineInvoke(Collection<Command> EMSCommands, out PipelineReader<object> Errors)
{
//Create Runspace if it has not already been created
if (r == null) { InitializeRunspace(); }
//Create Pipeline and add the commands
Pipeline pipeline = r.CreatePipeline();
foreach (Command item in EMSCommands)
{
pipeline.Commands.Add(item);
}
//Invoke the commands and return the results and errors
ICollection<PSObject> results = pipeline.Invoke();
Errors = pipeline.Error;
pipeline = null;
return results;
}
public ICollection<PSObject> PipelineInvoke(Collection<Command> EMSCommands)
{
//Create Runspace if it has not already been created
if (r == null) { InitializeRunspace(); }
//Create Pipeline and add the commands
Pipeline pipeline = r.CreatePipeline();
foreach (Command item in EMSCommands)
{
pipeline.Commands.Add(item);
}
//Invoke the commands and return the results
return pipeline.Invoke();
}
public ICollection<PSObject> PipelineInvoke(Command EMSCommand, out PipelineReader<object> Errors)
{
//Create Runspace if it has not already been created
if (r == null) { InitializeRunspace(); }
//Create Pipeline and add the command
Pipeline pipeline = r.CreatePipeline();
pipeline.Commands.Add(EMSCommand);
//Invoke the command and return the results and errors
ICollection<PSObject> results = pipeline.Invoke();
Errors = pipeline.Error;
pipeline = null;
return results;
}
public ICollection<PSObject> PipelineInvoke(Command EMSCommand)
{
//Create Runspace if it has not already been created
if (r == null) { InitializeRunspace(); }
//Create Pipeline and add teh command
Pipeline pipeline = r.CreatePipeline();
pipeline.Commands.Add(EMSCommand);
//Invoke the command and return the results
return pipeline.Invoke();
}
public void Dispose()
{
//Close the Runspace and cleanup
r.Close();
ri = null;
r = null;
}
private static System.Reflection.Assembly AssemblyResolver(object p, ResolveEventArgs args)
{
//Add path for the Exchange 2007 DLLs
if (args.Name.Contains("Microsoft.Exchange"))
{
return Assembly.LoadFrom(Path.Combine("C:\\Program Files\\Microsoft\\Exchange Server\\bin\\", args.Name.Split(',')[0] + ".dll"));
}
else
{
return null;
}
}
}
}
Examples
Each example will perform the same task and produce the same results using both the RuspaceInvoke and PipelineInvoke methods.
Initializing the Exchange Management Shell Wrapper
Singletons do not have a constructor so you must create an ‘instance’.
static void Main(string[] args)
{
//Initialize the ExchangeManagementShellWrapper
ExchangeManagementShellWrapper ems = ExchangeManagementShellWrapper.Instance;
ICollection<PSObject> results;
...
Invoking a Single Command
In this example both the RunspaceInvoke and PipelineInvoke methods are used to get information about the mailbox for ‘knsmith’. In each foreach loop are different methods for accessing the data within the PSObject.
//Use the RunspaceInvoke command with a command string
results = ems.RunspaceInvoke("Get-Mailbox knsmith");
foreach (PSObject item in results)
{
Console.WriteLine(item.Members["Name"].Value.ToString());
}
//Use the PipelineInvoke command with a Command object
Command EMSCommand = new Command("Get-Mailbox");
EMSCommand.Parameters.Add("Identity", "knsmith");
results = ems.PipelineInvoke(EMSCommand);
foreach (PSObject item in results)
{
//A different way of accessing property info from the results
PSPropertyInfo prop = (PSPropertyInfo)item.Properties["Name"];
if (prop != null)
{
Console.WriteLine(prop.Value.ToString());
}
}
Invoking Commands With Errors Ouput
In this example each command will attempt to create a new file at c:\temp called test.ps1. A new file will be created it one does not already exist. If the file does exist, an error will be returned and the output will be: “Error: The file ‘C:\temp\test.ps1’ already exists.
//Using RunspaceInvoke with errors
IList IErrors;
results = ems.RunspaceInvoke("New-Item -Path:'c:\\temp\\test.ps1' -Type:File", out IErrors);
foreach (PSObject item in results)
{
Console.WriteLine(item.Members["Name"].Value.ToString());
}
foreach (object item in IErrors)
{
Console.WriteLine("Error: {0}", item.ToString());
}
//Using PipelineInvoke with errors
EMSCommand = new Command("New-Item");
EMSCommand.Parameters.Add("Path", "c:\\temp\\test.ps1");
EMSCommand.Parameters.Add("Type", "File");
PipelineReader<object> errors;
results = ems.PipelineInvoke(EMSCommand, out errors);
foreach (PSObject item in results)
{
Console.WriteLine(item.Members["Name"].Value.ToString());
}
if (errors.Count > 0)
{
foreach (object obj in errors.ReadToEnd())
{
Console.WriteLine("Error: {0}", obj.ToString());
}
}
Invoking Multiple Commands
Invoking multiple commands using a pipe is one of my most common tasks in PowerShell. The RunspaceInvoke method is pretty straight forward as you can just pass in a command string the same as you use in the Exchange Management Shell. The PipelineInvoke method is a little different. You must create a Command object for each command you wish to issue. After the Command objects have been created, add them in the order they are to be executed to the collection.
//Using the RunspaceInvoke command with a command string
ems.RunspaceInvoke("Get-Mailbox knsmith | Set-Mailbox -EmailAddressPolicyEnabled:$True");
//Using PipelineInvoke with a Command Collection
Collection<Command> CommandsList = new Collection<Command>();
Command GetMailbox = new Command("Get-Mailbox");
GetMailbox.Parameters.Add("Identity", "knsmith");
CommandsList.Add(GetMailbox);
Command SetMailbox = new Command("Set-Mailbox");
SetMailbox.Parameters.Add("EmailAddressPolicyEnabled", true);
CommandsList.Add(SetMailbox);
ems.PipelineInvoke(CommandsList, out errors);
Passing an Input Object to the Runspace
The RunspaceInvoke method offers the added ability to pass in an input collection. The key to this is that you must start your command string with “$input | ” so that your input will be processed. This example will show how you can get the attributes for a mailbox, make a change and commit the changes by passing back the results and using the Set-Mailbox cmdlet.
//Use the RunspaceInvoke command with a command string
results = ems.RunspaceInvoke("Get-Mailbox knsmith");
foreach (PSObject item in results)
{
//Set the property to false
item.Members["UseDatabaseQuotaDefaults"].Value = false;
}
IList SetErrors;
//Save the changes we made to the results
//The $input variable will be equal to the 'results' variable we pass in
ems.RunspaceInvoke("$input | Set-Mailbox", results, out SetErrors);
foreach (object error in SetErrors)
{
Console.WriteLine(error.ToString());
}
Download the wrapper class and example code. (Right-click and 'Save Target As')
Conclusion
Whether or not you decide to use this wrapper class I hope it at least helps you with examples of how to utilize the RunspaceInvoke and Pipeline methods for invoking the Exchange management shell.
--Nick