Tuesday, March 27, 2007

Managing Exchange 2007 Recipients with C#

During our Exchange 2007 implementation I was faced with the task of integrating Exchange 2007 recipient management with our in-house identity management systems. Our active directory provisioning code was already written in .Net and implemented CDOEXM for recipient management. One of the things that concerned me the most about migrating to Exchange 2007 was the ability to manage recipients from within the existing code. CDOEXM is not supported for managing Exchange 2007 recipients and the idea of invoking the Exchange management shell from .Net code was a foreign concept to me. Information on how to do so was sparse. Many many thanks to Vivek Sharma for posting sample code before anything else was available. My early code was based directly off his examples. Since then I have created a few applications using these methods and I have learned quite a few lessons while doing so. Below are some of my experiences and customizations that made the process much simpler.

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

63 comments:

Anonymous said...

Hi Nick,

Nice and useful article.
Had a question,
If i invoke a command from C# code, is there a way to get the result value as failed or passed?
Or is it if object count in IErrors is zero then its success.
After i invoke cmd mount-database or dismount-database i want to know if it was successful.

Thanks
WN

Nick Smith said...

I would think that if the number of errors were equal to zero you could assume a successful operation. If you really wanted to be sure you could invoke a 'Get-MailboxDatabase <databaseID> -Status' cmdlet to check the mount status.

Remember to append a ' -Confirm:$False' to your cmdlet string when dismounting the database. Otherwise you will receive errors or an exception since you can't confirm the operation manually.

Anonymous said...

Nick,

I have used a similar techique for executing cmdlets; however, I have discovered that a cmdlet can fail without raising a terminating error or populating the error property on the pipeline that executed the command.

Is there any other ways to determine if the cmdlet actually worked in this situation? ie) checking the count property on the result set collection?

Nick Smith said...

That really depends on the command you are trying to invoke. Not all commands will return a result set. I would suggest running a Get- command to ensure the values were set correctly.

Thobias said...

Hi Nick,

Thanks for the article.

I have used your wrapper class to create mailbox in exchange 2007 from my c# code.

It works great in my development machine, with 32bit exchange server and visual studio 2005.

I am facing some issues in the production environment.

The exchange server in production is 64bit and 32bit powershell and 32 bit exchange management console is installed in the machine where my code runs.

My code runs without raising any exception, the invoke method returns with no result (PSObject with count 0).

I see following error messages in the Application eventlog:


An 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.


and


Performance counter updating error. Counter name is Latest Exchange Topology Discovery Time in Seconds, category name is MSExchange Topology. Optional code: 2.


I confirmed that the firewall is not blocking the RPC calls.

I also tried executing the code in the Exchange Server. I get the PSObject with count 0 and no error messages in the eventlog.

Please help me to resolve this.

Thanks
Thobias

Nick Smith said...

Thobias,

The error messages you mentioned are pretty standard for a machine (32 or 64 bit) running only the management tools.

Do you see the same results when running the code on but your development workstation and production server? Is the code running as a user that has the appropriate Exchagne permissions? Do you get a PSObject with a count of 0 when running a simple command like "Get-Mailbox"?

Would you please email the string that you are passing to the invoke method to me at k.nick.smith at gmail.com. I'd be happy to help you troubleshoot.

--Nick

lushdog said...

Is there a way to trap the WARNINGS (yellow text). This would be a useful if you are running a cmdlet like "Test-SystemHealth" that has useful information in the Errors and Warnings.

TIA,

Matt

neppoz said...

Hi,

thank you for the wrapper. I use it in my C# Code to Mailbox Enable newly created users. On my production machine i get the following error. On my develop machine everything is fine. I suggest a problem with threading nut not sure. Can you help ?

Cannot load Windows PowerShell snap-in Microsoft.Exchange.Management.PowerShell.Admin because of the following error: Could not load file or assembly 'Microsoft.Exchange.PowerShell.Configuration, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies. Failed to grant permission to execute. (Exception from HRESULT: 0x80131418)

Greetings

lushdog said...

