-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathServerConnection.cs
More file actions
270 lines (237 loc) · 9.98 KB
/
ServerConnection.cs
File metadata and controls
270 lines (237 loc) · 9.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
/*
* Copyright (c) 2026 Erik Darling, Darling Data LLC
*
* This file is part of the SQL Server Performance Monitor Lite.
*
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
*/
using System;
using System.Text.Json.Serialization;
using Microsoft.Data.SqlClient;
using PerformanceMonitorLite.Services;
namespace PerformanceMonitorLite.Models;
public class ServerConnection
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string ServerName { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
/// <summary>
/// Backward compatibility property for old servers.json files.
/// Returns true if authentication type is Windows.
/// Setter updates AuthenticationType for migration from old configs.
/// </summary>
public bool UseWindowsAuth
{
get => AuthenticationType == AuthenticationTypes.Windows;
set
{
// During JSON deserialization of old configs, update AuthenticationType based on UseWindowsAuth
// Only apply this if AuthenticationType is still at default (indicating old JSON without that field)
if (AuthenticationType == AuthenticationTypes.Windows && !value)
{
// Old config with UseWindowsAuth=false -> SQL Server auth
AuthenticationType = AuthenticationTypes.SqlServer;
}
// If value is true, keep Windows (already the default)
}
}
/// <summary>
/// Authentication type: Windows, SqlServer, or EntraMFA
/// </summary>
public string AuthenticationType { get; set; } = AuthenticationTypes.Windows;
public string? Description { get; set; }
public DateTime CreatedDate { get; set; } = DateTime.Now;
public DateTime LastConnected { get; set; } = DateTime.Now;
public bool IsFavorite { get; set; }
public bool IsEnabled { get; set; } = true;
/// <summary>
/// Encryption mode for the connection. Valid values: Optional, Mandatory, Strict.
/// Default is Mandatory for security. Users can opt down to Optional if needed.
/// </summary>
public string EncryptMode { get; set; } = "Mandatory";
/// <summary>
/// Whether to trust the server certificate without validation.
/// Default is false for security. Enable for servers with self-signed certificates.
/// </summary>
public bool TrustServerCertificate { get; set; } = false;
/// <summary>
/// Monthly cost of this server in USD, used for FinOps cost attribution.
/// Set to 0 to hide cost columns. All FinOps costs are proportional to this budget.
/// </summary>
public decimal MonthlyCostUsd { get; set; } = 0m;
/// <summary>
/// Optional database name for the initial connection.
/// Required for Azure SQL Database (which doesn't allow connecting to master).
/// Leave empty for on-premises SQL Server (defaults to master).
/// </summary>
public string? DatabaseName { get; set; }
/// <summary>
/// Optional database where community stored procedures (sp_IndexCleanup) are installed.
/// When null or empty, falls back to the connection database.
/// </summary>
public string? UtilityDatabase { get; set; }
/// <summary>
/// When true, sets ApplicationIntent=ReadOnly on the connection string.
/// Required for connecting to AG listener read-only replicas and
/// Azure SQL Business Critical / Managed Instance built-in read replicas.
/// </summary>
public bool ReadOnlyIntent { get; set; } = false;
/// <summary>
/// When true, sets MultiSubnetFailover=true on the connection string.
/// Recommended for AG listeners and FCIs spanning multiple subnets.
/// </summary>
public bool MultiSubnetFailover { get; set; } = false;
/// <summary>
/// User databases to skip in per-database collectors (query_store, file_io_stats, etc.).
/// System databases (master/tempdb/model/msdb) and the connection database itself are always
/// excluded by the collectors and aren't represented here.
/// </summary>
public System.Collections.Generic.List<string> ExcludedDatabases { get; set; } = new();
/// <summary>
/// Server name with "(Read-Only)" suffix when ReadOnlyIntent is enabled.
/// Used for sidebar subtitle and status text.
/// </summary>
[JsonIgnore]
public string ServerNameDisplay => ReadOnlyIntent ? $"{ServerName} (Read-Only)" : ServerName;
/// <summary>
/// Display name with "(Read-Only)" suffix when ReadOnlyIntent is enabled.
/// Used for alerts, tray notifications, status bar, and overview cards.
/// </summary>
[JsonIgnore]
public string DisplayNameWithIntent => ReadOnlyIntent ? $"{DisplayName} (Read-Only)" : DisplayName;
/// <summary>
/// Display-only property for showing authentication type in UI.
/// </summary>
[JsonIgnore]
public string AuthenticationDisplay => AuthenticationType switch
{
AuthenticationTypes.EntraMFA => "Microsoft Entra MFA",
AuthenticationTypes.SqlServer => "SQL Server",
_ => "Windows"
};
/// <summary>
/// Actual connection status from the most recent connection check.
/// null = not checked yet, true = online, false = offline.
/// </summary>
[JsonIgnore]
public bool? IsOnline { get; set; }
/// <summary>
/// Whether one or more collectors are currently failing for this server.
/// null = not yet determined; true = some collectors have consecutive errors; false = all healthy.
/// </summary>
[JsonIgnore]
public bool? HasCollectorErrors { get; set; }
/// <summary>
/// Computed dot status for the sidebar indicator. One of: "Unknown", "Online", "Warning", "Offline".
/// Drives the Ellipse fill via DataTrigger in MainWindow.xaml.
/// </summary>
[JsonIgnore]
public string DotStatus
{
get
{
if (IsOnline == true)
return HasCollectorErrors == true ? "Warning" : "Online";
if (IsOnline == false)
return "Offline";
return "Unknown"; // null — not yet checked
}
}
/// <summary>
/// Display-only property for showing status in UI.
/// </summary>
[JsonIgnore]
public string StatusDisplay => IsEnabled ? "Enabled" : "Disabled";
/// <summary>
/// Builds and returns a connection string for this server.
/// Credentials are retrieved from Windows Credential Manager if SQL auth is used.
/// </summary>
public string GetConnectionString(CredentialService credentialService)
{
string? username = null;
string? password = null;
if (AuthenticationType == AuthenticationTypes.SqlServer)
{
var cred = credentialService.GetCredential(Id);
if (cred.HasValue)
{
username = cred.Value.Username;
password = cred.Value.Password;
}
}
return BuildConnectionString(username, password);
}
/// <summary>
/// Returns a connection string targeting UtilityDatabase if set, otherwise falls back to GetConnectionString().
/// Used for locating community stored procedures (sp_IndexCleanup) that may be installed in a non-default database.
/// </summary>
public string GetUtilityConnectionString(CredentialService credentialService)
{
var baseConnStr = GetConnectionString(credentialService);
if (string.IsNullOrWhiteSpace(UtilityDatabase))
return baseConnStr;
var builder = new SqlConnectionStringBuilder(baseConnStr)
{
InitialCatalog = UtilityDatabase
};
return builder.ConnectionString;
}
/// <summary>
/// Builds the connection string with the given credentials.
/// </summary>
private string BuildConnectionString(string? username, string? password)
{
var builder = new SqlConnectionStringBuilder
{
DataSource = ServerName,
InitialCatalog = string.IsNullOrWhiteSpace(DatabaseName) ? "master" : DatabaseName,
ApplicationName = "PerformanceMonitorLite",
ConnectTimeout = 15,
CommandTimeout = 60,
TrustServerCertificate = TrustServerCertificate,
MultipleActiveResultSets = true,
ApplicationIntent = ReadOnlyIntent ? ApplicationIntent.ReadOnly : ApplicationIntent.ReadWrite,
MultiSubnetFailover = MultiSubnetFailover
};
// Set encryption mode
builder.Encrypt = EncryptMode switch
{
"Mandatory" => SqlConnectionEncryptOption.Mandatory,
"Strict" => SqlConnectionEncryptOption.Strict,
_ => SqlConnectionEncryptOption.Optional
};
if (AuthenticationType == AuthenticationTypes.Windows)
{
builder.IntegratedSecurity = true;
}
else if (AuthenticationType == AuthenticationTypes.SqlServer)
{
builder.IntegratedSecurity = false;
builder.UserID = username ?? string.Empty;
builder.Password = password ?? string.Empty;
}
else if (AuthenticationType == AuthenticationTypes.EntraMFA)
{
// Microsoft Entra MFA (Azure AD Interactive)
builder.IntegratedSecurity = false;
builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive;
// Optionally set UserID (email/UPN)
if (!string.IsNullOrWhiteSpace(username))
{
builder.UserID = username;
}
}
return builder.ConnectionString;
}
/// <summary>
/// Checks if credentials are stored in Windows Credential Manager for this server.
/// </summary>
public bool HasStoredCredentials(CredentialService credentialService)
{
if (AuthenticationType == AuthenticationTypes.Windows || AuthenticationType == AuthenticationTypes.EntraMFA)
{
return true;
}
return credentialService.CredentialExists(Id);
}
}