Creating an Exchange 2010 Mailbox from a remote C# program

On this post, I’ll show you how to create an Exchange Mailbox from a C# program that is not running on the Exchange Server (a client program). What C# is really doing is remotely executing Exchange PowerShell cmdlets.

If this is the first time you’re attempting to run Exchange cmdlets from C#, you probably want to follow this blog post to make sure things are set up properly on the server and that C# can run Exchange cmdlets when running from the server.

This is the client’s code. But it doesn’t “just work”, you need to complete the setup instructions on this post.

using System;
using System.Security;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

namespace PowerShellTest
{
    class Program
    {
        static void Main(string[] args)
        {

            // Prepare the credentials that will be used when connecting
            // to the server. More info on the user to use on the notes
            // below this code snippet.
            string runasUsername = @"username";
            string runasPassword = "password";
            SecureString ssRunasPassword = new SecureString();
            foreach (char x in runasPassword)
                ssRunasPassword.AppendChar(x);
            PSCredential credentials =
                new PSCredential(runasUsername, ssRunasPassword);

            // Prepare the connection
            var connInfo = new WSManConnectionInfo(
                new Uri("http://ServersIpAddress/PowerShell"),
                "http://schemas.microsoft.com/powershell/Microsoft.Exchange",
                credentials);
            connInfo.AuthenticationMechanism =
                AuthenticationMechanism.Basic;

            // Create the runspace where the command will be executed
            var runspace = RunspaceFactory.CreateRunspace(connInfo);

            // generate the command parameters
            var testNumber = 18;
            var firstName = "Test";
            var lastName = "User" + testNumber;
            var username = "tuser" + testNumber;
            var domainName = "pedro.test.local";
            var password = "ActiveDirectoryPassword1234";
            var ssPassword = new SecureString();
            foreach (char c in password)
                ssPassword.AppendChar(c);

            // create the PowerShell command
            var command = new Command("New-Mailbox");
            command.Parameters.Add("Name", firstName + " " + lastName);
            command.Parameters.Add("Alias", username);
            command.Parameters.Add(
                "UserPrincipalName", username + "@" + domainName);
            command.Parameters.Add("SamAccountName", username);
            command.Parameters.Add("FirstName", firstName);
            command.Parameters.Add("LastName", lastName);
            command.Parameters.Add("Password", ssPassword);
            command.Parameters.Add("ResetPasswordOnNextLogon", false);
            command.Parameters.Add(
                "OrganizationalUnit", "NeumontStudents");

            // Add the command to the runspace's pipeline
            runspace.Open();
            var pipeline = runspace.CreatePipeline();
            pipeline.Commands.Add(command);

            // Execute the command
            var results = pipeline.Invoke();

            runspace.Dispose();

            if (results.Count > 0)
                Console.WriteLine("SUCCESS");
            else
                Console.WriteLine("FAIL");

        }
    }
}

Some important things to notice on the code:

  • The “runas” user must:
    1. Belong to a Role Group that has Mail Recipient Creation rights. To do this, make the runas user belong to the “Recipient Management” Role Goup by going to “Exchange Management Console > Microsoft Exchange > Microsoft Exchange On-Premises > Toolbox > Role Based Access Control (RBAC) User Editor”.
    2. Must have Remote PowerShell rights. Do this by going to the Exchange Management Shell and running the following cmdlet: Set-User UserNameHere -RemotePowerShellEnabled:$true
  • It works using Basic authentication, which means that the “runas” credentials are being sent in clear text over the network. This is ok if you trust the network (which is my case because it never leaves the server room) or if you set up SSL.
  • Because of the parameters sent to the New-Mailbox cmdlet, besides creating an Exchange mailbox, I’m also creating an Active Directory user.

You need to configure WinRM on the client to:

  1. Allow unencrypted traffic
  2. Trust the remote machine

The easiest way to to this is via the Local Group Policy user interface. To access it, hit the windows start button, type run, run the “run” program, this opens the famous “run” window (which is wired to the Windows Key + R shortcut), on the run window enter gpedit.msc . You’re now looking at the Local Group Policy user interface. In the tree view go to “Local Computer Policy > Computer Configuration > Administrative Templates > Windows Components > Windows Remote Management (WinRM) > WinRM Client” and configure the previously listed items.