So what scope are we running the cmdlets in if we user your wrapper? Domain Scope or Forest Scope?

Nick Smith said...

Neppoz,

Sorry, I missed your comment earlier. The two possibilities that come to mind are:

1) Did you install Exchange onto a different drive letter than C:\? If so you will need to change the path at the bottom on the wrapper in the AssemblyResolver method.

2) Are you running the code with an account that has permissions on the Exchange bin directory?

Let me know if any of these resolve your problem.

--Nick

Nick Smith said...

Lushdog,

Commands executed from the wrapper class run in the forest scope.

--Nick

neppoz said...

Hi Nick,
before i answer your questions i must say that i developed on my test machine. There i had no problem. I suppose it's a problem with the Powershell itself. Ok, lets see:

1) Exchnage installed normally. No paths adjusted
2) That's rather interesting. My Webapp (Sharepoint Webpart) is using impersonation. In fact the account that is visiting the site under normal circumstance has no rights on the Exchnage bin directory. By the way Exchange Machine is remote, not installed locally.

Let me now.

Greetings neppoz

Alex said...

Hello,

I tried to execute a simple Exchange 2007 Powershell command in my C# code:

Get-MailboxStatistics -Identity TestUser1

This Command works well in the Powershell command window.

I tried your wrapper, which is by the way really great! And I also tried the follwowing code. Each time the same. The mailbox could not be found. No results...
Paradoxically it works for some mailboxes and don't work for others. Whereas is ALWAYS works in the Powershell window on the same computer.

--------------------------
RunspaceConfiguration rsConfig = RunspaceConfiguration.Create();

PSSnapInException snapInException = null;

PSSnapInInfo info =
rsConfig.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out
snapInException);

Runspace myRunSpace = RunspaceFactory.CreateRunspace(rsConfig);

myRunSpace.Open();

Pipeline pipeLine = myRunSpace.CreatePipeline();


RunspaceInvoke ri = new RunspaceInvoke(myRunSpace);

Collection /PSObject/
commandResults =
ri.Invoke("Get-MailboxStatistics -Identity testuser1");

foreach (PSObject cmdlet in commandResults)

{

string cmdletName = cmdlet.Properties["DisplayName"].Value.ToString();

System.Diagnostics.Debug.Print(cmdletName);

}

------------------



But unfortunatly for some Users this command returns, that the mailbox could
not be found, even though the command works well in the Powershell window.
Where are differences in executing a command in the powershell window or in
code???

Alex

Anonymous said...

Just a new information:

This only appears, if the program runs in Debug mode. If the program is started outside VS2005 it works well....

Alex

Anonymous said...

Hi Nick, great post.

Is there a way to get the output of the commands?

I have the following code, which runs without error:

mailboxCommand = new Command("Get-MailboxServer");
CommandParameter userParam = new CommandParameter("Identity", serverName);
mailboxCommand.Parameters.Add(userParam);

pipeLine.Commands.Add(mailboxCommand);

formatlistCommand = new Command("Format-List");
CommandParameter formatParam = new CommandParameter("Property", "MessageTrackingLogPath");
formatlistCommand.Parameters.Add(formatParam);

pipeLine.Commands.Add(formatlistCommand);

commandResults = pipeLine.Invoke();

What I need to get is the value of the MessageTrackingLogPath property, so I can then process the logs. commandResults is a PSObject collection, which does not give me anything useful.

Neither obj.Properties["MessageTrackingLogPath"].Value.ToString() nor obj.Properties["Name"].Value.ToString() work - returning "Reference not set to an instance of the object".

Any ideas?
Thanks

Burt said...

How about deployment? Can an application using the System.Management.Automation be deployed to any client. What would the prerequisites be for deploying this kind of application.

(Since I'm having an error like: "An error occurred when loading the system Windows PowerShell snap-ins". This when trying to run an application with similar code on a clean server...)

Anonymous said...

Hey,
do you have any tips to get this faster? If the application loads first time, its really slow. The second time and later it works fine. I think this resides on the management tools, but i have no idea where i can begin to search for a fix.
Many thanks!

Nick Smith said...

Wow, lots of new comments. I apologize I have not been as responsive as normal (and lack of updates). I have been in the process of changing jobs and trying to move to a new city. I promise to answer your questions and hopefully I will have some time at the airport this afternoon to do so.


I'll start my replies from the newest...

Anonymous 11/14/2007,

How long is it taking you to complete the first command? Running the application on a machine with an Exchange 2007 role installed will decrease the initialization time a bit (because it can access the ADAccess service). But even then there is a bit if initiation time penalty. Think of it as the time it takes to start the Exchange Management Shell.

--Nick

Nick Smith said...

Burt,

In order for code using this wrapper to work that machine must have Powershell and the Exchange 2007 management tools installed. Additionally the user account invoking the code must have the necessary permissions to perform the intended tasks.

--Nick

Nick Smith said...

Anonymous 11/12/2007,

I haven't tried using the format-list command in my managed code. I have alyways just pulled the information directly from the initial results. My suggestion would be to remove the format-list pipeline command it see if you can then access the desired information.

--Nick

Nick Smith said...

Alex,

Thanks for the info. I have always compiled the program and executed it in a seperate command window when testing. (Mostly because I was never developing with an account that had the necessary Exchange permissions.)

--Nick

Nick Smith said...

neppoz,

One of the requirements of this wrapper is that the Exchange management tools must be installed locally.

Our solution to incorporating our Exchange user management and a web front end was to have the webpage store all the configuration data within a database. We scheduled a job on a machine with the Exchange tools installed that ran every few minutes. It would query the database for any recent changes and run the necessary commands.

We chose the scheduled job option because we were unable to launch a real-time update onto a 64-bit machine. If you are running the code on a 32-bit machine with the management tools installed you can launch a PSExec command to that machine invoking the code. We have used both methods with great success.

--Nick

wchai said...

Hi Nick,

I wrote a c# class dll which
was used to retrieves Exchange
Server 2007 information using
cmdlets. I have another C++
code that interfaces with my C#
dll through COM object.
I always got errors from
CoCreateInstance()
complaining invalid argument
on win32 build and class not
not registered on x64
build. I was using Visual Studio
2005 to complile the code.
I run my C# code as a stand
alone program without problems.
Any idea? Thanks in advance!

Wchai

Nick Smith said...

Wchai,

My suggestion would be to make sure that when you are compiling your applicaion ensure that "Any CPU" or "x64" is selected for the platform.

--Nick

wchai said...

Hi Nick,

Yes, I have selected "any cpu" when building my C# dll and "x64" for my
C++ code. Do you know if the interop between C++ and managed 64 bit code is supported on 64 bit Windows 2003 server?

Wchai.

Anonymous said...

I'm creating application for creating users and mailbox in Exchange 2007 environment in VB.NET (2008). I've not been able to find how to add/remove values from multivalued properties f.ex. EmailAddresses. In powershell SDK they say you should do it like this:
$Mailbox = Get-Mailbox "Kim Akers"
$Mailbox.EmailAddresses += "kim@contoso.com"
Set-Mailbox "Kim Akers" -EmailAddresses $Mailbox.EmailAddresses.
How can I do this in code?

Regards,
Thorir

Nick Smith said...

Anonymous,

The code that you have posted can be accomplished using a scriptblock. Scriptblocks require a seperate runspace instance and I don't have that covered in the wrapper class.

For the purpose of adding email addresses using the wrapper class you can use the following one-liner command:

Set-Mailbox "Kim Akers" -EmailAddresses ((Get-Mailbox "Kim Akers").EmailAddresses + "smtp:kim@contoso.com")

--Nick

Timothy Schilbach said...

Hi Nick,

This has helped me greatly on implementing some web services that help manage our small ISP.

I am having one wierd issue I was hoping you could give me some insight into. I am new to powershell but understand the concepts and syntax pretty well.

Whenever I execute:

ems.RunspaceInvoke("($Private:secureString = ConvertTo-SecureString \"password\" -AsPlainText -Force) | foreach {New-mailbox -UserPrincipalName chris@contoso.com -alias chris -Name ChrisAshton -OrganizationalUnit aod -FirstName Chris -LastName Ashton -DisplayName \"Chris Ashton\" -Database (\"cd747c5d-aa8e-4f81-a136-ea4ff89e2896\") -Password $secureString}");

I get an error in C# that states:

Database "cd747c5d-aa8e-4f81-a136-ea4ff89e2896" was not found. Please make sure you have typed it correctly.

However, if I excute directly in powershell console, it works without any issues. Why am I unable to find the database from the code, but I can when in the console?

-Timothy

Nick Smith said...

Timothy,

Executing cmdlets from the wrapper class will not always operate the same way as they do in the Exchange Management Shell. To accurately test cmdlets that fail in the shell you will want to open a new instance of plain powershell. Execute the following command to register the Exchange cmdlets:

Add-PsSnapin Microsoft.Exchange.Management.PowerShell.Admin

You will be able to execute Exchange cmdlets after running this command but you will notice that all commands will run against the entire forest. I imagine this will give you insite as to why the command is failing.

--Nick

Derrall said...

Hello,

I am trying to implemented a few exchange management web services with .net and the c# wrapper class you have kindly provided. I have successfully been able to implement services for looking up mailboxes (ie Get-mailbox, etc). However any cmdlets that involve a write operation (Such as new-mailbox) I get the following error:

Active Directory operation failed on [name of our domain controller]. This error is not retriable. Additional information: Access is denied.
Active directory response: 00000005: SecErr: DSID-03151E04, problem 4003 (INSUFF_ACCESS_RIGHTS), data 0


Any ideas on what I can do to fix this error? Is there some kind of IIS, .Net or exchange configuration I am missing? .Net is telling me that the web services are being executed as myself (and yes I have the permissions to execute these cmdlets manually from PowerShell).
Thanks.

Nick Smith said...

Derrall,

My initial guess would be to check the identity that they application pool was running as.

I have not tried to use this code directly as part of web appliction. You may also want to avoid using impersonation when running this code from a web applicaion.

--Nick

Anonymous said...

Nick, this article is a great help. In my particular application im trying to deal with Exchange quotas and can find very little documentation.

Do you know how I can detect if the IssueWarningQuota property of a PSObject resulting from a get-mailbox is set to "Unlimited" in a "safe" way that isn't comparing typename strings (as that doesn't feel very future proof!)

Nick Smith said...

Anonymous,

If you want to check that the IssueWarningQuota is set to "Unlimited" without comparing strings I would recommend checking the user account attributes directly using the DirectoryEntry C# method. Look for the mDBStorageQuota to be set to null and mDBUseDefaults value to be set to False.

--Nick

Rosie said...

This is a great site. Thank you for your information. I THANK YOU I SALUTE YOU IT,S A AMZING SITE.

Jon said...

Hi Nick,

Thanks for this very useful post, a lot of nifty ideas and solutions :)

Cheers,
Jon

Anonymous said...

Hi Nick,

I have a problem, and as i see, you can help me resolve it. I made an msi package, what installs my application and runs a script, like you. If i take my code to a general application, then it's working well, but if i try to use from the msi package (the same code), then i receive an error. I posted my question to the microsoft forum too. It's here:
http://forums.microsoft.com/TechNet/ShowPost.aspx?PostID=3798889&SiteID=17&mode=1

Please take a look, and if you can please help me.
Thanks for all!

Kindly regards:
Imre Kovács

Anonymous said...

I'm trying to run the wrapper as a web app to create mailboxes, and am getting the following: Access to the address list service on all Exchange 2007 servers has been denied.

I don't have the impersonination on and am using Windows authentication and logging on as Administrator. What's the simplest way to accomplish this? It works if I just try to list a user or something...
Thank you !

Anonymous said...

Hi,

