From 1f68c4dc59eb512bd5dc26f2d47ef78f878ebaa0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 31 May 2026 23:48:10 +0000 Subject: [PATCH 1/4] Initial plan From bb80117c35acfd9b3f0caca79f33e4540f1dbc38 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 31 May 2026 23:57:26 +0000 Subject: [PATCH 2/4] fix rabbitmq message bus connection string parsing Co-authored-by: niemyjski <1020579+niemyjski@users.noreply.github.com> --- .../Configuration/MessageBusOptions.cs | 64 ++++++++++++++-- .../appsettings.Development.yml | 3 + .../appsettings.Production.yml | 3 + src/Exceptionless.Job/appsettings.Staging.yml | 3 + .../appsettings.Development.yml | 3 + .../appsettings.Production.yml | 3 + src/Exceptionless.Web/appsettings.Staging.yml | 3 + .../Configuration/MessageBusOptionsTests.cs | 73 +++++++++++++++++++ tests/Exceptionless.Tests/appsettings.yml | 3 + 9 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 tests/Exceptionless.Tests/Configuration/MessageBusOptionsTests.cs diff --git a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs index a0ad599a87..4e78a98f63 100644 --- a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs +++ b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs @@ -24,15 +24,37 @@ public static MessageBusOptions ReadFromConfiguration(IConfiguration config, App if (cs != null) { - options.Data = cs.ParseConnectionString(); - options.Provider = options.Data.GetString(nameof(options.Provider)); - string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; + if (TryGetRawRabbitMqConnectionString(cs, out var connectionString)) + { + options.Provider = "rabbitmq"; + options.ConnectionString = connectionString; + options.Data = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + [nameof(options.Provider)] = options.Provider + }; + } + else + { + options.Data = cs.ParseConnectionString(); + options.Provider = options.Data.GetString(nameof(options.Provider)); + } - var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: "server"); - options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); - options.Data.AddRange(providerOptions); + if (String.IsNullOrEmpty(options.ConnectionString)) + { + string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; + if (String.Equals(options.Provider, "rabbitmq", StringComparison.OrdinalIgnoreCase) && !String.IsNullOrWhiteSpace(providerConnectionString)) + { + options.ConnectionString = TrimMatchingQuotes(providerConnectionString.Trim()); + } + else + { + var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: "server"); + options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + options.Data.AddRange(providerOptions); - options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); + options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); + } + } } else { @@ -47,4 +69,32 @@ public static MessageBusOptions ReadFromConfiguration(IConfiguration config, App return options; } + + private static bool TryGetRawRabbitMqConnectionString(string connectionString, out string? rawConnectionString) + { + rawConnectionString = null; + + const string providerPrefix = "provider="; + if (!connectionString.StartsWith(providerPrefix, StringComparison.OrdinalIgnoreCase)) + return false; + + int separatorIndex = connectionString.IndexOf(';'); + if (separatorIndex <= providerPrefix.Length) + return false; + + string provider = connectionString.Substring(providerPrefix.Length, separatorIndex - providerPrefix.Length).Trim(); + if (!String.Equals(provider, "rabbitmq", StringComparison.OrdinalIgnoreCase)) + return false; + + rawConnectionString = TrimMatchingQuotes(connectionString[(separatorIndex + 1)..].Trim()); + return !String.IsNullOrEmpty(rawConnectionString); + } + + private static string TrimMatchingQuotes(string value) + { + if (value.Length >= 2 && ((value[0] == '"' && value[^1] == '"') || (value[0] == '\'' && value[^1] == '\''))) + return value[1..^1]; + + return value; + } } diff --git a/src/Exceptionless.Job/appsettings.Development.yml b/src/Exceptionless.Job/appsettings.Development.yml index 7b7a1b7601..8d67aefcb9 100644 --- a/src/Exceptionless.Job/appsettings.Development.yml +++ b/src/Exceptionless.Job/appsettings.Development.yml @@ -4,6 +4,9 @@ ConnectionStrings: # Elasticsearch: server=https://elastic:elastic@localhost:9200 # Cache: provider=redis; # MessageBus: provider=redis; +# # RabbitMQ examples: +# # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' +# # rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis; # Storage: provider=folder;path=..\Exceptionless.Web\storage Email: smtp://localhost:1025 diff --git a/src/Exceptionless.Job/appsettings.Production.yml b/src/Exceptionless.Job/appsettings.Production.yml index b01a5221e1..4ed301f2a7 100644 --- a/src/Exceptionless.Job/appsettings.Production.yml +++ b/src/Exceptionless.Job/appsettings.Production.yml @@ -3,6 +3,9 @@ ConnectionStrings: # Elasticsearch: server=http://localhost:9200 # Cache: provider=redis;server="localhost,abortConnect=false" # MessageBus: provider=redis;server="localhost,abortConnect=false" +# # RabbitMQ examples: +# # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' +# # rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis;server="localhost,abortConnect=false" # Storage: '' # Email: 'smtps://user:password@domain.com:587' diff --git a/src/Exceptionless.Job/appsettings.Staging.yml b/src/Exceptionless.Job/appsettings.Staging.yml index db415b6211..93a54217e4 100644 --- a/src/Exceptionless.Job/appsettings.Staging.yml +++ b/src/Exceptionless.Job/appsettings.Staging.yml @@ -4,6 +4,9 @@ ConnectionStrings: # Elasticsearch: server=http://localhost:9200;replicas=0 # Cache: provider=redis; # MessageBus: provider=redis; +# # RabbitMQ examples: +# # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' +# # rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis; # Storage: provider=folder;path=.\storage= OAuth: FacebookId=395178683904310;GitHubId=7ef1dd5bfbc4ccf7f5ef;GoogleId=809763155066-enkkdmt4ierc33q9cft9nf5d5c02h30q.apps.googleusercontent.com;MicrosoftId=000000004C137E8B;SlackId=34500115540.177239122322; diff --git a/src/Exceptionless.Web/appsettings.Development.yml b/src/Exceptionless.Web/appsettings.Development.yml index b2a91a34bd..14c0664c01 100644 --- a/src/Exceptionless.Web/appsettings.Development.yml +++ b/src/Exceptionless.Web/appsettings.Development.yml @@ -4,6 +4,9 @@ ConnectionStrings: # Elasticsearch: server=https://elastic:elastic@localhost:9200 # Cache: provider=redis; # MessageBus: provider=redis; +# # RabbitMQ examples: +# # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' +# # rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis; # Storage: provider=folder;path=.\storage # LDAP: '' diff --git a/src/Exceptionless.Web/appsettings.Production.yml b/src/Exceptionless.Web/appsettings.Production.yml index a6bbd82962..80dde33957 100644 --- a/src/Exceptionless.Web/appsettings.Production.yml +++ b/src/Exceptionless.Web/appsettings.Production.yml @@ -3,6 +3,9 @@ ConnectionStrings: # Elasticsearch: server=http://localhost:9200 # Cache: provider=redis;server="localhost,abortConnect=false" # MessageBus: provider=redis;server="localhost,abortConnect=false" +# # RabbitMQ examples: +# # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' +# # rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis;server="localhost,abortConnect=false" # Storage: '' # Email: 'smtps://user:password@domain.com:587' diff --git a/src/Exceptionless.Web/appsettings.Staging.yml b/src/Exceptionless.Web/appsettings.Staging.yml index 9d97c7b5a4..9c083efb65 100644 --- a/src/Exceptionless.Web/appsettings.Staging.yml +++ b/src/Exceptionless.Web/appsettings.Staging.yml @@ -4,6 +4,9 @@ ConnectionStrings: # Elasticsearch: server=http://localhost:9200;replicas=0 # Cache: provider=redis; # MessageBus: provider=redis; +# # RabbitMQ examples: +# # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' +# # rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis; # Storage: provider=folder;path=.\storage= OAuth: FacebookId=395178683904310;GitHubId=7ef1dd5bfbc4ccf7f5ef;GoogleId=809763155066-enkkdmt4ierc33q9cft9nf5d5c02h30q.apps.googleusercontent.com;MicrosoftId=000000004C137E8B;SlackId=34500115540.177239122322; diff --git a/tests/Exceptionless.Tests/Configuration/MessageBusOptionsTests.cs b/tests/Exceptionless.Tests/Configuration/MessageBusOptionsTests.cs new file mode 100644 index 0000000000..15b9a8583f --- /dev/null +++ b/tests/Exceptionless.Tests/Configuration/MessageBusOptionsTests.cs @@ -0,0 +1,73 @@ +using Exceptionless.Core; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Exceptionless.Tests.Configuration; + +public class MessageBusOptionsTests +{ + [Fact] + public void ReadFromConfiguration_WithInlineRabbitMqUri_PreservesRawConnectionString() + { + // Arrange + const string rabbitMqConnectionString = "amqp://localhost/%2F"; + var configuration = CreateConfiguration(new Dictionary + { + ["BaseURL"] = "http://localhost:7110/#!", + ["ConnectionStrings:MessageBus"] = $"provider=rabbitmq;{rabbitMqConnectionString}" + }); + + // Act + var options = AppOptions.ReadFromConfiguration(configuration); + + // Assert + Assert.Equal("rabbitmq", options.MessageBusOptions.Provider); + Assert.Equal(rabbitMqConnectionString, options.MessageBusOptions.ConnectionString); + } + + [Fact] + public void ReadFromConfiguration_WithQuotedRabbitMqUri_PreservesRawConnectionString() + { + // Arrange + const string rabbitMqConnectionString = "amqp://localhost/%2F"; + var configuration = CreateConfiguration(new Dictionary + { + ["BaseURL"] = "http://localhost:7110/#!", + ["ConnectionStrings:MessageBus"] = $"provider=rabbitmq;\"{rabbitMqConnectionString}\"" + }); + + // Act + var options = AppOptions.ReadFromConfiguration(configuration); + + // Assert + Assert.Equal("rabbitmq", options.MessageBusOptions.Provider); + Assert.Equal(rabbitMqConnectionString, options.MessageBusOptions.ConnectionString); + } + + [Fact] + public void ReadFromConfiguration_WithRabbitMqProviderConnectionString_PreservesRawConnectionString() + { + // Arrange + const string rabbitMqConnectionString = "amqp://localhost/%2F"; + var configuration = CreateConfiguration(new Dictionary + { + ["BaseURL"] = "http://localhost:7110/#!", + ["ConnectionStrings:MessageBus"] = "provider=rabbitmq", + ["ConnectionStrings:rabbitmq"] = $"\"{rabbitMqConnectionString}\"" + }); + + // Act + var options = AppOptions.ReadFromConfiguration(configuration); + + // Assert + Assert.Equal("rabbitmq", options.MessageBusOptions.Provider); + Assert.Equal(rabbitMqConnectionString, options.MessageBusOptions.ConnectionString); + } + + private static IConfiguration CreateConfiguration(Dictionary values) + { + return new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + } +} diff --git a/tests/Exceptionless.Tests/appsettings.yml b/tests/Exceptionless.Tests/appsettings.yml index 5bdb00cb6a..d7896b5ea2 100644 --- a/tests/Exceptionless.Tests/appsettings.yml +++ b/tests/Exceptionless.Tests/appsettings.yml @@ -4,6 +4,9 @@ ConnectionStrings: # Elasticsearch: server=https://elastic:elastic@localhost:9200 # Cache: provider=redis; # MessageBus: provider=redis; + # # RabbitMQ examples: + # # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' + # # rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis; Storage: provider=folder;path=..\..\..\..\..\src\Exceptionless.Web\storage From 922f1de0ae68187b66e881657ff57de75480dd05 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 15 Jul 2026 21:42:14 -0500 Subject: [PATCH 3/4] Polish RabbitMQ configuration examples --- src/Exceptionless.Job/appsettings.Development.yml | 7 ++++--- src/Exceptionless.Job/appsettings.Production.yml | 7 ++++--- src/Exceptionless.Job/appsettings.Staging.yml | 7 ++++--- src/Exceptionless.Web/appsettings.Development.yml | 7 ++++--- src/Exceptionless.Web/appsettings.Production.yml | 7 ++++--- src/Exceptionless.Web/appsettings.Staging.yml | 7 ++++--- .../Configuration/MessageBusOptionsTests.cs | 9 ++++++--- tests/Exceptionless.Tests/appsettings.yml | 7 ++++--- 8 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/Exceptionless.Job/appsettings.Development.yml b/src/Exceptionless.Job/appsettings.Development.yml index 8d67aefcb9..df8f086dfe 100644 --- a/src/Exceptionless.Job/appsettings.Development.yml +++ b/src/Exceptionless.Job/appsettings.Development.yml @@ -4,9 +4,10 @@ ConnectionStrings: # Elasticsearch: server=https://elastic:elastic@localhost:9200 # Cache: provider=redis; # MessageBus: provider=redis; -# # RabbitMQ examples: -# # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' -# # rabbitmq: 'amqp://localhost/%2F' +# RabbitMQ examples (choose inline or separate): +# MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' +# MessageBus: 'provider=rabbitmq' +# rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis; # Storage: provider=folder;path=..\Exceptionless.Web\storage Email: smtp://localhost:1025 diff --git a/src/Exceptionless.Job/appsettings.Production.yml b/src/Exceptionless.Job/appsettings.Production.yml index 4ed301f2a7..2a32af4d01 100644 --- a/src/Exceptionless.Job/appsettings.Production.yml +++ b/src/Exceptionless.Job/appsettings.Production.yml @@ -3,9 +3,10 @@ ConnectionStrings: # Elasticsearch: server=http://localhost:9200 # Cache: provider=redis;server="localhost,abortConnect=false" # MessageBus: provider=redis;server="localhost,abortConnect=false" -# # RabbitMQ examples: -# # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' -# # rabbitmq: 'amqp://localhost/%2F' +# RabbitMQ examples (choose inline or separate): +# MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' +# MessageBus: 'provider=rabbitmq' +# rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis;server="localhost,abortConnect=false" # Storage: '' # Email: 'smtps://user:password@domain.com:587' diff --git a/src/Exceptionless.Job/appsettings.Staging.yml b/src/Exceptionless.Job/appsettings.Staging.yml index 93a54217e4..4b2d6f0278 100644 --- a/src/Exceptionless.Job/appsettings.Staging.yml +++ b/src/Exceptionless.Job/appsettings.Staging.yml @@ -4,9 +4,10 @@ ConnectionStrings: # Elasticsearch: server=http://localhost:9200;replicas=0 # Cache: provider=redis; # MessageBus: provider=redis; -# # RabbitMQ examples: -# # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' -# # rabbitmq: 'amqp://localhost/%2F' +# RabbitMQ examples (choose inline or separate): +# MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' +# MessageBus: 'provider=rabbitmq' +# rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis; # Storage: provider=folder;path=.\storage= OAuth: FacebookId=395178683904310;GitHubId=7ef1dd5bfbc4ccf7f5ef;GoogleId=809763155066-enkkdmt4ierc33q9cft9nf5d5c02h30q.apps.googleusercontent.com;MicrosoftId=000000004C137E8B;SlackId=34500115540.177239122322; diff --git a/src/Exceptionless.Web/appsettings.Development.yml b/src/Exceptionless.Web/appsettings.Development.yml index 14c0664c01..913c3aa64c 100644 --- a/src/Exceptionless.Web/appsettings.Development.yml +++ b/src/Exceptionless.Web/appsettings.Development.yml @@ -4,9 +4,10 @@ ConnectionStrings: # Elasticsearch: server=https://elastic:elastic@localhost:9200 # Cache: provider=redis; # MessageBus: provider=redis; -# # RabbitMQ examples: -# # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' -# # rabbitmq: 'amqp://localhost/%2F' +# RabbitMQ examples (choose inline or separate): +# MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' +# MessageBus: 'provider=rabbitmq' +# rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis; # Storage: provider=folder;path=.\storage # LDAP: '' diff --git a/src/Exceptionless.Web/appsettings.Production.yml b/src/Exceptionless.Web/appsettings.Production.yml index 80dde33957..b3ccd3fb91 100644 --- a/src/Exceptionless.Web/appsettings.Production.yml +++ b/src/Exceptionless.Web/appsettings.Production.yml @@ -3,9 +3,10 @@ ConnectionStrings: # Elasticsearch: server=http://localhost:9200 # Cache: provider=redis;server="localhost,abortConnect=false" # MessageBus: provider=redis;server="localhost,abortConnect=false" -# # RabbitMQ examples: -# # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' -# # rabbitmq: 'amqp://localhost/%2F' +# RabbitMQ examples (choose inline or separate): +# MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' +# MessageBus: 'provider=rabbitmq' +# rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis;server="localhost,abortConnect=false" # Storage: '' # Email: 'smtps://user:password@domain.com:587' diff --git a/src/Exceptionless.Web/appsettings.Staging.yml b/src/Exceptionless.Web/appsettings.Staging.yml index 9c083efb65..a17384b868 100644 --- a/src/Exceptionless.Web/appsettings.Staging.yml +++ b/src/Exceptionless.Web/appsettings.Staging.yml @@ -4,9 +4,10 @@ ConnectionStrings: # Elasticsearch: server=http://localhost:9200;replicas=0 # Cache: provider=redis; # MessageBus: provider=redis; -# # RabbitMQ examples: -# # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' -# # rabbitmq: 'amqp://localhost/%2F' +# RabbitMQ examples (choose inline or separate): +# MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' +# MessageBus: 'provider=rabbitmq' +# rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis; # Storage: provider=folder;path=.\storage= OAuth: FacebookId=395178683904310;GitHubId=7ef1dd5bfbc4ccf7f5ef;GoogleId=809763155066-enkkdmt4ierc33q9cft9nf5d5c02h30q.apps.googleusercontent.com;MicrosoftId=000000004C137E8B;SlackId=34500115540.177239122322; diff --git a/tests/Exceptionless.Tests/Configuration/MessageBusOptionsTests.cs b/tests/Exceptionless.Tests/Configuration/MessageBusOptionsTests.cs index 15b9a8583f..65fefacdb1 100644 --- a/tests/Exceptionless.Tests/Configuration/MessageBusOptionsTests.cs +++ b/tests/Exceptionless.Tests/Configuration/MessageBusOptionsTests.cs @@ -44,8 +44,11 @@ public void ReadFromConfiguration_WithQuotedRabbitMqUri_PreservesRawConnectionSt Assert.Equal(rabbitMqConnectionString, options.MessageBusOptions.ConnectionString); } - [Fact] - public void ReadFromConfiguration_WithRabbitMqProviderConnectionString_PreservesRawConnectionString() + [Theory] + [InlineData("amqp://localhost/%2F")] + [InlineData("\"amqp://localhost/%2F\"")] + [InlineData("'amqp://localhost/%2F'")] + public void ReadFromConfiguration_WithRabbitMqProviderConnectionString_PreservesRawConnectionString(string configuredConnectionString) { // Arrange const string rabbitMqConnectionString = "amqp://localhost/%2F"; @@ -53,7 +56,7 @@ public void ReadFromConfiguration_WithRabbitMqProviderConnectionString_Preserves { ["BaseURL"] = "http://localhost:7110/#!", ["ConnectionStrings:MessageBus"] = "provider=rabbitmq", - ["ConnectionStrings:rabbitmq"] = $"\"{rabbitMqConnectionString}\"" + ["ConnectionStrings:rabbitmq"] = configuredConnectionString }); // Act diff --git a/tests/Exceptionless.Tests/appsettings.yml b/tests/Exceptionless.Tests/appsettings.yml index f6ec406005..3d26819a88 100644 --- a/tests/Exceptionless.Tests/appsettings.yml +++ b/tests/Exceptionless.Tests/appsettings.yml @@ -4,9 +4,10 @@ ConnectionStrings: # Elasticsearch: server=https://elastic:elastic@localhost:9200 # Cache: provider=redis; # MessageBus: provider=redis; - # # RabbitMQ examples: - # # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' - # # rabbitmq: 'amqp://localhost/%2F' + # RabbitMQ examples (choose inline or separate): + # MessageBus: 'provider=rabbitmq;amqp://localhost/%2F' + # MessageBus: 'provider=rabbitmq' + # rabbitmq: 'amqp://localhost/%2F' # Queue: provider=redis; Storage: provider=folder;path=..\..\..\..\..\src\Exceptionless.Web\storage From 202f7c2185ef41449164f8dcab3fb6b08395b3c8 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Sat, 25 Jul 2026 20:48:06 -0500 Subject: [PATCH 4/4] Harden RabbitMQ message bus configuration parsing --- .../Configuration/MessageBusOptions.cs | 64 ++++++------- .../Configuration/MessageBusOptionsTests.cs | 96 +++++++++++-------- 2 files changed, 86 insertions(+), 74 deletions(-) diff --git a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs index 86c405a9a0..444058818a 100644 --- a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs +++ b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs @@ -1,4 +1,5 @@ -using Exceptionless.Core.Extensions; +using System.Diagnostics.CodeAnalysis; +using Exceptionless.Core.Extensions; using Foundatio.Utility; using Microsoft.Extensions.Configuration; @@ -6,6 +7,9 @@ namespace Exceptionless.Core.Configuration; public class MessageBusOptions { + private const string RabbitMqProvider = "rabbitmq"; + private const string ServerKey = "server"; + public string? ConnectionString { get; internal set; } public string? Provider { get; internal set; } public Dictionary Data { get; internal set; } = null!; @@ -24,37 +28,27 @@ public static MessageBusOptions ReadFromConfiguration(IConfiguration config, App if (cs != null) { - if (TryGetRawRabbitMqConnectionString(cs, out var connectionString)) + if (TryGetRabbitMqConnectionString(config, cs, out string? connectionString)) { - options.Provider = "rabbitmq"; + options.Provider = RabbitMqProvider; options.ConnectionString = connectionString; options.Data = new Dictionary(StringComparer.OrdinalIgnoreCase) { [nameof(options.Provider)] = options.Provider }; - } - else - { - options.Data = cs.ParseConnectionString(); - options.Provider = options.Data.GetString(nameof(options.Provider)); + options.Data[ServerKey] = connectionString; + + return options; } - if (String.IsNullOrEmpty(options.ConnectionString)) - { - string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; - if (String.Equals(options.Provider, "rabbitmq", StringComparison.OrdinalIgnoreCase) && !String.IsNullOrWhiteSpace(providerConnectionString)) - { - options.ConnectionString = TrimMatchingQuotes(providerConnectionString.Trim()); - } - else - { - var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: "server"); - options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); - options.Data.AddRange(providerOptions); + options.Data = cs.ParseConnectionString(); + options.Provider = options.Data.GetString(nameof(options.Provider)); + string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; - options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); - } - } + var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: ServerKey); + options.Data.AddRange(providerOptions); + + options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); } else { @@ -70,24 +64,26 @@ public static MessageBusOptions ReadFromConfiguration(IConfiguration config, App return options; } - private static bool TryGetRawRabbitMqConnectionString(string connectionString, out string? rawConnectionString) + private static bool TryGetRabbitMqConnectionString(IConfiguration config, string selector, [NotNullWhen(true)] out string? connectionString) { - rawConnectionString = null; + connectionString = null; - const string providerPrefix = "provider="; - if (!connectionString.StartsWith(providerPrefix, StringComparison.OrdinalIgnoreCase)) - return false; + int separatorIndex = selector.IndexOf(';'); + string providerSelector = separatorIndex >= 0 ? selector[..separatorIndex] : selector; + var providerData = providerSelector.ParseConnectionString(); - int separatorIndex = connectionString.IndexOf(';'); - if (separatorIndex <= providerPrefix.Length) + if (!String.Equals(providerData.GetString(nameof(Provider)), RabbitMqProvider, StringComparison.OrdinalIgnoreCase)) return false; - string provider = connectionString.Substring(providerPrefix.Length, separatorIndex - providerPrefix.Length).Trim(); - if (!String.Equals(provider, "rabbitmq", StringComparison.OrdinalIgnoreCase)) + string? configuredConnectionString = separatorIndex >= 0 ? selector[(separatorIndex + 1)..] : null; + if (String.IsNullOrWhiteSpace(configuredConnectionString)) + configuredConnectionString = config.GetConnectionString(RabbitMqProvider); + + if (String.IsNullOrWhiteSpace(configuredConnectionString)) return false; - rawConnectionString = TrimMatchingQuotes(connectionString[(separatorIndex + 1)..].Trim()); - return !String.IsNullOrEmpty(rawConnectionString); + connectionString = TrimMatchingQuotes(configuredConnectionString.Trim()); + return true; } private static string TrimMatchingQuotes(string value) diff --git a/tests/Exceptionless.Tests/Configuration/MessageBusOptionsTests.cs b/tests/Exceptionless.Tests/Configuration/MessageBusOptionsTests.cs index 65fefacdb1..1bb6f3c1a3 100644 --- a/tests/Exceptionless.Tests/Configuration/MessageBusOptionsTests.cs +++ b/tests/Exceptionless.Tests/Configuration/MessageBusOptionsTests.cs @@ -1,4 +1,5 @@ using Exceptionless.Core; +using Exceptionless.Core.Configuration; using Microsoft.Extensions.Configuration; using Xunit; @@ -6,65 +7,80 @@ namespace Exceptionless.Tests.Configuration; public class MessageBusOptionsTests { - [Fact] - public void ReadFromConfiguration_WithInlineRabbitMqUri_PreservesRawConnectionString() + [Theory] + [InlineData("provider=rabbitmq;amqp://localhost/%2F", "amqp://localhost/%2F")] + [InlineData("provider=rabbitmq;\"amqp://localhost/%2F\"", "amqp://localhost/%2F")] + [InlineData("provider=rabbitmq;'amqp://localhost/%2F'", "amqp://localhost/%2F")] + [InlineData( + " PROVIDER = \"RABBITMQ\" ; 'amqps://user:p%40ss@rabbit.example.com:5671/team%2Fprod?heartbeat=30&connection_timeout=10000' ", + "amqps://user:p%40ss@rabbit.example.com:5671/team%2Fprod?heartbeat=30&connection_timeout=10000")] + public void ReadFromConfiguration_WithInlineRabbitMqUri_PreservesRawConnectionString(string configuredConnectionString, string expectedConnectionString) { - // Arrange - const string rabbitMqConnectionString = "amqp://localhost/%2F"; - var configuration = CreateConfiguration(new Dictionary + var options = ReadOptions(new Dictionary { - ["BaseURL"] = "http://localhost:7110/#!", - ["ConnectionStrings:MessageBus"] = $"provider=rabbitmq;{rabbitMqConnectionString}" + ["ConnectionStrings:MessageBus"] = configuredConnectionString }); - // Act - var options = AppOptions.ReadFromConfiguration(configuration); + Assert.Equal("rabbitmq", options.Provider); + Assert.Equal(expectedConnectionString, options.ConnectionString); + Assert.Equal(expectedConnectionString, options.Data["server"]); + } + + [Theory] + [InlineData("provider=rabbitmq", "amqp://localhost/%2F", "amqp://localhost/%2F")] + [InlineData("provider=rabbitmq;", "'amqp://localhost/%2F'", "amqp://localhost/%2F")] + [InlineData( + "provider=RaBbItMq", + "\"amqps://user:p%40ss@rabbit.example.com:5671/team%2Fprod?heartbeat=30\"", + "amqps://user:p%40ss@rabbit.example.com:5671/team%2Fprod?heartbeat=30")] + public void ReadFromConfiguration_WithNamedRabbitMqUri_PreservesRawConnectionString(string selector, string configuredConnectionString, string expectedConnectionString) + { + var options = ReadOptions(new Dictionary + { + ["ConnectionStrings:MessageBus"] = selector, + ["ConnectionStrings:rabbitmq"] = configuredConnectionString + }); - // Assert - Assert.Equal("rabbitmq", options.MessageBusOptions.Provider); - Assert.Equal(rabbitMqConnectionString, options.MessageBusOptions.ConnectionString); + Assert.Equal("rabbitmq", options.Provider); + Assert.Equal(expectedConnectionString, options.ConnectionString); + Assert.Equal(expectedConnectionString, options.Data["server"]); } [Fact] - public void ReadFromConfiguration_WithQuotedRabbitMqUri_PreservesRawConnectionString() + public void ReadFromConfiguration_WithRedisProviderSettings_MergesLegacyKeyValueData() { - // Arrange - const string rabbitMqConnectionString = "amqp://localhost/%2F"; - var configuration = CreateConfiguration(new Dictionary + var options = ReadOptions(new Dictionary { - ["BaseURL"] = "http://localhost:7110/#!", - ["ConnectionStrings:MessageBus"] = $"provider=rabbitmq;\"{rabbitMqConnectionString}\"" + ["ConnectionStrings:MessageBus"] = "provider=redis;ssl=true", + ["ConnectionStrings:redis"] = "server=localhost:6379;abortConnect=false" }); - // Act - var options = AppOptions.ReadFromConfiguration(configuration); - - // Assert - Assert.Equal("rabbitmq", options.MessageBusOptions.Provider); - Assert.Equal(rabbitMqConnectionString, options.MessageBusOptions.ConnectionString); + Assert.Equal("redis", options.Provider); + Assert.Equal("ssl=true;server=localhost:6379;abortConnect=false", options.ConnectionString); + Assert.Equal("true", options.Data["ssl"]); + Assert.Equal("localhost:6379", options.Data["server"]); + Assert.Equal("false", options.Data["abortConnect"]); } - [Theory] - [InlineData("amqp://localhost/%2F")] - [InlineData("\"amqp://localhost/%2F\"")] - [InlineData("'amqp://localhost/%2F'")] - public void ReadFromConfiguration_WithRabbitMqProviderConnectionString_PreservesRawConnectionString(string configuredConnectionString) + [Fact] + public void ReadFromConfiguration_WithInlineRedisConnectionString_PreservesLegacyFormatting() { - // Arrange - const string rabbitMqConnectionString = "amqp://localhost/%2F"; - var configuration = CreateConfiguration(new Dictionary + var options = ReadOptions(new Dictionary { - ["BaseURL"] = "http://localhost:7110/#!", - ["ConnectionStrings:MessageBus"] = "provider=rabbitmq", - ["ConnectionStrings:rabbitmq"] = configuredConnectionString + ["ConnectionStrings:MessageBus"] = "provider=redis;server=localhost:6379,abortConnect=false" }); - // Act - var options = AppOptions.ReadFromConfiguration(configuration); + Assert.Equal("redis", options.Provider); + Assert.Equal("server=localhost:6379,abortConnect=false", options.ConnectionString); + Assert.Equal("localhost:6379,abortConnect=false", options.Data["server"]); + } + + private static MessageBusOptions ReadOptions(Dictionary values) + { + var configuration = CreateConfiguration(values); + var appOptions = new AppOptions { AppScope = "production" }; - // Assert - Assert.Equal("rabbitmq", options.MessageBusOptions.Provider); - Assert.Equal(rabbitMqConnectionString, options.MessageBusOptions.ConnectionString); + return MessageBusOptions.ReadFromConfiguration(configuration, appOptions); } private static IConfiguration CreateConfiguration(Dictionary values)