If you don’t configure the WinRM client, you’ll get these exceptions:

System.Management.Automation.Remoting.PSRemotingTransportException was unhandled
  Message=Connecting to remote server failed with the following error message : The WinRM client cannot process the request. Unencrypted traffic is currently disabled in the client configuration. Change the client configuration and try the request again. For more information, see the about_Remote_Troubleshooting Help topic.
  Source=System.Management.Automation
  WasThrownFromThrowStatement=false
  ErrorCode=-2144108322
  TransportMessage=The WinRM client cannot process the request. Unencrypted traffic is currently disabled in the client configuration. Change the client configuration and try the request again.
System.Management.Automation.Remoting.PSRemotingTransportException was unhandled
  Message=Connecting to remote server failed with the following error message : The WinRM client cannot process the request. If the authentication scheme is different from Kerberos, or if the client computer is not joined to a domain, then HTTPS transport must be used or the destination machine must be added to the TrustedHosts configuration setting. Use winrm.cmd to configure TrustedHosts. Note that computers in the TrustedHosts list might not be authenticated. You can get more information about that by running the following command: winrm help config. For more information, see the about_Remote_Troubleshooting Help topic.
  Source=System.Management.Automation
  WasThrownFromThrowStatement=false
  ErrorCode=-2144108316
  TransportMessage=The WinRM client cannot process the request. If the authentication scheme is different from Kerberos, or if the client computer is not joined to a domain, then HTTPS transport must be used or the destination machine must be added to the TrustedHosts configuration setting. Use winrm.cmd to configure TrustedHosts. Note that computers in the TrustedHosts list might not be authenticated. You can get more information about that by running the following command: winrm help config.

On the server, you must configure the PowerShell IIS virtual directory to:

  1. Not Require SSL
  2. Allow Basic Authentication

To not require SSL, you simply go to “IIS Manager > Sites > Default Website > Powershell”, then select the “SSL Settings” feature and make sure “Require SSL” is not checked.

To allow Basic Authentication you go again to “IIS Manager > Sites > Default Website > Powershell”, this time select the “Authentication” feature and enable “Basic Authentication”.

If Basic Authentication is not an option on the Authentication feature page, you need to install it by going to the Server Manager, select the Web Server role, say “Add Role Services”, under the Security node in the treeview, select Basic Authentication.

You’re all set! Running the C# code on a remote machine should work.

Advertisements

Running Exchange 2010 Management Shell Commands (PowerShell) with C#

The goal is to be able to run C# code on the Exchange server and show that we’ve have all the Microsoft Exchange 2010 PowerShell commands (cmdlets) available.

If what you want to do is execute the C# code on a client machine that executes cmdlets on the Exchange server, check out this post.

This is the code that we want to run on the server:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

namespace PowerShellTest
{
    class Program
    {
        static void Main(string[] args)
        {

            var rsConfig = RunspaceConfiguration.Create();
            PSSnapInException snapInException;

            // NOTE 1: The project's platform target must match the server's hardware architecture (x64 in my case)
            var snapinInfo = rsConfig.AddPSSnapIn("Microsoft.Exchange.Management.PowerShell.E2010", out snapInException);

            var runspace = RunspaceFactory.CreateRunspace(rsConfig);

            runspace.Open();
            var pipeline = runspace.CreatePipeline();
            var command = new Command("get-command");
            pipeline.Commands.Add(command);

            // NOTE 2: Your code cannot be running the .NET Framework 4 .  3.5 or lower is ok.
            var results = pipeline.Invoke();

            foreach (var cmd in results)
            {
                string cmdletName = cmd.Properties["Name"].Value.ToString();
                Console.WriteLine(cmdletName);
            }

            runspace.Dispose();

        }
    }
}

Output:

%
?
A:
ac
Add-ADPermission
Add-AvailabilityAddressSpace
Add-Computer
Add-Content
Add-ContentFilterPhrase
Add-DatabaseAvailabilityGroupServer
Add-DistributionGroupMember
Add-FederatedDomain
Add-History
...
Enable-Mailbox
New-Mailbox
...

