added ServerConfigurationMapping
split up Abstractions so we have one type per file.
This commit is contained in:
parent
b7631ecdd0
commit
a5cb6ef7d4
13 changed files with 431 additions and 76 deletions
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Canonical, format-agnostic representation of a PostgreSQL connection.
|
|
||||||
/// Keep minimal fields for broad interoperability; store extras in Properties.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ConnectionDescriptor
|
|
||||||
{
|
|
||||||
public string? Name { get; init; }
|
|
||||||
|
|
||||||
// Primary hosts (support multi-host). If empty, implies localhost default.
|
|
||||||
public IReadOnlyList<HostEndpoint> Hosts { get; init; } = new List<HostEndpoint>();
|
|
||||||
|
|
||||||
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<string, string> Properties { get; init; } =
|
|
||||||
new Dictionary<string, string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Codec for a specific connection string format (parse and format only for its own format).
|
|
||||||
/// Do not implement format specifics yet; provide interface only.
|
|
||||||
/// </summary>
|
|
||||||
public interface IConnectionStringCodec
|
|
||||||
{
|
|
||||||
ConnStringFormat Format { get; }
|
|
||||||
string FormatName { get; }
|
|
||||||
|
|
||||||
// Parse input in this codec's format into a descriptor.
|
|
||||||
Result<ConnectionDescriptor> TryParse(string input);
|
|
||||||
|
|
||||||
// Format a descriptor into this codec's format.
|
|
||||||
Result<string> TryFormat(ConnectionDescriptor descriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// High-level service to detect, parse, format and convert between formats.
|
|
||||||
/// Implementations will compose specific codecs.
|
|
||||||
/// </summary>
|
|
||||||
public interface IConnectionStringService
|
|
||||||
{
|
|
||||||
Result<ConnStringFormat> DetectFormat(string input);
|
|
||||||
|
|
||||||
Result<ConnectionDescriptor> ParseToDescriptor(string input);
|
|
||||||
|
|
||||||
Result<string> FormatFromDescriptor(ConnectionDescriptor descriptor, ConnStringFormat targetFormat);
|
|
||||||
|
|
||||||
Result<string> Convert(string input, ConnStringFormat targetFormat);
|
|
||||||
}
|
|
||||||
9
pgLabII.PgUtils/ConnectionStrings/ConnStringFormat.cs
Normal file
9
pgLabII.PgUtils/ConnectionStrings/ConnStringFormat.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace pgLabII.PgUtils.ConnectionStrings;
|
||||||
|
|
||||||
|
public enum ConnStringFormat
|
||||||
|
{
|
||||||
|
Libpq,
|
||||||
|
Npgsql,
|
||||||
|
Url,
|
||||||
|
Jdbc
|
||||||
|
}
|
||||||
29
pgLabII.PgUtils/ConnectionStrings/ConnectionDescriptor.cs
Normal file
29
pgLabII.PgUtils/ConnectionStrings/ConnectionDescriptor.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
|
namespace pgLabII.PgUtils.ConnectionStrings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Canonical, format-agnostic representation of a PostgreSQL connection.
|
||||||
|
/// Keep minimal fields for broad interoperability; store extras in Properties.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ConnectionDescriptor
|
||||||
|
{
|
||||||
|
public string? Name { get; init; }
|
||||||
|
|
||||||
|
// Primary hosts (support multi-host). If empty, implies localhost default.
|
||||||
|
public IReadOnlyList<HostEndpoint> Hosts { get; init; } = new List<HostEndpoint>();
|
||||||
|
|
||||||
|
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<string, string> Properties { get; init; } =
|
||||||
|
new Dictionary<string, string>();
|
||||||
|
}
|
||||||
9
pgLabII.PgUtils/ConnectionStrings/HostEndpoint.cs
Normal file
9
pgLabII.PgUtils/ConnectionStrings/HostEndpoint.cs
Normal file
|
|
@ -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; }
|
||||||
|
}
|
||||||
19
pgLabII.PgUtils/ConnectionStrings/IConnectionStringCodec.cs
Normal file
19
pgLabII.PgUtils/ConnectionStrings/IConnectionStringCodec.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
using FluentResults;
|
||||||
|
|
||||||
|
namespace pgLabII.PgUtils.ConnectionStrings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Codec for a specific connection string format (parse and format only for its own format).
|
||||||
|
/// Do not implement format specifics yet; provide interface only.
|
||||||
|
/// </summary>
|
||||||
|
public interface IConnectionStringCodec
|
||||||
|
{
|
||||||
|
ConnStringFormat Format { get; }
|
||||||
|
string FormatName { get; }
|
||||||
|
|
||||||
|
// Parse input in this codec's format into a descriptor.
|
||||||
|
Result<ConnectionDescriptor> TryParse(string input);
|
||||||
|
|
||||||
|
// Format a descriptor into this codec's format.
|
||||||
|
Result<string> TryFormat(ConnectionDescriptor descriptor);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
using FluentResults;
|
||||||
|
|
||||||
|
namespace pgLabII.PgUtils.ConnectionStrings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// High-level service to detect, parse, format and convert between formats.
|
||||||
|
/// Implementations will compose specific codecs.
|
||||||
|
/// </summary>
|
||||||
|
public interface IConnectionStringService
|
||||||
|
{
|
||||||
|
Result<ConnStringFormat> DetectFormat(string input);
|
||||||
|
|
||||||
|
Result<ConnectionDescriptor> ParseToDescriptor(string input);
|
||||||
|
|
||||||
|
Result<string> FormatFromDescriptor(ConnectionDescriptor descriptor, ConnStringFormat targetFormat);
|
||||||
|
|
||||||
|
Result<string> Convert(string input, ConnStringFormat targetFormat);
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,21 @@ using Npgsql;
|
||||||
|
|
||||||
namespace pgLabII.PgUtils.ConnectionStrings;
|
namespace pgLabII.PgUtils.ConnectionStrings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
public sealed class NpgsqlCodec : IConnectionStringCodec
|
public sealed class NpgsqlCodec : IConnectionStringCodec
|
||||||
{
|
{
|
||||||
public ConnStringFormat Format => ConnStringFormat.Npgsql;
|
public ConnStringFormat Format => ConnStringFormat.Npgsql;
|
||||||
|
|
|
||||||
51
pgLabII.PgUtils/ConnectionStrings/PLAN.md
Normal file
51
pgLabII.PgUtils/ConnectionStrings/PLAN.md
Normal file
|
|
@ -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.
|
||||||
142
pgLabII.Tests/Services/ServerConfigurationMappingTests.cs
Normal file
142
pgLabII.Tests/Services/ServerConfigurationMappingTests.cs
Normal file
|
|
@ -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<string,string>{{"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);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
pgLabII.Tests/pgLabII.Tests.csproj
Normal file
23
pgLabII.Tests/pgLabII.Tests.csproj
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
|
<PackageReference Include="xunit" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" >
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector" >
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\pgLabII\pgLabII.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
10
pgLabII.sln
10
pgLabII.sln
|
|
@ -18,6 +18,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pgLabII.PgUtils", "pgLabII.
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pgLabII.PgUtils.Tests", "pgLabII.PgUtils.Tests\pgLabII.PgUtils.Tests.csproj", "{915C5439-4CF5-4625-AB7B-24F0E34E5B5F}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pgLabII.PgUtils.Tests", "pgLabII.PgUtils.Tests\pgLabII.PgUtils.Tests.csproj", "{915C5439-4CF5-4625-AB7B-24F0E34E5B5F}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pgLabII.Tests", "pgLabII.Tests\pgLabII.Tests.csproj", "{3A83F6AA-A445-4A1B-BB32-230640DEDF24}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|Any CPU.Build.0 = Release|Any CPU
|
||||||
{915C5439-4CF5-4625-AB7B-24F0E34E5B5F}.Release|x64.ActiveCfg = Release|x64
|
{915C5439-4CF5-4625-AB7B-24F0E34E5B5F}.Release|x64.ActiveCfg = Release|x64
|
||||||
{915C5439-4CF5-4625-AB7B-24F0E34E5B5F}.Release|x64.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
||||||
102
pgLabII/Services/ServerConfigurationMapping.cs
Normal file
102
pgLabII/Services/ServerConfigurationMapping.cs
Normal file
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public static class ServerConfigurationMapping
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public static ConnectionDescriptor ToDescriptor(ServerConfiguration cfg,
|
||||||
|
string? applicationName = null,
|
||||||
|
int? timeoutSeconds = null,
|
||||||
|
IReadOnlyDictionary<string, string>? extraProperties = null)
|
||||||
|
{
|
||||||
|
if (cfg == null) throw new ArgumentNullException(nameof(cfg));
|
||||||
|
|
||||||
|
var hosts = new List<HostEndpoint>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(cfg.Host))
|
||||||
|
{
|
||||||
|
hosts.Add(new HostEndpoint { Host = cfg.Host.Trim(), Port = cfg.Port });
|
||||||
|
}
|
||||||
|
|
||||||
|
var props = new Dictionary<string, string>(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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,4 +41,8 @@
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\pgLabII.PgUtils\pgLabII.PgUtils.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue