diff --git a/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs
new file mode 100644
index 000000000..3ddec51b2
--- /dev/null
+++ b/src/EventStore.Client/EventStoreClientSettings.ConnectionString.cs
@@ -0,0 +1,254 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+
+namespace EventStore.Client {
+ public partial class EventStoreClientSettings {
+ ///
+ /// Creates client settings from a connection string
+ ///
+ ///
+ ///
+ public static EventStoreClientSettings Create(string connectionString) {
+ return ConnectionStringParser.Parse(connectionString);
+ }
+
+ private static class ConnectionStringParser {
+ private const string SchemeSeparator = "://";
+ private const string UserInfoSeparator = "@";
+ private const string Colon = ":";
+ private const string Slash = "/";
+ private const string Comma = ",";
+ private const string Ampersand = "&";
+ private const string Equal = "=";
+ private const string QuestionMark = "?";
+ private static readonly string[] Schemes = { "esdb" };
+ private static readonly int DefaultPort = EventStoreClientConnectivitySettings.Default.Address.Port;
+ private static readonly bool DefaultUseTls = true;
+
+ private static readonly Dictionary SettingsType = new Dictionary (StringComparer.InvariantCultureIgnoreCase) {
+ {"ConnectionName", typeof(string)},
+ {"MaxDiscoverAttempts", typeof(int)},
+ {"DiscoveryInterval", typeof(int)},
+ {"GossipTimeout", typeof(int)},
+ {"NodePreference", typeof(string)},
+ {"Tls", typeof(bool)},
+ {"TlsVerifyCert", typeof(bool)},
+ {"OperationTimeout", typeof(int)},
+ {"ThrowOnAppendFailure", typeof(bool)}
+ };
+
+ public static EventStoreClientSettings Parse(string connectionString) {
+ var currentIndex = 0;
+ var schemeIndex = connectionString.IndexOf(SchemeSeparator, currentIndex, StringComparison.Ordinal);
+ if (schemeIndex == -1)
+ throw new NoSchemeException();
+ var scheme = ParseScheme(connectionString.Substring(0, schemeIndex));
+
+ currentIndex = schemeIndex + SchemeSeparator.Length;
+ var userInfoIndex = connectionString.IndexOf(UserInfoSeparator, currentIndex, StringComparison.Ordinal);
+ (string user, string pass) userInfo = (null, null);
+ if (userInfoIndex != -1) {
+ userInfo = ParseUserInfo(connectionString.Substring(currentIndex, userInfoIndex - currentIndex));
+ currentIndex = userInfoIndex + UserInfoSeparator.Length;
+ }
+
+
+ var slashIndex = connectionString.IndexOf(Slash, currentIndex, StringComparison.Ordinal);
+ var questionMarkIndex = connectionString.IndexOf(QuestionMark, Math.Max(currentIndex, slashIndex), StringComparison.Ordinal);
+ var endIndex = connectionString.Length;
+
+ //for simpler substring operations:
+ if (slashIndex == -1) slashIndex = int.MaxValue;
+ if (questionMarkIndex == -1) questionMarkIndex = int.MaxValue;
+
+ var hostSeparatorIndex = Math.Min(Math.Min(slashIndex, questionMarkIndex), endIndex);
+ var hosts = ParseHosts(connectionString.Substring(currentIndex,hostSeparatorIndex - currentIndex));
+ currentIndex = hostSeparatorIndex;
+
+ string path = "";
+ if (slashIndex != int.MaxValue)
+ path = connectionString.Substring(currentIndex,Math.Min(questionMarkIndex, endIndex) - currentIndex);
+
+ if (path != "" && path != "/")
+ throw new ConnectionStringParseException($"The specified path must be either an empty string or a forward slash (/) but the following path was found instead: '{path}'");
+
+ var options = new Dictionary();
+ if (questionMarkIndex != int.MaxValue) {
+ currentIndex = questionMarkIndex + QuestionMark.Length;
+ options = ParseKeyValuePairs(connectionString.Substring(currentIndex));
+ }
+
+ return CreateSettings(scheme, userInfo, hosts, options);
+ }
+
+ private static EventStoreClientSettings CreateSettings(string scheme, (string user, string pass) userInfo, EndPoint[] hosts, Dictionary options) {
+ var settings = new EventStoreClientSettings {
+ ConnectivitySettings = EventStoreClientConnectivitySettings.Default,
+ OperationOptions = EventStoreClientOperationOptions.Default
+ };
+
+ if (userInfo != (null, null))
+ settings.DefaultCredentials = new UserCredentials(userInfo.user, userInfo.pass);
+
+ var typedOptions = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
+ foreach (var option in options) {
+ if (!SettingsType.TryGetValue(option.Key, out var type)) throw new InvalidSettingException($"Unknown option: {option.Key}");
+ if(type == typeof(int)){
+ if (!int.TryParse(option.Value, out var intValue))
+ throw new InvalidSettingException($"{option.Key} must be an integer value");
+ typedOptions.Add(option.Key, intValue);
+ } else if (type == typeof(bool)) {
+ if (!bool.TryParse(option.Value, out var boolValue))
+ throw new InvalidSettingException($"{option.Key} must be either true or false");
+ typedOptions.Add(option.Key, boolValue);
+ } else if (type == typeof(string)) {
+ typedOptions.Add(option.Key, option.Value);
+ }
+ }
+
+ if (typedOptions.TryGetValue("ConnectionName", out object connectionName))
+ settings.ConnectionName = (string) connectionName;
+
+ var connSettings = settings.ConnectivitySettings;
+
+ if (typedOptions.TryGetValue("MaxDiscoverAttempts", out object maxDiscoverAttempts))
+ connSettings.MaxDiscoverAttempts = (int)maxDiscoverAttempts;
+
+ if (typedOptions.TryGetValue("DiscoveryInterval", out object discoveryInterval))
+ connSettings.DiscoveryInterval = TimeSpan.FromMilliseconds((int)discoveryInterval);
+
+ if (typedOptions.TryGetValue("GossipTimeout", out object gossipTimeout))
+ connSettings.GossipTimeout = TimeSpan.FromMilliseconds((int)gossipTimeout);
+
+ if (typedOptions.TryGetValue("NodePreference", out object nodePreference)) {
+ var nodePreferenceLowerCase = ((string)nodePreference).ToLowerInvariant();
+ switch (nodePreferenceLowerCase) {
+ case "leader":
+ connSettings.NodePreference = NodePreference.Leader;
+ break;
+ case "follower":
+ connSettings.NodePreference = NodePreference.Follower;
+ break;
+ case "random":
+ connSettings.NodePreference = NodePreference.Random;
+ break;
+ case "readonlyreplica":
+ connSettings.NodePreference = NodePreference.ReadOnlyReplica;
+ break;
+ default:
+ throw new InvalidSettingException($"Invalid NodePreference: {nodePreference}");
+ }
+ }
+
+ var useTls = DefaultUseTls;
+ if(typedOptions.TryGetValue("Tls", out object tls)) {
+ useTls = (bool)tls;
+ }
+
+ if (typedOptions.TryGetValue("TlsVerifyCert", out object tlsVerifyCert)) {
+ if (!(bool)tlsVerifyCert) {
+ settings.CreateHttpMessageHandler = () => new SocketsHttpHandler {
+ SslOptions = {
+ RemoteCertificateValidationCallback = delegate { return true; }
+ }
+ };
+ }
+ }
+
+ if(typedOptions.TryGetValue("OperationTimeout", out object operationTimeout))
+ settings.OperationOptions.TimeoutAfter = TimeSpan.FromMilliseconds((int) operationTimeout);
+
+ if(typedOptions.TryGetValue("ThrowOnAppendFailure", out object throwOnAppendFailure))
+ settings.OperationOptions.ThrowOnAppendFailure = (bool) throwOnAppendFailure;
+
+ if (hosts.Length == 1) {
+ connSettings.Address = new Uri(hosts[0].ToHttpUrl(useTls?Uri.UriSchemeHttps:Uri.UriSchemeHttp));
+ } else {
+ if (hosts.Any(x => x is DnsEndPoint))
+ connSettings.DnsGossipSeeds = hosts.Select(x => new DnsEndPoint(x.GetHost(), x.GetPort())).ToArray();
+ else
+ connSettings.IpGossipSeeds = hosts.Select(x => x as IPEndPoint).ToArray();
+
+ connSettings.GossipOverHttps = useTls;
+ }
+
+ return settings;
+ }
+
+ private static string ParseScheme(string s) {
+ if (!Schemes.Contains(s)) throw new InvalidSchemeException(s, Schemes);
+ return s;
+ }
+
+ private static (string,string) ParseUserInfo(string s) {
+ var tokens = s.Split(Colon);
+ if (tokens.Length != 2) throw new InvalidUserCredentialsException(s);
+ return (tokens[0], tokens[1]);
+ }
+
+ private static EndPoint[] ParseHosts(string s) {
+ var hostsTokens = s.Split(Comma);
+ var hosts = new List();
+ foreach (var hostToken in hostsTokens) {
+ var hostPortToken = hostToken.Split(Colon);
+ string host;
+ int port;
+ switch (hostPortToken.Length)
+ {
+ case 1:
+ host = hostPortToken[0];
+ port = DefaultPort;
+ break;
+ case 2:
+ {
+ host = hostPortToken[0];
+ if (!int.TryParse(hostPortToken[1], out port))
+ throw new InvalidHostException(hostToken);
+ break;
+ }
+ default:
+ throw new InvalidHostException(hostToken);
+ }
+
+ if (host.Length == 0) {
+ throw new InvalidHostException(hostToken);
+ }
+
+ if (IPAddress.TryParse(host, out IPAddress ip)) {
+ hosts.Add(new IPEndPoint(ip, port));
+ } else {
+ hosts.Add(new DnsEndPoint(host, port));
+ }
+ }
+
+ return hosts.ToArray();
+ }
+
+ private static Dictionary ParseKeyValuePairs(string s) {
+ var options = new Dictionary(StringComparer.InvariantCultureIgnoreCase);
+ var optionsTokens = s.Split(Ampersand);
+ foreach (var optionToken in optionsTokens) {
+ var (key, val) = ParseKeyValuePair(optionToken);
+ try {
+ options.Add(key, val);
+ } catch (ArgumentException) {
+ throw new DuplicateKeyException(key);
+ }
+ }
+ return options;
+ }
+
+ private static (string,string) ParseKeyValuePair(string s) {
+ var keyValueToken = s.Split(Equal);
+ if (keyValueToken.Length != 2) {
+ throw new InvalidKeyValuePairException(s);
+ }
+
+ return (keyValueToken[0], keyValueToken[1]);
+ }
+ }
+ }
+}
diff --git a/src/EventStore.Client/EventStoreClientSettings.cs b/src/EventStore.Client/EventStoreClientSettings.cs
index d4216a936..feba3f0c6 100644
--- a/src/EventStore.Client/EventStoreClientSettings.cs
+++ b/src/EventStore.Client/EventStoreClientSettings.cs
@@ -7,7 +7,7 @@
#nullable enable
namespace EventStore.Client {
- public class EventStoreClientSettings {
+ public partial class EventStoreClientSettings {
public IEnumerable? Interceptors { get; set; }
public string? ConnectionName { get; set; }
public Func? CreateHttpMessageHandler { get; set; }
diff --git a/src/EventStore.Client/AccessDeniedException.cs b/src/EventStore.Client/Exceptions/AccessDeniedException.cs
similarity index 100%
rename from src/EventStore.Client/AccessDeniedException.cs
rename to src/EventStore.Client/Exceptions/AccessDeniedException.cs
diff --git a/src/EventStore.Client/Exceptions/ConnectionString/ConnectionStringParseException.cs b/src/EventStore.Client/Exceptions/ConnectionString/ConnectionStringParseException.cs
new file mode 100644
index 000000000..843b04eb3
--- /dev/null
+++ b/src/EventStore.Client/Exceptions/ConnectionString/ConnectionStringParseException.cs
@@ -0,0 +1,7 @@
+using System;
+
+namespace EventStore.Client {
+ public class ConnectionStringParseException : Exception {
+ public ConnectionStringParseException(string message) : base(message) { }
+ }
+}
diff --git a/src/EventStore.Client/Exceptions/ConnectionString/DuplicateKeyException.cs b/src/EventStore.Client/Exceptions/ConnectionString/DuplicateKeyException.cs
new file mode 100644
index 000000000..283c1ab79
--- /dev/null
+++ b/src/EventStore.Client/Exceptions/ConnectionString/DuplicateKeyException.cs
@@ -0,0 +1,6 @@
+namespace EventStore.Client {
+ public class DuplicateKeyException : ConnectionStringParseException {
+ public DuplicateKeyException(string key)
+ : base($"Duplicate key: '{key}'") { }
+ }
+}
diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidHostException.cs b/src/EventStore.Client/Exceptions/ConnectionString/InvalidHostException.cs
new file mode 100644
index 000000000..c905044a8
--- /dev/null
+++ b/src/EventStore.Client/Exceptions/ConnectionString/InvalidHostException.cs
@@ -0,0 +1,6 @@
+namespace EventStore.Client {
+ public class InvalidHostException : ConnectionStringParseException {
+ public InvalidHostException(string host)
+ : base($"Invalid host: '{host}'") { }
+ }
+}
diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidKeyValuePairException.cs b/src/EventStore.Client/Exceptions/ConnectionString/InvalidKeyValuePairException.cs
new file mode 100644
index 000000000..93969d380
--- /dev/null
+++ b/src/EventStore.Client/Exceptions/ConnectionString/InvalidKeyValuePairException.cs
@@ -0,0 +1,6 @@
+namespace EventStore.Client {
+ public class InvalidKeyValuePairException : ConnectionStringParseException {
+ public InvalidKeyValuePairException(string keyValuePair)
+ : base($"Invalid key/value pair: '{keyValuePair}'") { }
+ }
+}
diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidSchemeException.cs b/src/EventStore.Client/Exceptions/ConnectionString/InvalidSchemeException.cs
new file mode 100644
index 000000000..31465e669
--- /dev/null
+++ b/src/EventStore.Client/Exceptions/ConnectionString/InvalidSchemeException.cs
@@ -0,0 +1,6 @@
+namespace EventStore.Client {
+ public class InvalidSchemeException : ConnectionStringParseException {
+ public InvalidSchemeException(string scheme, string[] supportedSchemes)
+ : base($"Invalid scheme: '{scheme}'. Supported values are: {string.Join(",", supportedSchemes)}") { }
+ }
+}
diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidSettingException.cs b/src/EventStore.Client/Exceptions/ConnectionString/InvalidSettingException.cs
new file mode 100644
index 000000000..ed1e4ac7e
--- /dev/null
+++ b/src/EventStore.Client/Exceptions/ConnectionString/InvalidSettingException.cs
@@ -0,0 +1,5 @@
+namespace EventStore.Client {
+ public class InvalidSettingException : ConnectionStringParseException {
+ public InvalidSettingException(string message) : base(message) { }
+ }
+}
diff --git a/src/EventStore.Client/Exceptions/ConnectionString/InvalidUserCredentialsException.cs b/src/EventStore.Client/Exceptions/ConnectionString/InvalidUserCredentialsException.cs
new file mode 100644
index 000000000..1a3df1de4
--- /dev/null
+++ b/src/EventStore.Client/Exceptions/ConnectionString/InvalidUserCredentialsException.cs
@@ -0,0 +1,6 @@
+namespace EventStore.Client {
+ public class InvalidUserCredentialsException : ConnectionStringParseException {
+ public InvalidUserCredentialsException(string userInfo)
+ : base($"Invalid user credentials: '{userInfo}'. Username & password must be delimited by a colon") { }
+ }
+}
diff --git a/src/EventStore.Client/Exceptions/ConnectionString/NoSchemeException.cs b/src/EventStore.Client/Exceptions/ConnectionString/NoSchemeException.cs
new file mode 100644
index 000000000..5c2469170
--- /dev/null
+++ b/src/EventStore.Client/Exceptions/ConnectionString/NoSchemeException.cs
@@ -0,0 +1,6 @@
+namespace EventStore.Client {
+ public class NoSchemeException : ConnectionStringParseException {
+ public NoSchemeException()
+ : base("Could not parse scheme from connection string") { }
+ }
+}
diff --git a/src/EventStore.Client/DiscoveryException.cs b/src/EventStore.Client/Exceptions/DiscoveryException.cs
similarity index 100%
rename from src/EventStore.Client/DiscoveryException.cs
rename to src/EventStore.Client/Exceptions/DiscoveryException.cs
diff --git a/src/EventStore.Client/NotAuthenticatedException.cs b/src/EventStore.Client/Exceptions/NotAuthenticatedException.cs
similarity index 100%
rename from src/EventStore.Client/NotAuthenticatedException.cs
rename to src/EventStore.Client/Exceptions/NotAuthenticatedException.cs
diff --git a/src/EventStore.Client/NotLeaderException.cs b/src/EventStore.Client/Exceptions/NotLeaderException.cs
similarity index 100%
rename from src/EventStore.Client/NotLeaderException.cs
rename to src/EventStore.Client/Exceptions/NotLeaderException.cs
diff --git a/src/EventStore.Client/RequiredMetadataPropertyMissingException.cs b/src/EventStore.Client/Exceptions/RequiredMetadataPropertyMissingException.cs
similarity index 100%
rename from src/EventStore.Client/RequiredMetadataPropertyMissingException.cs
rename to src/EventStore.Client/Exceptions/RequiredMetadataPropertyMissingException.cs
diff --git a/src/EventStore.Client/ScavengeNotFoundException.cs b/src/EventStore.Client/Exceptions/ScavengeNotFoundException.cs
similarity index 100%
rename from src/EventStore.Client/ScavengeNotFoundException.cs
rename to src/EventStore.Client/Exceptions/ScavengeNotFoundException.cs
diff --git a/src/EventStore.Client/StreamDeletedException.cs b/src/EventStore.Client/Exceptions/StreamDeletedException.cs
similarity index 100%
rename from src/EventStore.Client/StreamDeletedException.cs
rename to src/EventStore.Client/Exceptions/StreamDeletedException.cs
diff --git a/src/EventStore.Client/StreamNotFoundException.cs b/src/EventStore.Client/Exceptions/StreamNotFoundException.cs
similarity index 100%
rename from src/EventStore.Client/StreamNotFoundException.cs
rename to src/EventStore.Client/Exceptions/StreamNotFoundException.cs
diff --git a/src/EventStore.Client/UserNotFoundException.cs b/src/EventStore.Client/Exceptions/UserNotFoundException.cs
similarity index 100%
rename from src/EventStore.Client/UserNotFoundException.cs
rename to src/EventStore.Client/Exceptions/UserNotFoundException.cs
diff --git a/src/EventStore.Client/WrongExpectedVersionException.cs b/src/EventStore.Client/Exceptions/WrongExpectedVersionException.cs
similarity index 100%
rename from src/EventStore.Client/WrongExpectedVersionException.cs
rename to src/EventStore.Client/Exceptions/WrongExpectedVersionException.cs
diff --git a/test/EventStore.Client.Tests/ConnectionStringTests.cs b/test/EventStore.Client.Tests/ConnectionStringTests.cs
new file mode 100644
index 000000000..29a33ca4b
--- /dev/null
+++ b/test/EventStore.Client.Tests/ConnectionStringTests.cs
@@ -0,0 +1,334 @@
+using System;
+using System.Net;
+using Xunit;
+
+namespace EventStore.Client {
+ public class ConnectionStringTests {
+ [Fact]
+ public void connection_string_with_no_schema() {
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create(":so/mething/random");
+ });
+ }
+
+ [Fact]
+ public void connection_string_with_invalid_scheme_should_throw() {
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdbwrong://");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("wrong://");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("badesdb://");
+ });
+ }
+
+ [Fact]
+ public void connection_string_with_invalid_userinfo_should_throw() {
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://userpass@127.0.0.1/");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pa:ss@127.0.0.1/");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://us:er:pa:ss@127.0.0.1/");
+ });
+ }
+
+ [Fact]
+ public void connection_string_with_invalid_host_should_throw() {
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:abc");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:abc/");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:1234,127.0.0.2:abc,127.0.0.3:4321");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:1234,127.0.0.2:abc,127.0.0.3:4321/");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:abc:def");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:abc:def/");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@localhost:1234,127.0.0.2:abc:def,127.0.0.3:4321");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@localhost:1234,127.0.0.2:abc:def,127.0.0.3:4321/");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@localhost:1234,,127.0.0.3:4321");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@localhost:1234,,127.0.0.3:4321/");
+ });
+ }
+
+ [Fact]
+ public void connection_string_with_empty_path_after_host_should_not_throw() {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1");
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1:1234");
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/");
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1?maxDiscoverAttempts=10");
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=10");
+ }
+
+ [Fact]
+ public void connection_string_with_non_empty_path_should_throw() {
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/test");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/maxDiscoverAttempts=10");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/hello?maxDiscoverAttempts=10");
+ });
+ }
+
+ [Fact]
+ public void connection_string_with_no_key_value_pairs_specified_should_not_throw() {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1");
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/");
+ }
+
+ [Fact]
+ public void connection_string_with_invalid_key_value_pair_should_throw() {
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=12=34");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts1234");
+ });
+ }
+
+ [Fact]
+ public void connection_string_with_duplicate_key_should_throw() {
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=1234&MaxDiscoverAttempts=10");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?gossipTimeout=10&gossipTimeout=30");
+ });
+ }
+
+ [Fact]
+ public void connection_string_with_invalid_settings_should_throw() {
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?unknown=1234");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=1234&hello=test");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=abcd");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?discoveryInterval=abcd");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?gossipTimeout=defg");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?tlsVerifyCert=truee");
+ });
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?nodePreference=blabla");
+ });
+ }
+
+ [Fact]
+ public void with_different_node_preferences() {
+ Assert.Equal(NodePreference.Leader, EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?nodePreference=leader").ConnectivitySettings.NodePreference);
+ Assert.Equal(NodePreference.Follower, EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?nodePreference=Follower").ConnectivitySettings.NodePreference);
+ Assert.Equal(NodePreference.Random, EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?nodePreference=rAndom").ConnectivitySettings.NodePreference);
+ Assert.Equal(NodePreference.ReadOnlyReplica, EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?nodePreference=ReadOnlyReplica").ConnectivitySettings.NodePreference);
+
+ Assert.Throws(() => {
+ EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?nodePreference=invalid");
+ });
+ }
+
+ [Fact]
+ public void with_valid_single_node_connection_string() {
+ EventStoreClientSettings settings;
+
+ settings = EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1/?maxDiscoverAttempts=13&DiscoveryInterval=37&gossipTimeout=33&nOdEPrEfErence=FoLLoWer&tlsVerifyCert=false");
+ Assert.Equal("user", settings.DefaultCredentials.Username);
+ Assert.Equal("pass", settings.DefaultCredentials.Password);
+ Assert.Equal("https://127.0.0.1:2113/",settings.ConnectivitySettings.Address.ToString());
+ Assert.Empty(settings.ConnectivitySettings.GossipSeeds);
+ Assert.Null(settings.ConnectivitySettings.IpGossipSeeds);
+ Assert.Null(settings.ConnectivitySettings.DnsGossipSeeds);
+ Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts);
+ Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds);
+ Assert.Equal(33, settings.ConnectivitySettings.GossipTimeout.TotalMilliseconds);
+ Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference);
+ Assert.NotNull(settings.CreateHttpMessageHandler);
+
+ settings = EventStoreClientSettings.Create("esdb://127.0.0.1?connectionName=test&maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tls=true&tlsVerifyCert=true&operationTimeout=330&throwOnAppendFailure=faLse");
+ Assert.Null(settings.DefaultCredentials);
+ Assert.Equal("test", settings.ConnectionName);
+ Assert.Equal("https://127.0.0.1:2113/",settings.ConnectivitySettings.Address.ToString());
+ Assert.Empty(settings.ConnectivitySettings.GossipSeeds);
+ Assert.Null(settings.ConnectivitySettings.IpGossipSeeds);
+ Assert.Null(settings.ConnectivitySettings.DnsGossipSeeds);
+ Assert.True(settings.ConnectivitySettings.GossipOverHttps);
+ Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts);
+ Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds);
+ Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference);
+ Assert.Null(settings.CreateHttpMessageHandler);
+ Assert.Equal(330, settings.OperationOptions.TimeoutAfter.Value.TotalMilliseconds);
+ Assert.False(settings.OperationOptions.ThrowOnAppendFailure);
+
+ settings = EventStoreClientSettings.Create("esdb://hostname:4321/?tls=false");
+ Assert.Null(settings.DefaultCredentials);
+ Assert.Equal("http://hostname:4321/",settings.ConnectivitySettings.Address.ToString());
+ Assert.Empty(settings.ConnectivitySettings.GossipSeeds);
+ Assert.Null(settings.ConnectivitySettings.IpGossipSeeds);
+ Assert.Null(settings.ConnectivitySettings.DnsGossipSeeds);
+ Assert.True(settings.ConnectivitySettings.GossipOverHttps);
+ Assert.Null(settings.CreateHttpMessageHandler);
+ }
+
+ [Fact]
+ public void with_default_settings() {
+ EventStoreClientSettings settings;
+ settings = EventStoreClientSettings.Create("esdb://hostname:4321/");
+
+ Assert.Null(settings.ConnectionName);
+ Assert.Equal(EventStoreClientConnectivitySettings.Default.Address.Scheme, settings.ConnectivitySettings.Address.Scheme);
+ Assert.Equal(EventStoreClientConnectivitySettings.Default.DiscoveryInterval.TotalMilliseconds, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds);
+ Assert.Null(EventStoreClientConnectivitySettings.Default.DnsGossipSeeds);
+ Assert.Empty(EventStoreClientConnectivitySettings.Default.GossipSeeds);
+ Assert.Equal(EventStoreClientConnectivitySettings.Default.GossipTimeout.TotalMilliseconds, settings.ConnectivitySettings.GossipTimeout.TotalMilliseconds);
+ Assert.Null(EventStoreClientConnectivitySettings.Default.IpGossipSeeds);
+ Assert.Equal(EventStoreClientConnectivitySettings.Default.MaxDiscoverAttempts, settings.ConnectivitySettings.MaxDiscoverAttempts);
+ Assert.Equal(EventStoreClientConnectivitySettings.Default.NodePreference, settings.ConnectivitySettings.NodePreference);
+ Assert.Equal(EventStoreClientConnectivitySettings.Default.GossipOverHttps, settings.ConnectivitySettings.GossipOverHttps);
+ Assert.Equal(EventStoreClientOperationOptions.Default.TimeoutAfter.Value.TotalMilliseconds, settings.OperationOptions.TimeoutAfter.Value.TotalMilliseconds);
+ Assert.Equal(EventStoreClientOperationOptions.Default.ThrowOnAppendFailure, settings.OperationOptions.ThrowOnAppendFailure);
+ }
+
+ [Fact]
+ public void with_valid_cluster_connection_string() {
+ EventStoreClientSettings settings;
+
+ settings = EventStoreClientSettings.Create("esdb://user:pass@127.0.0.1,127.0.0.2:3321,127.0.0.3/?maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tlsVerifyCert=false");
+ Assert.Equal("user", settings.DefaultCredentials.Username);
+ Assert.Equal("pass", settings.DefaultCredentials.Password);
+ Assert.NotEmpty(settings.ConnectivitySettings.GossipSeeds);
+ Assert.NotNull(settings.ConnectivitySettings.IpGossipSeeds);
+ Assert.Null(settings.ConnectivitySettings.DnsGossipSeeds);
+ Assert.True(settings.ConnectivitySettings.GossipOverHttps);
+ Assert.True(settings.ConnectivitySettings.IpGossipSeeds.Length == 3 &&
+ Equals(settings.ConnectivitySettings.IpGossipSeeds[0].Address, IPAddress.Parse("127.0.0.1")) &&
+ Equals(settings.ConnectivitySettings.IpGossipSeeds[0].Port, 2113) &&
+ Equals(settings.ConnectivitySettings.IpGossipSeeds[1].Address, IPAddress.Parse("127.0.0.2")) &&
+ Equals(settings.ConnectivitySettings.IpGossipSeeds[1].Port, 3321) &&
+ Equals(settings.ConnectivitySettings.IpGossipSeeds[2].Address, IPAddress.Parse("127.0.0.3")) &&
+ Equals(settings.ConnectivitySettings.IpGossipSeeds[2].Port, 2113));
+ Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts);
+ Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds);
+ Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference);
+ Assert.NotNull(settings.CreateHttpMessageHandler);
+
+
+ settings = EventStoreClientSettings.Create("esdb://user:pass@host1,host2:3321,127.0.0.3/?tls=false&maxDiscoverAttempts=13&DiscoveryInterval=37&nOdEPrEfErence=FoLLoWer&tlsVerifyCert=false");
+ Assert.Equal("user", settings.DefaultCredentials.Username);
+ Assert.Equal("pass", settings.DefaultCredentials.Password);
+ Assert.NotEmpty(settings.ConnectivitySettings.GossipSeeds);
+ Assert.Null(settings.ConnectivitySettings.IpGossipSeeds);
+ Assert.NotNull(settings.ConnectivitySettings.DnsGossipSeeds);
+ Assert.False(settings.ConnectivitySettings.GossipOverHttps);
+ Assert.True(settings.ConnectivitySettings.DnsGossipSeeds.Length == 3 &&
+ Equals(settings.ConnectivitySettings.DnsGossipSeeds[0].Host, "host1") &&
+ Equals(settings.ConnectivitySettings.DnsGossipSeeds[0].Port, 2113) &&
+ Equals(settings.ConnectivitySettings.DnsGossipSeeds[1].Host, "host2") &&
+ Equals(settings.ConnectivitySettings.DnsGossipSeeds[1].Port, 3321) &&
+ Equals(settings.ConnectivitySettings.DnsGossipSeeds[2].Host, "127.0.0.3") &&
+ Equals(settings.ConnectivitySettings.DnsGossipSeeds[2].Port, 2113));
+ Assert.Equal(13, settings.ConnectivitySettings.MaxDiscoverAttempts);
+ Assert.Equal(37, settings.ConnectivitySettings.DiscoveryInterval.TotalMilliseconds);
+ Assert.Equal(NodePreference.Follower, settings.ConnectivitySettings.NodePreference);
+ Assert.NotNull(settings.CreateHttpMessageHandler);
+ }
+
+ [Fact]
+ public void with_different_tls_settings() {
+ EventStoreClientSettings settings;
+
+ settings = EventStoreClientSettings.Create("esdb://127.0.0.1/");
+ Assert.Equal(Uri.UriSchemeHttps, settings.ConnectivitySettings.Address.Scheme);
+
+ settings = EventStoreClientSettings.Create("esdb://127.0.0.1?tls=true");
+ Assert.Equal(Uri.UriSchemeHttps, settings.ConnectivitySettings.Address.Scheme);
+
+ settings = EventStoreClientSettings.Create("esdb://127.0.0.1/?tls=FaLsE");
+ Assert.Equal(Uri.UriSchemeHttp, settings.ConnectivitySettings.Address.Scheme);
+
+ settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/");
+ Assert.True(settings.ConnectivitySettings.GossipOverHttps);
+
+ settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3?tls=true");
+ Assert.True(settings.ConnectivitySettings.GossipOverHttps);
+
+ settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/?tls=fAlSe");
+ Assert.False(settings.ConnectivitySettings.GossipOverHttps);
+ }
+
+ [Fact]
+ public void with_different_tls_verify_cert_settings() {
+ EventStoreClientSettings settings;
+
+ settings = EventStoreClientSettings.Create("esdb://127.0.0.1/");
+ Assert.Null(settings.CreateHttpMessageHandler);
+
+ settings = EventStoreClientSettings.Create("esdb://127.0.0.1/?tlsVerifyCert=TrUe");
+ Assert.Null(settings.CreateHttpMessageHandler);
+
+ settings = EventStoreClientSettings.Create("esdb://127.0.0.1/?tlsVerifyCert=FaLsE");
+ Assert.NotNull(settings.CreateHttpMessageHandler);
+
+ settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/");
+ Assert.Null(settings.CreateHttpMessageHandler);
+
+ settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/?tlsVerifyCert=true");
+ Assert.Null(settings.CreateHttpMessageHandler);
+
+ settings = EventStoreClientSettings.Create("esdb://127.0.0.1,127.0.0.2:3321,127.0.0.3/?tlsVerifyCert=false");
+ Assert.NotNull(settings.CreateHttpMessageHandler);
+ }
+
+ }
+}