Did you set the permissions? I used the add-mailboxpermission... command for every operation. (accessrights fullaccess). It's working for me. (or you can use the impersonation... :) )

If i would like to set anything, then the error response is the same. But if i run query (for example: Get-Exchangeserver), then it's working fine.

Thanks,

Mahesh said...

Nicek, this is a fantastic page for people who are struggling with exchange shell commands. Thank you. I have a problem I need to address:

I am writing C# code to execute the Move-DatabasePath and Move-StorageGroupPath commands programmatically. They look like this:

Move-DatabasePath –Identity ‘MAIL2K7\First Storage Group\Mailbox Database’ –EdbFilePath ‘X:\MailStore\Mailbox Database.edb’ –Confirm:$False

Move-StorageGroupPath ‘MAIL2K7\First Storage Group’ –LogFolderPath ‘X:\MailStore’ –SystemFolderPath ‘X:\MailStore’ –ConfigurationOnly –Confirm:$False

The code I have is as follows:

Command myCommand = new Command(strCommand);
myCommand.Parameters.Add("Identity", strIdentity);
myCommand.Parameters.Add("EdbFilePath", strEDBFilePath);
myCommand.Parameters.Add("Confirm", false);
pipeLine.Commands.Add(myCommand);
ICollection< PSObject > results = pipeLine.Invoke();

When I run this, I get an exception: “System.Management.Automation.CmdletInvocationException: Cannot invoke this function because the current host does not implement it. ---> System.Management.Automation.Host.HostException: Cannot invoke this function because the current host does not implement it.”

I think it is because of the ‘Confirm’ parameter. These commands ask for confirmation, and I think the pipeline doesn’t accept confirm programmatically.

How can I programmatically send the Confirm -> False argument?

If you could help me with this, that would be awesome.

Nick Smith said...

Mahesh,

After playing around with the Move-DatabasePath cmdlet I found that you need both the -Confirm:$False and -Force parameters to avoid a confirmaion prompt (there are actually two confirmation prompts when running this command). I was able to get the code to run with the following commands:

string strCommand = "Move-DatabasePath";
string strIdentity = @"EX2007ALL\First Storage Group\Mailbox Database";
string strEDBFilePath = @"C:\Exchange\Databases\Mailbox Database.edb";

Command myCommand = new Command(strCommand);
myCommand.Parameters.Add("Identity", strIdentity);
myCommand.Parameters.Add("EdbFilePath", strEDBFilePath);
myCommand.Parameters.Add("OutVariable", "$Status");
myCommand.Parameters.Add("Force");
myCommand.Parameters.Add("Confirm", false);
results = ems.PipelineInvoke(myCommand);

--Nick

Anonymous said...

Really great this what I wanted to know but still I am facing a issue that I am unable to execute any of the exchange relalted cmdlets .when ever I try to run the code it says
Unhandled Exception: System.Management.Automation.CommandNotFoundException: The
term 'Get-excommand' is not recognized as a cmdlet, function, operable program,
or script file. Verify the term and try again.
at System.Management.Automation.CommandDiscovery.LookupCommandInfo(String com
mandName)
at System.Management.Automation.CommandDiscovery.LookupCommandProcessor(Strin
g commandName, CommandOrigin commandOrigin)
at System.Management.Automation.CommandFactory._CreateCommand(String commandN
ame, CommandOrigin commandOrigin)
at System.Management.Automation.CommandFactory.CreateCommand(String commandNa
me, CommandOrigin commandOrigin)
at System.Management.Automation.Runspaces.Command.CreateCommandProcessor(Exec
utionContext executionContext, CommandFactory commandFactory, Boolean addToHisto
ry)
at System.Management.Automation.Runspaces.LocalPipeline.CreatePipelineProcess
or()
at System.Management.Automation.Runspaces.LocalPipeline.InvokeHelper()
at System.Management.Automation.Runspaces.LocalPipeline.InvokeThreadProc()

Candi said...

