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:

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: ErrorEvent Source: MSExchange ADAccessEvent Category: GeneralEvent ID: 2152Date: 3/26/2007Time: 7:38:52 PMUser: N/AComputer: HASTYDescription: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.

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.

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 = newCommand("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.

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.

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

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

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.

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?

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.

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.

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)

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 adjusted2) 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.

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.

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???

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!

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.

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.

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.

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

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.

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!

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?

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.

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:

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, 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!)

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.

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

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 !

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.

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:

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 saysUnhandled Exception: System.Management.Automation.CommandNotFoundException: Theterm '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 commandName) at System.Management.Automation.CommandDiscovery.LookupCommandProcessor(String commandName, CommandOrigin commandOrigin) at System.Management.Automation.CommandFactory._CreateCommand(String commandName, CommandOrigin commandOrigin) at System.Management.Automation.CommandFactory.CreateCommand(String commandName, CommandOrigin commandOrigin) at System.Management.Automation.Runspaces.Command.CreateCommandProcessor(ExecutionContext executionContext, CommandFactory commandFactory, Boolean addToHistory) at System.Management.Automation.Runspaces.LocalPipeline.CreatePipelineProcessor() at System.Management.Automation.Runspaces.LocalPipeline.InvokeHelper() at System.Management.Automation.Runspaces.LocalPipeline.InvokeThreadProc()

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#?

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.

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?

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

I get the following error in my application logRunspaceInvoke.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.

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.

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.

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.