Skip to content

Commit df788a5

Browse files
feat(migration): Convert Compare-DbaAg* commands to C# binary cmdlets
Convert 6 Compare-DbaAg* PowerShell functions to C# binary cmdlets: - Compare-DbaAgReplicaAgentJob - Compare-DbaAgReplicaCredential - Compare-DbaAgReplicaLogin - Compare-DbaAgReplicaOperator - Compare-DbaAgReplicaSync - Compare-DbaAvailabilityGroup Includes shared AgReplicaHelpers static class for common functionality (GetAgReplicaInfoScript, property helpers, FindByName, error handling). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e4d1696 commit df788a5

8 files changed

Lines changed: 2044 additions & 0 deletions

dbatools.library.psd1

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@
4343
'Add-DbaAgListener',
4444
'Add-DbaAgReplica',
4545
'Clear-DbaConnectionPool',
46+
'Compare-DbaAgReplicaAgentJob',
47+
'Compare-DbaAgReplicaCredential',
48+
'Compare-DbaAgReplicaLogin',
49+
'Compare-DbaAgReplicaOperator',
50+
'Compare-DbaAgReplicaSync',
51+
'Compare-DbaAvailabilityGroup',
4652
'Connect-DbaInstance',
4753
'Disable-DbaAgHadr',
4854
'Disconnect-DbaInstance',
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Management.Automation;
4+
5+
namespace Dataplat.Dbatools.Commands
6+
{
7+
/// <summary>
8+
/// Shared utility methods and script blocks used by the Compare-DbaAgReplica* command family.
9+
/// </summary>
10+
internal static class AgReplicaHelpers
11+
{
12+
/// <summary>
13+
/// Connects to an instance, validates HADR is enabled, and returns availability group
14+
/// information including replica names. Throws sentinel strings for known error conditions.
15+
/// </summary>
16+
internal static readonly ScriptBlock GetAgReplicaInfoScript = ScriptBlock.Create(@"
17+
param($si, $sc, $hasCred, $agFilter, $hasAgFilter)
18+
$params = @{ SqlInstance = $si; MinimumVersion = 11 }
19+
if ($hasCred) { $params['SqlCredential'] = $sc }
20+
$server = Connect-DbaInstance @params
21+
if (-not $server.IsHadrEnabled) { throw 'HADR_NOT_ENABLED' }
22+
$ags = $server.AvailabilityGroups
23+
if ($hasAgFilter) { $ags = $ags | Where-Object Name -in $agFilter }
24+
if (-not $ags) { throw 'NO_AG_FOUND' }
25+
foreach ($ag in $ags) {
26+
$replicaNames = @($ag.AvailabilityReplicas | ForEach-Object { $_.Name })
27+
[PSCustomObject]@{ AgName = $ag.Name; ReplicaNames = $replicaNames }
28+
}
29+
");
30+
31+
/// <summary>
32+
/// Gets a string property value from a PSObject.
33+
/// </summary>
34+
internal static string GetPropertyString(PSObject obj, string propertyName)
35+
{
36+
if (obj == null) return null;
37+
try
38+
{
39+
PSPropertyInfo prop = obj.Properties[propertyName];
40+
if (prop != null && prop.Value != null)
41+
return prop.Value.ToString();
42+
}
43+
catch (Exception)
44+
{
45+
// PSObject property access can throw for dynamic or computed properties
46+
}
47+
return null;
48+
}
49+
50+
/// <summary>
51+
/// Gets a property value (any type) from a PSObject.
52+
/// </summary>
53+
internal static object GetPropertyValue(PSObject obj, string propertyName)
54+
{
55+
if (obj == null) return null;
56+
try
57+
{
58+
PSPropertyInfo prop = obj.Properties[propertyName];
59+
if (prop != null) return prop.Value;
60+
}
61+
catch (Exception)
62+
{
63+
// PSObject property access can throw for dynamic or computed properties
64+
}
65+
return null;
66+
}
67+
68+
/// <summary>
69+
/// Gets a string array property from a PSObject, handling PowerShell array coercion.
70+
/// </summary>
71+
internal static string[] GetStringArray(PSObject obj, string propertyName)
72+
{
73+
if (obj == null) return null;
74+
try
75+
{
76+
PSPropertyInfo prop = obj.Properties[propertyName];
77+
if (prop == null || prop.Value == null) return null;
78+
79+
if (prop.Value is string[] strArray)
80+
return strArray;
81+
82+
if (prop.Value is object[] objArray)
83+
{
84+
List<string> result = new List<string>();
85+
foreach (object item in objArray)
86+
{
87+
if (item != null)
88+
{
89+
if (item is PSObject pso)
90+
result.Add(pso.BaseObject != null ? pso.BaseObject.ToString() : pso.ToString());
91+
else
92+
result.Add(item.ToString());
93+
}
94+
}
95+
return result.ToArray();
96+
}
97+
98+
return new string[] { prop.Value.ToString() };
99+
}
100+
catch (Exception)
101+
{
102+
// PSObject property access can throw for dynamic or computed properties
103+
}
104+
return null;
105+
}
106+
107+
/// <summary>
108+
/// Gets the full exception message chain including inner exceptions.
109+
/// Used for sentinel string detection in errors thrown from embedded PowerShell scripts.
110+
/// </summary>
111+
internal static string GetFullExceptionMessage(Exception ex)
112+
{
113+
if (ex == null)
114+
return "";
115+
string msg = ex.Message ?? "";
116+
Exception inner = ex.InnerException;
117+
while (inner != null)
118+
{
119+
if (inner.Message != null)
120+
msg = msg + " " + inner.Message;
121+
inner = inner.InnerException;
122+
}
123+
return msg;
124+
}
125+
126+
/// <summary>
127+
/// Finds a PSObject in a list by its Name property (case-insensitive).
128+
/// </summary>
129+
internal static PSObject FindByName(List<PSObject> items, string name)
130+
{
131+
foreach (PSObject item in items)
132+
{
133+
string itemName = GetPropertyString(item, "Name");
134+
if (String.Equals(itemName, name, StringComparison.OrdinalIgnoreCase))
135+
return item;
136+
}
137+
return null;
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)