diff --git a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs index 420a9a4..38623ea 100644 --- a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs +++ b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs @@ -810,7 +810,7 @@ private async Task ShowConnectionDialogAsync() _connectionString = _serverConnection.GetConnectionString(_credentialService, _selectedDatabase); ServerLabel.Text = _serverConnection.ApplicationIntentReadOnly - ? $"{_serverConnection.ServerName} (Secondary)" + ? $"{_serverConnection.ServerName} (Read-only)" : _serverConnection.ServerName; ServerLabel.Foreground = Brushes.LimeGreen; ConnectButton.Content = "Reconnect"; diff --git a/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml b/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml index e9fc75e..0ae333d 100644 --- a/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml +++ b/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml @@ -81,10 +81,10 @@ + Content="Read-only intent (for AG listeners and readable replicas - including Query Store on secondary replicas)" + Foreground="{DynamicResource ForegroundBrush}" + ToolTip.Tip="Sets ApplicationIntent=ReadOnly. Required when connecting via an AG listener or Azure failover group endpoint to route to a readable secondary (including Query Store on secondary replicas)." + FontSize="12"/> diff --git a/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml.cs b/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml.cs index 5603b3b..38e11c2 100644 --- a/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml.cs +++ b/src/PlanViewer.App/Dialogs/ConnectionDialog.axaml.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Interactivity; using Microsoft.Data.SqlClient; @@ -133,7 +132,10 @@ private async void TestConnection_Click(object? sender, RoutedEventArgs e) try { var connection = BuildServerConnection(); - var connectionString = BuildConnectionString(connection); + var connectionString = connection.GetConnectionString( + LoginBox.Text?.Trim(), + PasswordBox.Text, + "master"); await using var conn = new SqlConnection(connectionString); await conn.OpenAsync(); @@ -237,42 +239,4 @@ private string GetSelectedEncryptMode() return "Mandatory"; } - private string BuildConnectionString(ServerConnection connection) - { - var builder = new SqlConnectionStringBuilder - { - DataSource = connection.ServerName, - InitialCatalog = "master", - ApplicationName = "PlanViewer", - ConnectTimeout = 15, - TrustServerCertificate = connection.TrustServerCertificate, - Encrypt = connection.EncryptMode switch - { - "Optional" => SqlConnectionEncryptOption.Optional, - "Strict" => SqlConnectionEncryptOption.Strict, - _ => SqlConnectionEncryptOption.Mandatory - }, - ApplicationIntent = connection.ApplicationIntentReadOnly - ? ApplicationIntent.ReadOnly - : ApplicationIntent.ReadWrite - }; - - switch (connection.AuthenticationType) - { - case AuthenticationTypes.SqlServer: - builder.UserID = LoginBox.Text?.Trim() ?? ""; - builder.Password = PasswordBox.Text ?? ""; - break; - case AuthenticationTypes.EntraMFA: - builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive; - if (!string.IsNullOrEmpty(LoginBox.Text?.Trim())) - builder.UserID = LoginBox.Text!.Trim(); - break; - default: - builder.IntegratedSecurity = true; - break; - } - - return builder.ConnectionString; - } } diff --git a/src/PlanViewer.Core/Models/ServerConnection.cs b/src/PlanViewer.Core/Models/ServerConnection.cs index b21c073..e4c1a47 100644 --- a/src/PlanViewer.Core/Models/ServerConnection.cs +++ b/src/PlanViewer.Core/Models/ServerConnection.cs @@ -27,6 +27,32 @@ public class ServerConnection }; public string GetConnectionString(ICredentialService credentialService, string? databaseName = null) + { + string? username = null; + string? password = null; + + switch (AuthenticationType) + { + case AuthenticationTypes.EntraMFA: + var mfaCred = credentialService.GetCredential(Id); + if (mfaCred.HasValue) + username = mfaCred.Value.Username; + break; + + case AuthenticationTypes.SqlServer: + var cred = credentialService.GetCredential(Id); + if (!cred.HasValue) + throw new InvalidOperationException( + $"SQL Server authentication credentials are missing for server '{ServerName}'. Please configure credentials before connecting."); + username = cred.Value.Username; + password = cred.Value.Password; + break; + } + + return GetConnectionString(username, password, databaseName); + } + + public string GetConnectionString(string? username, string? password, string? databaseName = null) { var builder = new SqlConnectionStringBuilder { @@ -53,18 +79,13 @@ public string GetConnectionString(ICredentialService credentialService, string? { case AuthenticationTypes.EntraMFA: builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive; - var mfaCred = credentialService.GetCredential(Id); - if (mfaCred.HasValue && !string.IsNullOrEmpty(mfaCred.Value.Username)) - builder.UserID = mfaCred.Value.Username; + if (!string.IsNullOrEmpty(username)) + builder.UserID = username; break; case AuthenticationTypes.SqlServer: - var cred = credentialService.GetCredential(Id); - if (!cred.HasValue) - throw new InvalidOperationException( - $"SQL Server authentication credentials are missing for server '{ServerName}'. Please configure credentials before connecting."); - builder.UserID = cred.Value.Username; - builder.Password = cred.Value.Password; + builder.UserID = username ?? ""; + builder.Password = password ?? ""; break; default: // Windows