It all started with a customer who wanted to know which servers in the DAG setup have the copies for a specific Exchange database. There were other properties like Activation Preferences that he was interested in too.
The easiest way to get the details about the copies for a database is by using the command (Get-MailboxDatabase "Database Name").DatabaseCopies. When I did this on the CAS Server using Exchange Management Shell, I got the following output:
[PS] C:\Windows\system32>$copies = (Get-MailboxDatabase "Mailbox Database 1056078029").DatabaseCopies[PS] C:\Windows\system32>$copiesDatabaseName : Mailbox Database 1056078029HostServerName : AKAS23474121ActivationPreference : 1ParentObjectClass : msExchPrivateMDBReplayLagTime : 00:00:00TruncationLagTime : 00:00:00AdminDisplayName :ExchangeVersion : 0.10 (14.0.100.0)DistinguishedName : CN=AKAS23474121,CN=Mailbox Database 1056078029,CN=Databases,CN=Exchange Administrative Group (FY DIBOHF23SPDLT),CN=Administrative Groups,CN=First Organization,CN=Microsoft Exchange,CN=Services, CN=Configuration,DC=DOM234741,DC=LOCALIdentity : Mailbox Database 1056078029\AKAS23474121Guid : 562cb271-7cee-4130-9582-e3ce0f08cb3dObjectCategory : DOM234741.LOCAL/Configuration/Schema/ms-Exch-MDB-CopyObjectClass : {top, msExchMDBCopy}WhenChanged : 7/21/2011 4:29:06 PMWhenCreated : 7/21/2011 4:29:06 PMWhenChangedUTC : 7/21/2011 10:59:06 AMWhenCreatedUTC : 7/21/2011 10:59:06 AMOrganizationId :OriginatingServer : AKAS23474118.DOM234741.LOCALIsValid : TrueDatabaseName : Mailbox Database 1056078029HostServerName : AKAS23474120ActivationPreference : 2ParentObjectClass : msExchPrivateMDBReplayLagTime : 00:00:00TruncationLagTime : 00:00:00AdminDisplayName :ExchangeVersion : 0.10 (14.0.100.0)...
[PS] C:\Windows\system32>$copies = (Get-MailboxDatabase "Mailbox Database 1056078029").DatabaseCopies[PS] C:\Windows\system32>$copiesDatabaseName : Mailbox Database 1056078029HostServerName : AKAS23474121ActivationPreference : 1ParentObjectClass : msExchPrivateMDBReplayLagTime : 00:00:00TruncationLagTime : 00:00:00AdminDisplayName :ExchangeVersion : 0.10 (14.0.100.0)DistinguishedName : CN=AKAS23474121,CN=Mailbox Database 1056078029,CN=Databases,CN=Exchange Administrative Group (FY DIBOHF23SPDLT),CN=Administrative Groups,CN=First Organization,CN=Microsoft Exchange,CN=Services, CN=Configuration,DC=DOM234741,DC=LOCALIdentity : Mailbox Database 1056078029\AKAS23474121Guid : 562cb271-7cee-4130-9582-e3ce0f08cb3dObjectCategory : DOM234741.LOCAL/Configuration/Schema/ms-Exch-MDB-CopyObjectClass : {top, msExchMDBCopy}WhenChanged : 7/21/2011 4:29:06 PMWhenCreated : 7/21/2011 4:29:06 PMWhenChangedUTC : 7/21/2011 10:59:06 AMWhenCreatedUTC : 7/21/2011 10:59:06 AMOrganizationId :OriginatingServer : AKAS23474118.DOM234741.LOCALIsValid : True
DatabaseName : Mailbox Database 1056078029HostServerName : AKAS23474120ActivationPreference : 2ParentObjectClass : msExchPrivateMDBReplayLagTime : 00:00:00TruncationLagTime : 00:00:00AdminDisplayName :ExchangeVersion : 0.10 (14.0.100.0)...
PS C:\Users\Superman> $copies=(Get-MailboxDatabase "Mailbox Database 1056078029").DatabaseCopiesPS C:\Users\Superman> $copiesMailbox Database 1056078029\AKAS23474121Mailbox Database 1056078029\AKAS23474120
I then did a Get-Member on the $copies variable first on the local CAS box and below is what I see
[PS] C:\Windows\system32>$copies | Get-Member -MemberType Property TypeName: Microsoft.Exchange.Data.Directory.SystemConfiguration.DatabaseCopyName MemberType Definition---- ---------- ----------ActivationPreference Property System.Int32 ActivationPreference {get;}AdminDisplayName Property System.String AdminDisplayName {get;}DatabaseName Property System.String DatabaseName {get;}DistinguishedName Property System.String DistinguishedName {get;}ExchangeVersion Property Microsoft.Exchange.Data.ExchangeObjectVersion ExchangeVersion {get;}Guid Property System.Guid Guid {get;}HostServerName Property System.String HostServerName {get;}Identity Property Microsoft.Exchange.Data.ObjectId Identity {get;}IsValid Property System.Boolean IsValid {get;}ObjectCategory Property Microsoft.Exchange.Data.Directory.ADObjectId ObjectCategory {get;}...
[PS] C:\Windows\system32>$copies | Get-Member -MemberType Property TypeName: Microsoft.Exchange.Data.Directory.SystemConfiguration.DatabaseCopyName MemberType Definition---- ---------- ----------ActivationPreference Property System.Int32 ActivationPreference {get;}AdminDisplayName Property System.String AdminDisplayName {get;}DatabaseName Property System.String DatabaseName {get;}DistinguishedName Property System.String DistinguishedName {get;}ExchangeVersion Property Microsoft.Exchange.Data.ExchangeObjectVersion ExchangeVersion {get;}Guid Property System.Guid Guid {get;}HostServerName Property System.String HostServerName {get;}Identity Property Microsoft.Exchange.Data.ObjectId Identity {get;}IsValid Property System.Boolean IsValid {get;}ObjectCategory Property Microsoft.Exchange.Data.Directory.ADObjectId ObjectCategory {get;}
...
When I do Get-Member on the $copies variable on the Remote machine below is what I see
PS C:\Users\Superman> $copies | Get-Member -MemberType Properties TypeName: System.StringName MemberType Definition---- ---------- ----------Length Property System.Int32 Length {get;}
Notice the difference in TypeName. On the Local box it is “Microsoft.Exchange.Data.Directory.SystemConfiguration.DatabaseCopy” and on the Remote box it is “System.String”. Why does this happen?
When you run remote commands that generate output, the command output is transmitted across the network back to the local computer. Because most live Microsoft .NET Framework objects (such as the objects that Windows PowerShell cmdlets return) cannot be transmitted over the network, the live objects are "serialized". In other words, the live objects are converted into XML representations of the object and its properties. Then, the XML-based serialized object is transmitted across the network. On the local computer, Windows PowerShell receives the XML-based serialized object and "deserializes" it by converting the XML-based object into a standard .NET Framework object. However, the deserialized object is not a live object. It is a snapshot of the object at the time that it was serialized. This is how an MSDN article defines serialization in .NET framework. More details can be found in the article “How objects are sent to and from remote sessions”.
Luckily the data that we needed was available buy just using the Get-MailboxDatabase cmd-let by accessing the DatabaseCopies Property. To prove what is said above and show how to do it in C#, below is the code that I wrote:
using System;using System.Collections.Generic;using System.Text;using System.Management.Automation;using System.Management.Automation.Runspaces;using System.Management.Automation.Remoting;using System.Collections.ObjectModel;using System.Security;using System.Collections;namespace CallingPSCmdlet{ class Program { static void Main(string[] args) { string password = "Passowrd"; string userName = "Domain\\UserName"; System.Uri uri = new Uri(“http://CAS-SERVER/powershell?serializationLevel=Full”); System.Security.SecureString securePassword = String2SecureString(password); System.Management.Automation.PSCredential creds = new System.Management.Automation.PSCredential(userName, securePassword); Runspace runspace = System.Management.Automation.Runspaces.RunspaceFactory.CreateRunspace(); PowerShell powershell = PowerShell.Create(); PSCommand command = new PSCommand(); command.AddCommand("New-PSSession"); command.AddParameter("ConfigurationName", "Microsoft.Exchange"); command.AddParameter("ConnectionUri", uri); command.AddParameter("Credential", creds); command.AddParameter("Authentication", "Default"); PSSessionOption sessionOption = new PSSessionOption(); sessionOption.SkipCACheck = true; sessionOption.SkipCNCheck = true; sessionOption.SkipRevocationCheck = true; command.AddParameter("SessionOption", sessionOption); powershell.Commands = command; try { // open the remote runspace runspace.Open(); // associate the runspace with powershell powershell.Runspace = runspace; // invoke the powershell to obtain the results Collection<PSSession> result = powershell.Invoke<PSSession>(); foreach (ErrorRecord current in powershell.Streams.Error) Console.WriteLine("The following Error happen when opening the remote Runspace: " + current.Exception.ToString() + " | InnerException: " + current.Exception.InnerException); if (result.Count != 1) throw new Exception("Unexpected number of Remote Runspace connections returned."); // Set the runspace as a local variable on the runspace powershell = PowerShell.Create(); command = new PSCommand(); command.AddCommand("Set-Variable"); command.AddParameter("Name", "ra"); command.AddParameter("Value", result[0]); powershell.Commands = command; powershell.Runspace = runspace; powershell.Invoke(); // First import the cmdlets in the current runspace (using Import-PSSession) command = new PSCommand(); command.AddScript("Import-PSSession -Session $ra"); powershell.Commands = command; powershell.Runspace = runspace; powershell.Invoke(); // Now call the Get-MaiboxDatabase command = new PSCommand(); command.AddCommand("Get-MailboxDatabase"); //Change the name of the database command.AddParameter("Identity", "Mailbox Database XXXXXXXX"); powershell.Commands = command; powershell.Runspace = runspace; Collection<PSObject> results = new Collection<PSObject>(); results = powershell.Invoke(); PSMemberInfo Member = null; foreach (PSObject oResult in results) { foreach (PSMemberInfo psMember in oResult.Members) { Member = psMember; DumpProperties(ref Member); } } results = null; Member = null; } finally { // dispose the runspace and enable garbage collection runspace.Dispose(); runspace = null; // Finally dispose the powershell and set all variables to null to free up any resources. powershell.Dispose(); powershell = null; } } //Method to Dump out the Properties public static void DumpProperties(ref PSMemberInfo psMember) { // Only look at Properties if (psMember.MemberType.ToString() == "Property") { switch (psMember.Name) { case "ActivationPreference": case "DatabaseCopies": if (psMember.Value != null) { PSObject oPSObject; ArrayList oArrayList; oPSObject = (PSObject)psMember.Value; oArrayList = (ArrayList)oPSObject.BaseObject; Console.WriteLine("Member Name:" + psMember.Name); Console.WriteLine("Member Type:" + psMember.TypeNameOfValue); Console.WriteLine("----------------------"); foreach (string item in oArrayList) { Console.WriteLine(item); } Console.WriteLine("----------------------"); } break; } } } private static SecureString String2SecureString(string password) { SecureString remotePassword = new SecureString(); for (int i = 0; i < password.Length; i++) remotePassword.AppendChar(password[i]); return remotePassword; } }}
using System;using System.Collections.Generic;using System.Text;using System.Management.Automation;using System.Management.Automation.Runspaces;using System.Management.Automation.Remoting;using System.Collections.ObjectModel;using System.Security;using System.Collections;namespace CallingPSCmdlet{ class Program { static void Main(string[] args) { string password = "Passowrd"; string userName = "Domain\\UserName";
System.Uri uri = new Uri(“http://CAS-SERVER/powershell?serializationLevel=Full”); System.Security.SecureString securePassword = String2SecureString(password); System.Management.Automation.PSCredential creds = new System.Management.Automation.PSCredential(userName, securePassword);
Runspace runspace = System.Management.Automation.Runspaces.RunspaceFactory.CreateRunspace();
PowerShell powershell = PowerShell.Create(); PSCommand command = new PSCommand(); command.AddCommand("New-PSSession"); command.AddParameter("ConfigurationName", "Microsoft.Exchange"); command.AddParameter("ConnectionUri", uri); command.AddParameter("Credential", creds); command.AddParameter("Authentication", "Default"); PSSessionOption sessionOption = new PSSessionOption(); sessionOption.SkipCACheck = true; sessionOption.SkipCNCheck = true; sessionOption.SkipRevocationCheck = true; command.AddParameter("SessionOption", sessionOption); powershell.Commands = command;
try { // open the remote runspace runspace.Open(); // associate the runspace with powershell powershell.Runspace = runspace; // invoke the powershell to obtain the results Collection<PSSession> result = powershell.Invoke<PSSession>(); foreach (ErrorRecord current in powershell.Streams.Error) Console.WriteLine("The following Error happen when opening the remote Runspace: " + current.Exception.ToString() + " | InnerException: " + current.Exception.InnerException); if (result.Count != 1) throw new Exception("Unexpected number of Remote Runspace connections returned.");
// Set the runspace as a local variable on the runspace powershell = PowerShell.Create(); command = new PSCommand(); command.AddCommand("Set-Variable"); command.AddParameter("Name", "ra"); command.AddParameter("Value", result[0]); powershell.Commands = command; powershell.Runspace = runspace; powershell.Invoke();
// First import the cmdlets in the current runspace (using Import-PSSession) command = new PSCommand(); command.AddScript("Import-PSSession -Session $ra"); powershell.Commands = command; powershell.Runspace = runspace; powershell.Invoke();
// Now call the Get-MaiboxDatabase
command = new PSCommand(); command.AddCommand("Get-MailboxDatabase"); //Change the name of the database command.AddParameter("Identity", "Mailbox Database XXXXXXXX"); powershell.Commands = command; powershell.Runspace = runspace; Collection<PSObject> results = new Collection<PSObject>(); results = powershell.Invoke(); PSMemberInfo Member = null;
foreach (PSObject oResult in results) { foreach (PSMemberInfo psMember in oResult.Members) { Member = psMember; DumpProperties(ref Member); } } results = null; Member = null; } finally { // dispose the runspace and enable garbage collection runspace.Dispose(); runspace = null;
// Finally dispose the powershell and set all variables to null to free up any resources. powershell.Dispose(); powershell = null; } }
//Method to Dump out the Properties public static void DumpProperties(ref PSMemberInfo psMember) { // Only look at Properties if (psMember.MemberType.ToString() == "Property") { switch (psMember.Name) { case "ActivationPreference": case "DatabaseCopies": if (psMember.Value != null) { PSObject oPSObject; ArrayList oArrayList; oPSObject = (PSObject)psMember.Value; oArrayList = (ArrayList)oPSObject.BaseObject; Console.WriteLine("Member Name:" + psMember.Name); Console.WriteLine("Member Type:" + psMember.TypeNameOfValue); Console.WriteLine("----------------------"); foreach (string item in oArrayList) { Console.WriteLine(item); } Console.WriteLine("----------------------"); } break; } } } private static SecureString String2SecureString(string password) { SecureString remotePassword = new SecureString(); for (int i = 0; i < password.Length; i++) remotePassword.AppendChar(password[i]); return remotePassword; } }}
Member Name:DatabaseCopiesMember Type:Deserialized.Microsoft.Exchange.Data.Directory.SystemConfiguration.DatabaseCopy[]----------------------Mailbox Database 1056078029\AKAS23474121Mailbox Database 1056078029\AKAS23474120----------------------Member Name:ActivationPreferenceMember Type:Deserialized.System.Collections.Generic.KeyValuePair`2[[Microsoft.Exchange.Data.Directory.ADObjectId, Microsoft.Exchange.Data.Directory, Version=14.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35],[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]][]----------------------[AKAS23474121, 1][AKAS23474120, 2]----------------------
Perfect!!!
In addition to this:
IS THE OUTPUT OF REMOTE COMMANDS DIFFERENT FROM LOCAL OUTPUT?
When you use Windows PowerShell locally, you send and receive "live" .NET
Framework objects; "live" objects are objects that are associated with
actual programs or system components. When you invoke the methods or change
the properties of live objects, the changes affect the actual program or
component. And, when the properties of a program or component change,
the properties of the object that represent them also change.
However, because most live objects cannot be transmitted over the network,
Windows PowerShell "serializes" most of the objects sent in remote commands,
that is, it converts each object into a series of XML (Constraint Language
in XML [CLiXML]) data elements for transmission.
When Windows PowerShell receives a serialized object, it converts
the XML into a deserialized object type. The deserialized object
is an accurate record of the properties of the program or component at
a previous time, but it is no longer "live", that is, it
is no longer directly associated with the component. And, the methods are
removed because they are no longer effective.
Typically, you can use deserialized objects just as you would use live
objects, but you must be aware of their limitations. Also, the objects
that are returned by the Invoke-Command cmdlet have additional properties
that help you to determine the origin of the command.
Some object types, such as DirectoryInfo objects and GUIDs, are converted
back into live objects when they are received. These objects do not need
any special handling or formatting.
For information about interpreting and formatting remote output, see
about_Remote_Output.
Thank you Vishal for the additonal details!