From a5cb6ef7d4850b6505102368e941b8c3cb1639f2 Mon Sep 17 00:00:00 2001 From: eelke Date: Sun, 31 Aug 2025 06:49:37 +0200 Subject: [PATCH] added ServerConfigurationMapping split up Abstractions so we have one type per file. --- .../ConnectionStrings/Abstractions.cs | 76 ---------- .../ConnectionStrings/ConnStringFormat.cs | 9 ++ .../ConnectionStrings/ConnectionDescriptor.cs | 29 ++++ .../ConnectionStrings/HostEndpoint.cs | 9 ++ .../IConnectionStringCodec.cs | 19 +++ .../IConnectionStringService.cs | 18 +++ .../ConnectionStrings/NpgsqlCodec.cs | 15 ++ pgLabII.PgUtils/ConnectionStrings/PLAN.md | 51 +++++++ .../ServerConfigurationMappingTests.cs | 142 ++++++++++++++++++ pgLabII.Tests/pgLabII.Tests.csproj | 23 +++ pgLabII.sln | 10 ++ .../Services/ServerConfigurationMapping.cs | 102 +++++++++++++ pgLabII/pgLabII.csproj | 4 + 13 files changed, 431 insertions(+), 76 deletions(-) delete mode 100644 pgLabII.PgUtils/ConnectionStrings/Abstractions.cs create mode 100644 pgLabII.PgUtils/ConnectionStrings/ConnStringFormat.cs create mode 100644 pgLabII.PgUtils/ConnectionStrings/ConnectionDescriptor.cs create mode 100644 pgLabII.PgUtils/ConnectionStrings/HostEndpoint.cs create mode 100644 pgLabII.PgUtils/ConnectionStrings/IConnectionStringCodec.cs create mode 100644 pgLabII.PgUtils/ConnectionStrings/IConnectionStringService.cs create mode 100644 pgLabII.PgUtils/ConnectionStrings/PLAN.md create mode 100644 pgLabII.Tests/Services/ServerConfigurationMappingTests.cs create mode 100644 pgLabII.Tests/pgLabII.Tests.csproj create mode 100644 pgLabII/Services/ServerConfigurationMapping.cs diff --git a/pgLabII.PgUtils/ConnectionStrings/Abstractions.cs b/pgLabII.PgUtils/ConnectionStrings/Abstractions.cs deleted file mode 100644 index 178d257..0000000 --- a/pgLabII.PgUtils/ConnectionStrings/Abstractions.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Collections.Generic; -using FluentResults; -using Npgsql; - -namespace pgLabII.PgUtils.ConnectionStrings; - -public enum ConnStringFormat -{ - Libpq, - Npgsql, - Url, - Jdbc -} - -public sealed class HostEndpoint -{ - public string Host { get; init; } = string.Empty; - public ushort? Port { get; init; } -} - -/// -/// Canonical, format-agnostic representation of a PostgreSQL connection. -/// Keep minimal fields for broad interoperability; store extras in Properties. -/// -public sealed class ConnectionDescriptor -{ - public string? Name { get; init; } - - // Primary hosts (support multi-host). If empty, implies localhost default. - public IReadOnlyList Hosts { get; init; } = new List(); - - public string? Database { get; init; } - public string? Username { get; init; } - public string? Password { get; init; } - - public SslMode? SslMode { get; init; } - - // Common optional fields - public string? ApplicationName { get; init; } - public int? TimeoutSeconds { get; init; } // connect_timeout - - // Additional parameters preserved across conversions - public IReadOnlyDictionary Properties { get; init; } = - new Dictionary(); -} - -/// -/// Codec for a specific connection string format (parse and format only for its own format). -/// Do not implement format specifics yet; provide interface only. -/// -public interface IConnectionStringCodec -{ - ConnStringFormat Format { get; } - string FormatName { get; } - - // Parse input in this codec's format into a descriptor. - Result TryParse(string input); - - // Format a descriptor into this codec's format. - Result TryFormat(ConnectionDescriptor descriptor); -} - -/// -/// High-level service to detect, parse, format and convert between formats. -/// Implementations will compose specific codecs. -/// -public interface IConnectionStringService -{ - Result DetectFormat(string input); - - Result ParseToDescriptor(string input); - - Result FormatFromDescriptor(ConnectionDescriptor descriptor, ConnStringFormat targetFormat); - - Result Convert(string input, ConnStringFormat targetFormat); -} diff --git a/pgLabII.PgUtils/ConnectionStrings/ConnStringFormat.cs b/pgLabII.PgUtils/ConnectionStrings/ConnStringFormat.cs new file mode 100644 index 0000000..9e97943 --- /dev/null +++ b/pgLabII.PgUtils/ConnectionStrings/ConnStringFormat.cs @@ -0,0 +1,9 @@ +namespace pgLabII.PgUtils.ConnectionStrings; + +public enum ConnStringFormat +{ + Libpq, + Npgsql, + Url, + Jdbc +} \ No newline at end of file diff --git a/pgLabII.PgUtils/ConnectionStrings/ConnectionDescriptor.cs b/pgLabII.PgUtils/ConnectionStrings/ConnectionDescriptor.cs new file mode 100644 index 0000000..f8dfe8c --- /dev/null +++ b/pgLabII.PgUtils/ConnectionStrings/ConnectionDescriptor.cs @@ -0,0 +1,29 @@ +using Npgsql; + +namespace pgLabII.PgUtils.ConnectionStrings; + +/// +/// Canonical, format-agnostic representation of a PostgreSQL connection. +/// Keep minimal fields for broad interoperability; store extras in Properties. +/// +public sealed class ConnectionDescriptor +{ + public string? Name { get; init; } + + // Primary hosts (support multi-host). If empty, implies localhost default. + public IReadOnlyList Hosts { get; init; } = new List(); + + public string? Database { get; init; } + public string? Username { get; init; } + public string? Password { get; init; } + + public SslMode? SslMode { get; init; } + + // Common optional fields + public string? ApplicationName { get; init; } + public int? TimeoutSeconds { get; init; } // connect_timeout + + // Additional parameters preserved across conversions + public IReadOnlyDictionary Properties { get; init; } = + new Dictionary(); +} \ No newline at end of file diff --git a/pgLabII.PgUtils/ConnectionStrings/HostEndpoint.cs b/pgLabII.PgUtils/ConnectionStrings/HostEndpoint.cs new file mode 100644 index 0000000..b03d3f7 --- /dev/null +++ b/pgLabII.PgUtils/ConnectionStrings/HostEndpoint.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace pgLabII.PgUtils.ConnectionStrings; + +public sealed class HostEndpoint +{ + public string Host { get; init; } = string.Empty; + public ushort? Port { get; init; } +} diff --git a/pgLabII.PgUtils/ConnectionStrings/IConnectionStringCodec.cs b/pgLabII.PgUtils/ConnectionStrings/IConnectionStringCodec.cs new file mode 100644 index 0000000..050c46f --- /dev/null +++ b/pgLabII.PgUtils/ConnectionStrings/IConnectionStringCodec.cs @@ -0,0 +1,19 @@ +using FluentResults; + +namespace pgLabII.PgUtils.ConnectionStrings; + +/// +/// Codec for a specific connection string format (parse and format only for its own format). +/// Do not implement format specifics yet; provide interface only. +/// +public interface IConnectionStringCodec +{ + ConnStringFormat Format { get; } + string FormatName { get; } + + // Parse input in this codec's format into a descriptor. + Result TryParse(string input); + + // Format a descriptor into this codec's format. + Result TryFormat(ConnectionDescriptor descriptor); +} \ No newline at end of file diff --git a/pgLabII.PgUtils/ConnectionStrings/IConnectionStringService.cs b/pgLabII.PgUtils/ConnectionStrings/IConnectionStringService.cs new file mode 100644 index 0000000..fb6f04d --- /dev/null +++ b/pgLabII.PgUtils/ConnectionStrings/IConnectionStringService.cs @@ -0,0 +1,18 @@ +using FluentResults; + +namespace pgLabII.PgUtils.ConnectionStrings; + +/// +/// High-level service to detect, parse, format and convert between formats. +/// Implementations will compose specific codecs. +/// +public interface IConnectionStringService +{ + Result DetectFormat(string input); + + Result ParseToDescriptor(string input); + + Result FormatFromDescriptor(ConnectionDescriptor descriptor, ConnStringFormat targetFormat); + + Result Convert(string input, ConnStringFormat targetFormat); +} \ No newline at end of file diff --git a/pgLabII.PgUtils/ConnectionStrings/NpgsqlCodec.cs b/pgLabII.PgUtils/ConnectionStrings/NpgsqlCodec.cs index 7e2f0fa..7aa6519 100644 --- a/pgLabII.PgUtils/ConnectionStrings/NpgsqlCodec.cs +++ b/pgLabII.PgUtils/ConnectionStrings/NpgsqlCodec.cs @@ -8,6 +8,21 @@ using Npgsql; namespace pgLabII.PgUtils.ConnectionStrings; +/// +/// Parser/formatter for Npgsql-style .NET connection strings. We intentionally do not +/// rely on NpgsqlConnectionStringBuilder here because: +/// - We need a lossless, format-agnostic round-trip to our ConnectionDescriptor, including +/// unknown/extension keys and per-host port lists. NpgsqlConnectionStringBuilder normalizes +/// names, may drop unknown keys or coerce values, which breaks lossless conversions. +/// - We support multi-host with per-host ports and want to preserve the original textual +/// representation across conversions. The builder flattens/rewrites these details. +/// - We aim to keep pgLabII.PgUtils independent from Npgsql's evolving parsing rules and +/// version-specific behaviors to ensure stable UX and deterministic tests. +/// - We need symmetric formatting matching our other codecs (libpq/URL/JDBC) and consistent +/// quoting rules across formats. +/// If required, we still reference Npgsql for enums and interop types, but parsing/formatting +/// is done by this small, well-tested custom codec for full control and stability. +/// public sealed class NpgsqlCodec : IConnectionStringCodec { public ConnStringFormat Format => ConnStringFormat.Npgsql; diff --git a/pgLabII.PgUtils/ConnectionStrings/PLAN.md b/pgLabII.PgUtils/ConnectionStrings/PLAN.md new file mode 100644 index 0000000..b30d7fa --- /dev/null +++ b/pgLabII.PgUtils/ConnectionStrings/PLAN.md @@ -0,0 +1,51 @@ +# Connection Strings Plan + +This document tracks the plan for supporting multiple PostgreSQL connection string formats, converting between them, and mapping to/from a canonical model. + +## Current Status (2025-08-30) + +Implemented: +- Abstractions: `ConnStringFormat`, `HostEndpoint`, `ConnectionDescriptor`, `IConnectionStringCodec`, `IConnectionStringService`. +- Codecs: + - `LibpqCodec` (libpq): parse/format; multi-host; `sslmode`, `application_name`, `connect_timeout`; quoting/escaping; preserves extras. + - `NpgsqlCodec` (.NET/Npgsql): parse/format; alias recognition; multi-host with single or per-host ports; `SSL Mode`, `Application Name`, `Timeout`; double-quote rules; preserves extras. +- Tests for both codecs: parse, format, round-trip, edge quoting. + +Not yet implemented: +- URL (postgresql://) codec ✓ +- JDBC (jdbc:postgresql://) codec +- Composite `ConnectionStringService` (detect + convert) ✓ +- Mapping helpers to/from `ServerConfiguration` ✓ + +## Updated Plan + +1. Define canonical model and interfaces for connection strings. ✓ +2. Establish normalization strategy for parameter aliases and extra `Properties` handling. ✓ +3. Implement format-specific codecs: + - libpq codec (parse/format; multi-host, quoting, sslmode, timeout, extras). ✓ + - Npgsql codec (parse/format; aliases, multi-host/ports, quoting, ssl mode, timeout, extras). ✓ + - URL (postgresql://) codec (parse/format; userinfo, host[:port], db, query params, percent-encoding). ✓ + - JDBC (jdbc:postgresql://) codec (parse/format; hosts, ports, db, properties; URL-like semantics). +4. Composite conversion service: + - Implement `ConnectionStringService` composing codecs, detecting formats, converting via `ConnectionDescriptor`, and resolving alias priorities. +5. Mapping with application model: + - Add mapping utilities between `ConnectionDescriptor` and `ServerConfiguration` (primary host/port, db, SSL mode), with sensible defaults. +6. Validation and UX: + - Validation for malformed inputs & edge cases (mismatched host/port counts, invalid SSL mode, missing db/host, IPv6 bracket handling). + - Ensure sensitive fields (password) are masked in logs/preview. +7. Tests: + - Unit tests for URL and JDBC codecs; composite service detect/convert; mapping functions; cross-format round-trips; edge cases (spaces, quotes, unicode, IPv6, percent-encoding). +8. Documentation: + - Keep this plan updated and enrich XML docs on codecs/service including alias mappings and quoting/escaping rules per format. + +## Next Small Step + +Implement the URL (postgresql://) codec with unit tests. Scope: +- Parse: `postgresql://[user[:password]@]host1[:port1][,hostN[:portN]]/[database]?param=value&...` + - Support percent-decoding for user, password, database, and query values. + - Handle IPv6 literals in `[::1]` form; allow multiple hosts with optional per-host ports. + - Map common params: `sslmode`, `application_name`, `connect_timeout` and preserve other query params in `Properties`. +- Format: Build a URL using percent-encoding where required; emit multi-hosts and parameters from `Properties` not already emitted. +- Tests: basic parse/format, quoting/percent-encoding, multi-host with mixed ports, round-trips. + +After that, implement the composite `ConnectionStringService` to detect/convert across libpq, Npgsql, and URL formats. diff --git a/pgLabII.Tests/Services/ServerConfigurationMappingTests.cs b/pgLabII.Tests/Services/ServerConfigurationMappingTests.cs new file mode 100644 index 0000000..185f3cc --- /dev/null +++ b/pgLabII.Tests/Services/ServerConfigurationMappingTests.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using Npgsql; +using pgLabII.Model; +using pgLabII.PgUtils.ConnectionStrings; +using pgLabII.Services; +using Xunit; + +namespace pgLabII.Tests.Services; + +public class ServerConfigurationMappingTests +{ + [Fact] + public void ToDescriptor_Basic_MapsExpectedFields() + { + var cfg = new ServerConfiguration + { + Name = "Prod", + Host = "db.example.com", + Port = 5433, + InitialDatabase = "appdb", + DefaultSslMode = SslMode.Require, + User = new ServerUser { Name = "alice", Password = "secret" } + }; + + var extra = new Dictionary{{"search_path","public"}}; + var d = ServerConfigurationMapping.ToDescriptor(cfg, applicationName: "pgLabII", timeoutSeconds: 15, extraProperties: extra); + + Assert.Equal("Prod", d.Name); + Assert.Single(d.Hosts); + Assert.Equal("db.example.com", d.Hosts[0].Host); + Assert.Equal((ushort)5433, d.Hosts[0].Port); + Assert.Equal("appdb", d.Database); + Assert.Equal("alice", d.Username); + Assert.Equal("secret", d.Password); + Assert.Equal(SslMode.Require, d.SslMode); + Assert.Equal("pgLabII", d.ApplicationName); + Assert.Equal(15, d.TimeoutSeconds); + Assert.True(d.Properties.ContainsKey("search_path")); + Assert.Equal("public", d.Properties["search_path"]); + } + + [Fact] + public void ToDescriptor_OmitsEmptyFields() + { + var cfg = new ServerConfiguration + { + Name = "Empty", + Host = "", + InitialDatabase = "", + User = new ServerUser { Name = "", Password = "" } + }; + + var d = ServerConfigurationMapping.ToDescriptor(cfg); + + Assert.Empty(d.Hosts); + Assert.Null(d.Database); + Assert.Null(d.Username); + Assert.Null(d.Password); + } + + [Fact] + public void FromDescriptor_CreatesNew_UsesFirstHost() + { + var desc = new ConnectionDescriptor + { + Name = "Staging", + Hosts = new [] + { + new HostEndpoint{ Host = "host1", Port = 5432 }, + new HostEndpoint{ Host = "host2", Port = 5434 } + }, + Database = "stagedb", + Username = "bob", + Password = "pwd", + SslMode = SslMode.VerifyFull + }; + + var cfg = ServerConfigurationMapping.FromDescriptor(desc); + + Assert.Equal("Staging", cfg.Name); + Assert.Equal("host1", cfg.Host); + Assert.Equal((ushort)5432, cfg.Port); + Assert.Equal("stagedb", cfg.InitialDatabase); + Assert.Equal(SslMode.VerifyFull, cfg.DefaultSslMode); + Assert.Equal("bob", cfg.User.Name); + Assert.Equal("pwd", cfg.User.Password); + } + + [Fact] + public void FromDescriptor_UpdatesExisting_PreservesMissing() + { + var existing = new ServerConfiguration + { + Name = "Existing", + Host = "keep-host", + Port = 5432, + InitialDatabase = "keepdb", + DefaultSslMode = SslMode.Prefer, + User = new ServerUser { Name = "keepuser", Password = "keeppwd" } + }; + + // Descriptor missing db and user/pass and sslmode + var desc = new ConnectionDescriptor + { + Hosts = new [] { new HostEndpoint{ Host = "new-host" } } + }; + + var cfg = ServerConfigurationMapping.FromDescriptor(desc, existing); + + Assert.Equal("new-host", cfg.Host); + Assert.Equal((ushort)5432, cfg.Port); // unchanged + Assert.Equal("keepdb", cfg.InitialDatabase); // preserved + Assert.Equal(SslMode.Prefer, cfg.DefaultSslMode); // preserved + Assert.Equal("keepuser", cfg.User.Name); // preserved + Assert.Equal("keeppwd", cfg.User.Password); // preserved + } + + [Fact] + public void Roundtrip_Basic() + { + var cfg = new ServerConfiguration + { + Name = "Round", + Host = "localhost", + Port = 5432, + InitialDatabase = "postgres", + DefaultSslMode = SslMode.Allow, + User = new ServerUser { Name = "me", Password = "pw" } + }; + + var d = ServerConfigurationMapping.ToDescriptor(cfg); + var cfg2 = ServerConfigurationMapping.FromDescriptor(d); + + Assert.Equal(cfg.Name, cfg2.Name); + Assert.Equal(cfg.Host, cfg2.Host); + Assert.Equal(cfg.Port, cfg2.Port); + Assert.Equal(cfg.InitialDatabase, cfg2.InitialDatabase); + Assert.Equal(cfg.DefaultSslMode, cfg2.DefaultSslMode); + Assert.Equal(cfg.User.Name, cfg2.User.Name); + Assert.Equal(cfg.User.Password, cfg2.User.Password); + } +} diff --git a/pgLabII.Tests/pgLabII.Tests.csproj b/pgLabII.Tests/pgLabII.Tests.csproj new file mode 100644 index 0000000..f10cf0d --- /dev/null +++ b/pgLabII.Tests/pgLabII.Tests.csproj @@ -0,0 +1,23 @@ + + + net9.0 + false + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/pgLabII.sln b/pgLabII.sln index f519b85..06f47f7 100644 --- a/pgLabII.sln +++ b/pgLabII.sln @@ -18,6 +18,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pgLabII.PgUtils", "pgLabII. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pgLabII.PgUtils.Tests", "pgLabII.PgUtils.Tests\pgLabII.PgUtils.Tests.csproj", "{915C5439-4CF5-4625-AB7B-24F0E34E5B5F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pgLabII.Tests", "pgLabII.Tests\pgLabII.Tests.csproj", "{3A83F6AA-A445-4A1B-BB32-230640DEDF24}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +60,14 @@ Global {915C5439-4CF5-4625-AB7B-24F0E34E5B5F}.Release|Any CPU.Build.0 = Release|Any CPU {915C5439-4CF5-4625-AB7B-24F0E34E5B5F}.Release|x64.ActiveCfg = Release|x64 {915C5439-4CF5-4625-AB7B-24F0E34E5B5F}.Release|x64.Build.0 = Release|x64 + {3A83F6AA-A445-4A1B-BB32-230640DEDF24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A83F6AA-A445-4A1B-BB32-230640DEDF24}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A83F6AA-A445-4A1B-BB32-230640DEDF24}.Debug|x64.ActiveCfg = Debug|Any CPU + {3A83F6AA-A445-4A1B-BB32-230640DEDF24}.Debug|x64.Build.0 = Debug|Any CPU + {3A83F6AA-A445-4A1B-BB32-230640DEDF24}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A83F6AA-A445-4A1B-BB32-230640DEDF24}.Release|Any CPU.Build.0 = Release|Any CPU + {3A83F6AA-A445-4A1B-BB32-230640DEDF24}.Release|x64.ActiveCfg = Release|Any CPU + {3A83F6AA-A445-4A1B-BB32-230640DEDF24}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/pgLabII/Services/ServerConfigurationMapping.cs b/pgLabII/Services/ServerConfigurationMapping.cs new file mode 100644 index 0000000..9f51efd --- /dev/null +++ b/pgLabII/Services/ServerConfigurationMapping.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Npgsql; +using pgLabII.Model; +using pgLabII.PgUtils.ConnectionStrings; + +namespace pgLabII.Services; + +/// +/// Helpers to map between the app's ServerConfiguration model and the canonical ConnectionDescriptor +/// used by connection string codecs. This keeps PgUtils decoupled from the app model. +/// +public static class ServerConfigurationMapping +{ + /// + /// Creates a ConnectionDescriptor from a ServerConfiguration. + /// Notes: + /// - Only maps fields that exist on ServerConfiguration: Name, Host/Port (single host), InitialDatabase, + /// DefaultSslMode, User.Name/Password. + /// - ApplicationName and TimeoutSeconds don't exist on ServerConfiguration; we preserve any passed-in + /// values via optional parameters or Properties if provided by caller. + /// + public static ConnectionDescriptor ToDescriptor(ServerConfiguration cfg, + string? applicationName = null, + int? timeoutSeconds = null, + IReadOnlyDictionary? extraProperties = null) + { + if (cfg == null) throw new ArgumentNullException(nameof(cfg)); + + var hosts = new List(); + if (!string.IsNullOrWhiteSpace(cfg.Host)) + { + hosts.Add(new HostEndpoint { Host = cfg.Host.Trim(), Port = cfg.Port }); + } + + var props = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (extraProperties != null) + { + foreach (var kv in extraProperties) + props[kv.Key] = kv.Value; + } + + return new ConnectionDescriptor + { + Name = cfg.Name, + Hosts = hosts, + Database = string.IsNullOrWhiteSpace(cfg.InitialDatabase) ? null : cfg.InitialDatabase, + Username = string.IsNullOrWhiteSpace(cfg.User?.Name) ? null : cfg.User!.Name, + Password = string.IsNullOrEmpty(cfg.User?.Password) ? null : cfg.User!.Password, + SslMode = cfg.DefaultSslMode, + ApplicationName = applicationName, + TimeoutSeconds = timeoutSeconds, + Properties = props + }; + } + + /// + /// Updates or creates a ServerConfiguration from a ConnectionDescriptor. + /// - If existing is supplied, updates mutable fields and preserves UI-related fields (color, commands, etc.). + /// - If descriptor has multiple hosts, the first is mapped to Host/Port. + /// - If descriptor omits sslmode/database/username/password, existing values are preserved (if any). + /// + public static ServerConfiguration FromDescriptor(ConnectionDescriptor descriptor, ServerConfiguration? existing = null) + { + if (descriptor == null) throw new ArgumentNullException(nameof(descriptor)); + var cfg = existing ?? new ServerConfiguration(); + + // Name + if (!string.IsNullOrWhiteSpace(descriptor.Name)) + cfg.Name = descriptor.Name!; + + // Host/Port: take first + if (descriptor.Hosts != null && descriptor.Hosts.Count > 0) + { + var h = descriptor.Hosts[0]; + if (!string.IsNullOrWhiteSpace(h.Host)) + cfg.Host = h.Host; + if (h.Port.HasValue) + cfg.Port = h.Port.Value; + } + + // Database + if (!string.IsNullOrWhiteSpace(descriptor.Database)) + cfg.InitialDatabase = descriptor.Database!; + + // SSL Mode + if (descriptor.SslMode.HasValue) + cfg.DefaultSslMode = descriptor.SslMode.Value; + + // User + if (cfg.User == null) + cfg.User = new ServerUser(); + if (!string.IsNullOrWhiteSpace(descriptor.Username)) + cfg.User.Name = descriptor.Username!; + if (!string.IsNullOrEmpty(descriptor.Password)) + cfg.User.Password = descriptor.Password!; + + // Nothing to do for ApplicationName/TimeoutSeconds here; not represented in ServerConfiguration. + return cfg; + } +} diff --git a/pgLabII/pgLabII.csproj b/pgLabII/pgLabII.csproj index f2c36f9..b0f4298 100644 --- a/pgLabII/pgLabII.csproj +++ b/pgLabII/pgLabII.csproj @@ -41,4 +41,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + +