Skip to content

Commit f302205

Browse files
Add MultiSubnetFailover connection option to Dashboard and Lite (#813) (#821)
Adds an opt-in checkbox in both Dashboard and Lite Add/Edit Server dialogs that sets MultiSubnetFailover=true on the connection string. Recommended for AG listeners and FCIs spanning multiple subnets — connects to all IPs in parallel during failover instead of sequentially. Defaults to off; persists to servers.json; backward compatible with existing configs. Fixes #813 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b4fe866 commit f302205

7 files changed

Lines changed: 41 additions & 6 deletions

File tree

Dashboard/AddServerDialog.xaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@
111111
Foreground="{DynamicResource ForegroundBrush}" Margin="0,0,0,6"
112112
ToolTip="Sets ApplicationIntent=ReadOnly. Required when connecting via an AG listener or Azure failover group endpoint to route to a readable secondary."/>
113113

114+
<!-- Multi-Subnet Failover -->
115+
<CheckBox x:Name="MultiSubnetFailoverCheckBox"
116+
Content="Multi-subnet failover (for AG listeners and FCIs)"
117+
Foreground="{DynamicResource ForegroundBrush}" Margin="0,0,0,6"
118+
ToolTip="Sets MultiSubnetFailover=true. Connects to all IPs in parallel during failover. Recommended for AG listeners and failover cluster instances spanning multiple subnets."/>
119+
114120
<!-- Is Favorite -->
115121
<CheckBox x:Name="IsFavoriteCheckBox" Content="Mark as favorite (appears at top of list)"
116122
Foreground="{DynamicResource ForegroundBrush}" Margin="0,0,0,6"/>

Dashboard/AddServerDialog.xaml.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public AddServerDialog(ServerConnection existingServer)
7878
};
7979
TrustServerCertificateCheckBox.IsChecked = existingServer.TrustServerCertificate;
8080
ReadOnlyIntentCheckBox.IsChecked = existingServer.ReadOnlyIntent;
81+
MultiSubnetFailoverCheckBox.IsChecked = existingServer.MultiSubnetFailover;
8182

8283
if (existingServer.AuthenticationType == AuthenticationTypes.EntraMFA)
8384
{
@@ -154,7 +155,8 @@ private SqlConnectionStringBuilder BuildConnectionBuilder()
154155
Encrypt = ParseEncryptOption(GetSelectedEncryptMode()),
155156
ApplicationIntent = ReadOnlyIntentCheckBox.IsChecked == true
156157
? ApplicationIntent.ReadOnly
157-
: ApplicationIntent.ReadWrite
158+
: ApplicationIntent.ReadWrite,
159+
MultiSubnetFailover = MultiSubnetFailoverCheckBox.IsChecked == true
158160
};
159161

160162
if (WindowsAuthRadio.IsChecked == true)
@@ -821,6 +823,7 @@ private void SetFormEnabled(bool enabled)
821823
EncryptModeComboBox.IsEnabled = enabled;
822824
TrustServerCertificateCheckBox.IsEnabled = enabled;
823825
ReadOnlyIntentCheckBox.IsEnabled = enabled;
826+
MultiSubnetFailoverCheckBox.IsEnabled = enabled;
824827
IsFavoriteCheckBox.IsEnabled = enabled;
825828
MonthlyCostTextBox.IsEnabled = enabled;
826829
DescriptionTextBox.IsEnabled = enabled;
@@ -915,6 +918,7 @@ private async void Save_Click(object sender, RoutedEventArgs e)
915918
ServerConnection.EncryptMode = GetSelectedEncryptMode();
916919
ServerConnection.TrustServerCertificate = TrustServerCertificateCheckBox.IsChecked == true;
917920
ServerConnection.ReadOnlyIntent = ReadOnlyIntentCheckBox.IsChecked == true;
921+
ServerConnection.MultiSubnetFailover = MultiSubnetFailoverCheckBox.IsChecked == true;
918922
if (decimal.TryParse(MonthlyCostTextBox.Text, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var editCost) && editCost >= 0)
919923
ServerConnection.MonthlyCostUsd = editCost;
920924
}
@@ -936,6 +940,7 @@ private async void Save_Click(object sender, RoutedEventArgs e)
936940
EncryptMode = GetSelectedEncryptMode(),
937941
TrustServerCertificate = TrustServerCertificateCheckBox.IsChecked == true,
938942
ReadOnlyIntent = ReadOnlyIntentCheckBox.IsChecked == true,
943+
MultiSubnetFailover = MultiSubnetFailoverCheckBox.IsChecked == true,
939944
MonthlyCostUsd = monthlyCost
940945
};
941946
}