Now to the details on how to make the code work:

You first must install the Windows PowerShell 2.0 SDK, mostly to be able to reference the System.Management.Automation.dll . For testing purposes, I installed Visual Studio on the server running Exchange (which is a test server). My ultimate goal (and maybe future post) is to run the C# code on a remote machine.

If you already have PowerShell 2.0 on your machine (which Windows 7 and Windows Sever 2008 R2 already do) I ran into some posts that said you could manually edit your .csproj file and add a reference to System.Management.Automation . This is how I initially did my tests but then hell broke loose (not sure if this was the cause of the problems) so I followed the formal rules and installed the SDK.

Then create the Console Application in Visual Studio and add a reference to the System.Management.Automation.dll . If you installed the SDK with the default options, you will find the dll here:

image

Copy paste the C# code above into your Program.cs file. If you build and run the code, you will most likely run into two strange errors; they will both happen right below my comment lines that start with NOTE. I use Visual Studio .NET 2010 which by default uses the .NET Framework 4 and for some reason the default project’s platform target is 32 bit (x86). Both of these defaults turned out to be a problem.

NOTE 1: 

I was getting the a “No snap-ins have been registered for Windows PowerShell version 2” error on that line:

System.Management.Automation.PSArgumentException was unhandled
  Message=No snap-ins have been registered for Windows PowerShell version 2.
  Source=System.Management.Automation
  ParamName=psVersion
  StackTrace:
       at System.Management.Automation.PSSnapInReader.GetMshSnapinRootKey(RegistryKey versionRootKey, String psVersion)
       at System.Management.Automation.PSSnapInReader.Read(String psVersion, String mshsnapinId)
       at System.Management.Automation.Runspaces.MshConsoleInfo.AddPSSnapIn(String mshSnapInID)
       at System.Management.Automation.Runspaces.RunspaceConfigForSingleShell.DoAddPSSnapIn(String name, PSSnapInException& warning)
       at System.Management.Automation.Runspaces.RunspaceConfiguration.AddPSSnapIn(String name, PSSnapInException& warning)
       at PowerShellTest.Program.Main(String[] args) in C:\Users\Administrator\Desktop\PowerShellTest\PowerShellTest\blog.cs:line 20
       at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException:

To fix it I just made the project’s Platform Target for All Configurations be x64.

image

NOTE 2:

I was getting a “Mixed mode assembly is built against version ‘v2.0.50727’ of the runtime and cannot be loaded in the 4.0 runtime without additional configuration information.” error on that line:

System.Management.Automation.CmdletInvocationException was unhandled
  Message=The type initializer for 'Microsoft.Exchange.Configuration.Tasks.Task' threw an exception.
  Source=System.Management.Automation
  WasThrownFromThrowStatement=false
  StackTrace:
       at System.Management.Automation.CommandProcessor.Init(CmdletInfo cmdletInformation)
       at System.Management.Automation.CommandInfo.GetMergedCommandParameterMetdata()
       at System.Management.Automation.CommandInfo.get_ParameterSets()
       at Microsoft.PowerShell.Commands.GetCommandCommand.AccumulateMatchingCommands(Collection`1 commandNames)
       at System.Management.Automation.CommandProcessor.ProcessRecord()
  InnerException: System.TypeInitializationException
       Message=The type initializer for 'Microsoft.Exchange.Configuration.Tasks.Task' threw an exception.
       Source=Microsoft.Exchange.Configuration.ObjectModel
       TypeName=Microsoft.Exchange.Configuration.Tasks.Task
       StackTrace:
            at Microsoft.Exchange.Configuration.Tasks.Task.AssemblyResolveEventHandler(Object sender, ResolveEventArgs args)
            at System.AppDomain.OnAssemblyResolveEvent(RuntimeAssembly assembly, String assemblyFullName)
       InnerException: System.IO.FileLoadException
            Message=Mixed mode assembly is built against version 'v2.0.50727' of the runtime and cannot be loaded in the 4.0 runtime without additional configuration information.
            Source=Microsoft.Exchange.Data.Directory
            StackTrace:
                 at Microsoft.Exchange.Data.Directory.DSAccessTopologyProvider..ctor(String machineName)
                 at Microsoft.Exchange.Data.Directory.DSAccessTopologyProvider..ctor()
                 at Microsoft.Exchange.Data.Directory.DirectoryServicesTopologyProvider.DiscoverConfigDC()
                 at Microsoft.Exchange.Data.Directory.DirectoryServicesTopologyProvider..ctor()
                 at Microsoft.Exchange.Data.Directory.TopologyProvider.InitializeInstance()
                 at Microsoft.Exchange.Data.Directory.TopologyProvider.GetInstance()
                 at Microsoft.Exchange.Data.Directory.ADSession.GetConnection(String preferredServer, Boolean isWriteOperation, Boolean isNotifyOperation, String optionalBaseDN, ADObjectId& rootId, ADScope scope)
                 at Microsoft.Exchange.Data.Directory.ADSession.GetReadConnection(String preferredServer, String optionalBaseDN, ADObjectId& rootId, ADRawEntry scopeDeteriminingObject)
                 at Microsoft.Exchange.Data.Directory.ADSession.Find(ADObjectId rootId, String optionalBaseDN, ADObjectId readId, QueryScope scope, QueryFilter filter, SortBy sortBy, Int32 maxResults, IEnumerable`1 properties, CreateObjectDelegate objectCreator, CreateObjectsDelegate arrayCreator, Boolean includeDeletedObjects)
                 at Microsoft.Exchange.Data.Directory.ADSession.Find(ADObjectId rootId, QueryScope scope, QueryFilter filter, SortBy sortBy, Int32 maxResults, IEnumerable`1 properties, CreateObjectDelegate objectCtor, CreateObjectsDelegate arrayCtor)
                 at Microsoft.Exchange.Data.Directory.ADSession.Find[TResult](ADObjectId rootId, QueryScope scope, QueryFilter filter, SortBy sortBy, Int32 maxResults, IEnumerable`1 properties)
                 at Microsoft.Exchange.Data.Directory.SystemConfiguration.ADSystemConfigurationSession.Find[TResult](ADObjectId rootId, QueryScope scope, QueryFilter filter, SortBy sortBy, Int32 maxResults)
                 at Microsoft.Exchange.Data.Directory.SystemConfiguration.ADSystemConfigurationSession.FindServerByFqdn(String serverFqdn)
                 at Microsoft.Exchange.Data.Directory.SystemConfiguration.ADSystemConfigurationSession.FindLocalServer()
                 at Microsoft.Exchange.Configuration.SQM.CmdletSqmSession.GetOptInStatus()
                 at Microsoft.Exchange.Configuration.SQM.SqmSession.UpdateData(Boolean flushToDisk, Boolean closing)
                 at Microsoft.Exchange.Configuration.SQM.SqmSession.OnCreate()
                 at Microsoft.Exchange.Configuration.SQM.SqmSession.Open()
                 at Microsoft.Exchange.Configuration.SQM.CmdletSqmSession..ctor()
                 at Microsoft.Exchange.Configuration.SQM.CmdletSqmSession.get_Instance()
                 at Microsoft.Exchange.Configuration.Tasks.Task..cctor()
            InnerException:

To fix it I made the project’s Target Framework be the .NET Framework 3.5.

image

I mostly followed the instructions on this article, but they were made for Exchange 2007.

Adding the row number to an ASP.NET GridView

I often want my GridViews to have the first column just be a number identifying the row. I see this beneficial because:

  • The user can see the number of rows returned by scrolling to the end of the GridView
  • If there are two people looking at the GridView, they can say “look at item #5 …”

image

To make this happen you need to add this element to the <columns> element of your GridView:

 
<asp:GridView ...>
    <Columns> 
        <asp:TemplateField> 
            <ItemTemplate> 
                <%# Container.DataItemIndex + 1 %>. 
            </ItemTemplate> 
            <ItemStyle HorizontalAlign="Right" /> 
        </asp:TemplateField> 
        ... 
    </Columns> 
    ... 
</asp:GridView>	

This looks weird if paging is on. But you can tweak the math to make it work right.