Nick,
This is great! My code is executing in one domain. I need to create a mailbox in another domain. I'm running as an impersonated domain admin in the domain I'm trying to create the mailbox in. Using Exchange Management Shell I set default scope in $adminsessionADSettings to the domain I want to create the
mailbox in. I also set the configuration domain controller to a domain controller in that domain. Then I use the enable-mailbox command.

My question is: How do I duplicate setting $adminsessionADSettings in C#?

Thanks,
Candi

Anonymous said...

Hi Nick,

Great example. But, I am not able to execute simple Get-Mailboxstatistics command though it works find through powershell.

Any ideas why ?

Thanks
John

Anonymous said...

Nick,
In your code you assume that the Identity parameter represents the sAMAccountName property of the user object. Your assumption is not completely wrong, the parameter must be specified differently though e.g. as DOMAIN\sAMAccountName.
Only in this way you can be sure the query for that user will search for the sAMAccountName, and not the name property instead.

Greetz,

Mike

Joseph said...

I love your code and it works great for everything I need to do. However I was wondering if it was possible to specify creds for this. In our enviroment our daily accounts are limited and we have alt accounts we use for privalaged things. So when debuging my normal account isn't an exchange admin so i get the following error, "Access to the address list service on all Exchange 2007 servers has been denied." however when I run Visual Stuido or my project as exchange admin i'm able to complete is there any way I can hardcode Creds into the connection?

dattaforit said...

Hello Nick,

Very good article. Thanks.

I have to dismount the database using the sample code you provided. What should be the cmdlet to be fired, so the confirmation is not asked. i have following code snippet

Command EMSMntCommand = new Command("Dismount-Database");
EMSMntCommand.Parameters.Add("Identity", prop2.Value.ToString());
EMSMntCommand.Parameters.Add("Confirm", "FALSE");

Here prop2.value.tostring is "Mailbox Database" //the default database available.

When i run the code snippet i am getting exception as

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

Any help would be appreciated.

Thank you so much

DK

shravan said...

Hi Nick,
Very nice article.I have one task like i want to execute a command "get-help add-content -detailed" in c# using runspaces and pipeline and get the help details.
After invoking the pipeline i would be getting a PSObject for which i have to create nested loops and get the relevant help details like syntax,examples and related links.
My question is do we have an option to get the whole help details as a single string.
something like ps.properties["Text"] should give all the help details. Thanks in advance

Tyler said...

Does anyone know if we can set default scope or other $adminsessionADSettings variables in the runspace before we execute a cmdlet? It would be very helpful to know. Thanks,
Tyler

Seema said...

Hi Nick,

Nice Article.
I have a question and need your help.
I am executing the cmdlets using C# code.

rc = RunspaceConfiguration.Create();
info = rc.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out snapEx);
r = RunspaceFactory.CreateRunspace(rc);
r.Open();
ri = new RunspaceInvoke(r);

I am executing ret = ri.Invoke(sCmd, null, out errors);
where sCmd = "Get-Queue"

I get the following error in my application log
RunspaceInvoke.Invoke returned error
Invoke error: The Queue Viewer operation on computer "localhost" has failed with an exception. The error message is: Access is denied

I am able to Run the cmdlet in Exchange Management Shell and get the available queues.

Using the same code I am able to run cmd-lets like get-ADSiteLink.
But Get-Queue fails.

Can I some way debug/log the powershell activity?

Please help me.

Regards,
Seema

Narfix said...

Hello !!

I use your class in one of my projects and it seems the initialisation fails to work when called in a thread.

You can find more info on the problem here : http://stackoverflow.com/questions/1013331/powershell-runspace-in-a-thread

Thanks for your class !!!!

Anonymous said...

Hi Nick,

great post

why must it be a singleton? i have tried to do it non-singleton, and it works the same...

regards
Pilu

The Iguana said...

This is a great tool. How about Exchange 2010? The PowerShell interface is very different... (grrr) - could you provide an equivalent class that uses the Ex2010 PS interface?

Anonymous said...

I agree with "The Iguana"! how do you manage exchange 2010 with c# code???

Laeeq Qazi said...

Hi,