Dashboard/Models/ServerConnection.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ public bool UseWindowsAuth
6969
/// </summary>
7070
public bool ReadOnlyIntent { get; set; } = false;
7171

72+
/// <summary>
73+
/// When true, sets MultiSubnetFailover=true on the connection string.
74+
/// Recommended for AG listeners and FCIs spanning multiple subnets.
75+
/// </summary>
76+
public bool MultiSubnetFailover { get; set; } = false;
77+
7278
/// <summary>
7379
/// Monthly cost of this server in USD, used for FinOps cost attribution.
7480
/// Set to 0 to hide cost columns. All FinOps costs are proportional to this budget.
@@ -120,6 +126,7 @@ public string GetConnectionString(ICredentialService credentialService)
120126
_ => SqlConnectionEncryptOption.Mandatory
121127
},
122128
ApplicationIntent = ReadOnlyIntent ? ApplicationIntent.ReadOnly : ApplicationIntent.ReadWrite,
129+
MultiSubnetFailover = MultiSubnetFailover,
123130
Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive
124131
};
125132

@@ -151,7 +158,8 @@ public string GetConnectionString(ICredentialService credentialService)
151158
password,
152159
EncryptMode,
153160
TrustServerCertificate,
154-
ReadOnlyIntent
161+
ReadOnlyIntent,
162+
MultiSubnetFailover
155163
).ConnectionString;
156164
}
157165

Dashboard/Services/DatabaseService.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ public static SqlConnectionStringBuilder BuildConnectionString(
9292
string? password = null,
9393
string encryptMode = "Mandatory",
9494
bool trustServerCertificate = false,
95-
bool readOnlyIntent = false)
95+
bool readOnlyIntent = false,
96+
bool multiSubnetFailover = false)
9697
{
9798
var builder = new SqlConnectionStringBuilder
9899
{
@@ -101,7 +102,8 @@ public static SqlConnectionStringBuilder BuildConnectionString(
101102
TrustServerCertificate = trustServerCertificate,
102103
IntegratedSecurity = useWindowsAuth,
103104
MultipleActiveResultSets = true,
104-
ApplicationIntent = readOnlyIntent ? ApplicationIntent.ReadOnly : ApplicationIntent.ReadWrite
105+
ApplicationIntent = readOnlyIntent ? ApplicationIntent.ReadOnly : ApplicationIntent.ReadWrite,
106+
MultiSubnetFailover = multiSubnetFailover
105107
};
106108

107109
// Set encryption mode

Lite/Models/ServerConnection.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ public bool UseWindowsAuth
8989
/// </summary>
9090
public bool ReadOnlyIntent { get; set; } = false;
9191

92+
/// <summary>
93+
/// When true, sets MultiSubnetFailover=true on the connection string.
94+
/// Recommended for AG listeners and FCIs spanning multiple subnets.
95+
/// </summary>
96+
public bool MultiSubnetFailover { get; set; } = false;
97+
9298
/// <summary>
9399
/// Server name with "(Read-Only)" suffix when ReadOnlyIntent is enabled.
94100
/// Used for sidebar subtitle and status text.
@@ -205,7 +211,8 @@ private string BuildConnectionString(string? username, string? password)
205211
CommandTimeout = 60,
206212
TrustServerCertificate = TrustServerCertificate,
207213
MultipleActiveResultSets = true,
208-
ApplicationIntent = ReadOnlyIntent ? ApplicationIntent.ReadOnly : ApplicationIntent.ReadWrite
214+
ApplicationIntent = ReadOnlyIntent ? ApplicationIntent.ReadOnly : ApplicationIntent.ReadWrite,
215+
MultiSubnetFailover = MultiSubnetFailover
209216
};
210217

211218
// Set encryption mode

Lite/Windows/AddServerDialog.xaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@
9696
<CheckBox x:Name="ReadOnlyIntentCheckBox" Content="Read-only intent (for AG listeners and readable replicas)"
9797
Foreground="{DynamicResource ForegroundBrush}" Margin="0,6,0,0"
9898
ToolTip="Sets ApplicationIntent=ReadOnly. Required when connecting via an AG listener or Azure failover group endpoint to route to a readable secondary."/>
99+
<CheckBox x:Name="MultiSubnetFailoverCheckBox" Content="Multi-subnet failover (for AG listeners and FCIs)"
100+
Foreground="{DynamicResource ForegroundBrush}" Margin="0,6,0,0"
101+
ToolTip="Sets MultiSubnetFailover=true. Connects to all IPs in parallel during failover. Recommended for AG listeners and failover cluster instances spanning multiple subnets."/>
99102
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
100103
<TextBlock Text="Monthly Cost ($):" Foreground="{DynamicResource ForegroundBrush}" VerticalAlignment="Center" Width="110"/>
101104
<TextBox x:Name="MonthlyCostBox" Width="100" TextAlignment="Right" Text="0"

Lite/Windows/AddServerDialog.xaml.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public AddServerDialog(ServerManager serverManager, ServerConnection existing) :
6161
DatabaseNameBox.Text = existing.DatabaseName ?? "";
6262
UtilityDatabaseBox.Text = existing.UtilityDatabase ?? "";
6363
ReadOnlyIntentCheckBox.IsChecked = existing.ReadOnlyIntent;
64+
MultiSubnetFailoverCheckBox.IsChecked = existing.MultiSubnetFailover;
6465
MonthlyCostBox.Text = existing.MonthlyCostUsd.ToString(System.Globalization.CultureInfo.InvariantCulture);
6566

6667
// Set authentication mode
@@ -146,7 +147,8 @@ private SqlConnectionStringBuilder BuildConnectionBuilder()
146147
Encrypt = ParseEncryptOption(GetSelectedEncryptMode()),
147148
ApplicationIntent = ReadOnlyIntentCheckBox.IsChecked == true
148149
? ApplicationIntent.ReadOnly
149-
: ApplicationIntent.ReadWrite
150+
: ApplicationIntent.ReadWrite,
151+
MultiSubnetFailover = MultiSubnetFailoverCheckBox.IsChecked == true
150152
};
151153

152154
if (WindowsAuthRadio.IsChecked == true)
@@ -350,6 +352,7 @@ private async void SaveButton_Click(object sender, RoutedEventArgs e)
350352
AddedServer.DatabaseName = string.IsNullOrWhiteSpace(DatabaseNameBox.Text) ? null : DatabaseNameBox.Text.Trim();
351353
AddedServer.UtilityDatabase = string.IsNullOrWhiteSpace(UtilityDatabaseBox.Text) ? null : UtilityDatabaseBox.Text.Trim();
352354
AddedServer.ReadOnlyIntent = ReadOnlyIntentCheckBox.IsChecked == true;
355+
AddedServer.MultiSubnetFailover = MultiSubnetFailoverCheckBox.IsChecked == true;
353356
if (decimal.TryParse(MonthlyCostBox.Text, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var editCost) && editCost >= 0)
354357
AddedServer.MonthlyCostUsd = editCost;
355358

@@ -375,6 +378,7 @@ private async void SaveButton_Click(object sender, RoutedEventArgs e)
375378
DatabaseName = string.IsNullOrWhiteSpace(DatabaseNameBox.Text) ? null : DatabaseNameBox.Text.Trim(),
376379
UtilityDatabase = string.IsNullOrWhiteSpace(UtilityDatabaseBox.Text) ? null : UtilityDatabaseBox.Text.Trim(),
377380
ReadOnlyIntent = ReadOnlyIntentCheckBox.IsChecked == true,
381+
MultiSubnetFailover = MultiSubnetFailoverCheckBox.IsChecked == true,
378382
MonthlyCostUsd = monthlyCost
379383
};
380384

0 commit comments

Comments
 (0)