For Exchange 2010 you can use same code and just have to change this

Microsoft.Exchange.Management.PowerShell.Admin

to this

Microsoft.Exchange.Management.PowerShell.E2010

I have tested it successfully.

Regards,

Draek said...

I have Windows 7 64-bit installed, Exchange 2007 SP2 installed (with supposed PowerShell 2.0 support) and Visual Studio 2008.

I'm working on a ASP.NET 2.0 application.

When I try and do the same thing, I get "No powershell snap-in's available for v2" (paraphrased)

When I run Exchange Management Powershell tool, and I list the snapins it says that the Microsoft.Exchange.Management.Powershell.Admin snap in is PSVersion 1.0 and the Microsoft.Exchange.Management.Powershell.Support snapin is PSVersion 2.0.

WHY IS THE ADMIN STUFF NOT VERSION 2.0?!

Darren said...

Excellent work on this one. Question, how could the following be called:
Get-MailboxStatistics -Identity foo | fl totalItemSize

Alex said...

There were mant troubles, when I lost my Exchange info. Luckily with the help of one program I resolved it. This issue might determine as well as mine - microsoft exchange email recovery.

Anonymous said...

Hi Nick,
this looks great, I'm just battling to find out how to use this to generate a new alias on an existing mailbox instead of creating a new mailbox.

Himanshu Bhargav said...

Hi Nick,

I need some help to integrate Exchange Server 2007 using C#.

Currently I am doing on my Testing machine which is 32 bit Windows XP(SP3) with Visual Studio 2010. I am using Windows Powershell 1.0 that already shipped with windows.

I am using the sample code that you have mentioned in your second post but unfortunality getting error ""Microsoft.Exchange.Management.PowerShell.Admin " is not installed on my machine. Please advice what all prerequiste i need to install on my local machine to test the integration.

Thanks & Regards
Himanshu Bhargav

Misha N. said...

Hi Nick,

I really like your article. Helped me a lot to understand how I should organize powershell calls in my app.

I see that you keep ExchangeManagementShellWrapper instance in static field. Do you think if ExchangeManagementShellWrapper is hosted in web application that it will be thread safe?
Right now, I create Runspace instance for each web request.

Thanks,
Miroslav

Anonymous said...

For those who are looking for Impersonation, see this post

http://stackoverflow.com/questions/7527469/error-when-using-impersonation-with-powershell-and-exchange-2007-from-c-sharp

Aczire

B said...

How can you do this with impersonation?

I've tried a couple of ways and they all fail with impersonation, but work fine without it.

Mohd Azam said...

I am trying to execute the following exchange management shell cmdlets using C# in order to get total number of mailbox on the server.

cmdlets:-

Get-mailbox -resultsize unlimited
My code snippet is as following

PSCredential credential = new PSCredential("Administrator", securePassword); // the password must be of type SecureString
WSManConnectionInfo connectionInfo = new WSManConnectionInfo(connectTo,schemaURI, credential);
connectionInfo.MaximumConnectionRedirectionCount = 5;
connectionInfo.SkipCACheck = true;
connectionInfo.SkipCNCheck = true;

try
{
Runspace remoteRunspace = RunspaceFactory.CreateRunspace(connectionInfo);
remoteRunspace.Open();
var command = new Command("Get-mailbox");
command.Parameters.Add("resultsize", "unlimited");
var pipeline = remoteRunspace.CreatePipeline();
pipeline.Commands.Add(command);
// Execute the command
var results = pipeline.Invoke();
MessageBox.Show(results.Count.ToString());
remoteRunspace.Dispose();
}
catch (Exception ex)
{
//Handle error
}

The above code gives the desired result i.e. total number of mailboxes.But how can i select some properties of all the mailboxes i.e. how can i execute the following cmdlets

cmdlets:


Get-mailbox | select-object DisplayName, PrimarySmtpAddress, ForwardingAddress, alias, identity, legacyexchangeDN | where-object {$_.ForwardingAddress -ne $Null}

Please guid, how can i execute the above given cmdlets...
Thank you