diff --git a/Directory.Packages.props b/Directory.Packages.props
index b234a984b..3896476ad 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -28,7 +28,7 @@
-
+
diff --git a/src/Http/Wolverine.Http/Transport/HttpEndpoint.cs b/src/Http/Wolverine.Http/Transport/HttpEndpoint.cs
index 0e8a72985..74f420a1b 100644
--- a/src/Http/Wolverine.Http/Transport/HttpEndpoint.cs
+++ b/src/Http/Wolverine.Http/Transport/HttpEndpoint.cs
@@ -1,4 +1,5 @@
using System.Text.Json;
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using Wolverine.Configuration;
using Wolverine.Runtime;
@@ -21,6 +22,7 @@ public override ValueTask BuildListenerAsync(IWolverineRuntime runtim
return ValueTask.FromResult(new NulloListener(Uri));
}
+ [IgnoreDescription]
public JsonSerializerOptions SerializerOptions { get; set; } = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
diff --git a/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsSubscription.cs b/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsSubscription.cs
index 02405fc52..6f37b3209 100644
--- a/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsSubscription.cs
+++ b/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsSubscription.cs
@@ -37,4 +37,10 @@ internal AmazonSnsSubscription(Subscription subscription)
};
public AmazonSnsSubscriptionAttributes Attributes { get; }
+
+ ///
+ /// Used by OptionsDescription (via [DescribeAsStringArray]) to render this
+ /// subscription as a single readable line.
+ ///
+ public override string ToString() => $"{Protocol}:{Endpoint}";
}
diff --git a/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTopic.cs b/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTopic.cs
index b6a39e106..a8e7946cb 100644
--- a/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTopic.cs
+++ b/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTopic.cs
@@ -3,6 +3,7 @@
using Amazon.SQS;
using Amazon.SQS.Model;
using JasperFx.Core;
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using Wolverine.Configuration;
using Wolverine.Runtime;
@@ -59,7 +60,9 @@ internal ISnsEnvelopeMapper BuildMapper(IWolverineRuntime runtime)
public string TopicName { get; }
public string TopicArn { get; set; }
+ [ChildDescription]
public CreateTopicRequest Configuration { get; }
+ [DescribeAsStringArray]
public IList TopicSubscriptions { get; set; } = new List();
public async ValueTask CheckAsync()
diff --git a/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTransport.cs b/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTransport.cs
index 7a5947b44..6df2e5a5b 100644
--- a/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTransport.cs
+++ b/src/Transports/AWS/Wolverine.AmazonSns/Internal/AmazonSnsTransport.cs
@@ -2,6 +2,7 @@
using Amazon.SimpleNotificationService;
using Amazon.SQS;
using JasperFx.Core;
+using JasperFx.Descriptors;
using Wolverine.AmazonSqs.Internal;
using Wolverine.Runtime;
using Wolverine.Transports;
@@ -29,9 +30,12 @@ internal AmazonSnsTransport(IAmazonSimpleNotificationService snsClient, IAmazonS
public override Uri ResourceUri => new(SnsConfig.ServiceURL);
+ [DescribeAsConfigurationState]
public Func? CredentialSource { get; set; }
+ [IgnoreDescription]
public LightweightCache Topics { get; }
-
+
+ [ChildDescription]
public AmazonSimpleNotificationServiceConfig SnsConfig { get; } = new();
internal IAmazonSimpleNotificationService? SnsClient { get; private set; }
internal IAmazonSQS? SqsClient { get; private set; }
@@ -131,6 +135,7 @@ private AmazonSQSClient buildSqsClient(IWolverineRuntime runtime)
/// Override to customize the queue policy for permissions for an AWS SQS queue that subscribes to
/// an SNS topic
///
+ [IgnoreDescription]
public Func QueuePolicyBuilder { get; set; } = description =>
{
var queuePolicy = $$"""
diff --git a/src/Transports/AWS/Wolverine.AmazonSns/Internal/CloudEventsSnsMapper.cs b/src/Transports/AWS/Wolverine.AmazonSns/Internal/CloudEventsSnsMapper.cs
index 5772c6285..2d747a7d1 100644
--- a/src/Transports/AWS/Wolverine.AmazonSns/Internal/CloudEventsSnsMapper.cs
+++ b/src/Transports/AWS/Wolverine.AmazonSns/Internal/CloudEventsSnsMapper.cs
@@ -12,6 +12,8 @@ public CloudEventsSnsMapper(CloudEventsMapper inner)
_inner = inner;
}
+ public override string ToString() => "Cloud Events";
+
public string BuildMessageBody(Envelope envelope)
{
return _inner.WriteToString(envelope);
diff --git a/src/Transports/AWS/Wolverine.AmazonSns/Internal/MassTransitMapper.cs b/src/Transports/AWS/Wolverine.AmazonSns/Internal/MassTransitMapper.cs
index a5e1f985c..a9c4ba27f 100644
--- a/src/Transports/AWS/Wolverine.AmazonSns/Internal/MassTransitMapper.cs
+++ b/src/Transports/AWS/Wolverine.AmazonSns/Internal/MassTransitMapper.cs
@@ -16,6 +16,8 @@ public MassTransitMapper(IMassTransitInteropEndpoint endpoint)
_serializer = new MassTransitJsonSerializer(endpoint);
}
+ public override string ToString() => "MassTransit Interop";
+
public MassTransitJsonSerializer Serializer => _serializer;
public string BuildMessageBody(Envelope envelope)
diff --git a/src/Transports/AWS/Wolverine.AmazonSns/Internal/NServiceBusEnvelopeMapper.cs b/src/Transports/AWS/Wolverine.AmazonSns/Internal/NServiceBusEnvelopeMapper.cs
index b2c3e216a..5e7f0b6f6 100644
--- a/src/Transports/AWS/Wolverine.AmazonSns/Internal/NServiceBusEnvelopeMapper.cs
+++ b/src/Transports/AWS/Wolverine.AmazonSns/Internal/NServiceBusEnvelopeMapper.cs
@@ -22,6 +22,8 @@ public NServiceBusEnvelopeMapper(string replyName, Endpoint endpoint)
_endpoint = endpoint;
}
+ public override string ToString() => "NServiceBus Interop";
+
public IEnumerable> ToAttributes(Envelope envelope)
{
yield return new ("NServiceBus.ConversationId",
diff --git a/src/Transports/AWS/Wolverine.AmazonSns/RawJsonSnsEnvelopeMapper.cs b/src/Transports/AWS/Wolverine.AmazonSns/RawJsonSnsEnvelopeMapper.cs
index cf05ef048..ac9dd9e8f 100644
--- a/src/Transports/AWS/Wolverine.AmazonSns/RawJsonSnsEnvelopeMapper.cs
+++ b/src/Transports/AWS/Wolverine.AmazonSns/RawJsonSnsEnvelopeMapper.cs
@@ -16,6 +16,8 @@ public RawJsonSnsEnvelopeMapper(Type defaultMessageType, JsonSerializerOptions s
_serializerOptions = serializerOptions;
}
+ public override string ToString() => "Raw JSON";
+
public string BuildMessageBody(Envelope envelope)
{
return JsonSerializer.Serialize(
diff --git a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsQueue.cs b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsQueue.cs
index 7142f6bda..2ba62099e 100644
--- a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsQueue.cs
+++ b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsQueue.cs
@@ -2,6 +2,7 @@
using Amazon.SQS.Model;
using JasperFx.Core;
using JasperFx.Core.Reflection;
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using Wolverine.Configuration;
using Wolverine.Runtime;
@@ -84,6 +85,7 @@ public int VisibilityTimeout
///
/// Additional configuration for how an SQS queue should be created
///
+ [ChildDescription]
public CreateQueueRequest Configuration { get; }
///
diff --git a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsTransport.cs b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsTransport.cs
index 01861cbd4..b5903a119 100644
--- a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsTransport.cs
+++ b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/AmazonSqsTransport.cs
@@ -2,6 +2,7 @@
using Amazon.SQS;
using Amazon.SQS.Model;
using JasperFx.Core;
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using Spectre.Console;
using Wolverine.Configuration;
@@ -41,10 +42,13 @@ internal AmazonSqsTransport(IAmazonSQS client) : this()
Client = client;
}
+ [DescribeAsConfigurationState]
public Func? CredentialSource { get; set; }
+ [IgnoreDescription]
public LightweightCache Queues { get; }
+ [ChildDescription]
public AmazonSQSConfig Config { get; } = new();
internal IAmazonSQS? Client { get; private set; }
diff --git a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/CloudEventsSqsMapper.cs b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/CloudEventsSqsMapper.cs
index fa87014fc..079e60a37 100644
--- a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/CloudEventsSqsMapper.cs
+++ b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/CloudEventsSqsMapper.cs
@@ -13,6 +13,8 @@ public CloudEventsSqsMapper(CloudEventsMapper inner)
_inner = inner;
}
+ public override string ToString() => "Cloud Events";
+
public string BuildMessageBody(Envelope envelope)
{
return _inner.WriteToString(envelope);
diff --git a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/MassTransitMapper.cs b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/MassTransitMapper.cs
index a42755561..e0ea531b7 100644
--- a/src/Transports/AWS/Wolverine.AmazonSqs/Internal/MassTransitMapper.cs
+++ b/src/Transports/AWS/Wolverine.AmazonSqs/Internal/MassTransitMapper.cs
@@ -18,6 +18,8 @@ public MassTransitMapper(IMassTransitInteropEndpoint endpoint)
_serializer = new MassTransitJsonSerializer(endpoint);
}
+ public override string ToString() => "MassTransit Interop";
+
public MassTransitJsonSerializer Serializer => _serializer;
public string BuildMessageBody(Envelope envelope)
diff --git a/src/Transports/AWS/Wolverine.AmazonSqs/RawJsonSqsEnvelopeMapper.cs b/src/Transports/AWS/Wolverine.AmazonSqs/RawJsonSqsEnvelopeMapper.cs
index d71b93cfe..0cf17b304 100644
--- a/src/Transports/AWS/Wolverine.AmazonSqs/RawJsonSqsEnvelopeMapper.cs
+++ b/src/Transports/AWS/Wolverine.AmazonSqs/RawJsonSqsEnvelopeMapper.cs
@@ -16,6 +16,8 @@ public RawJsonSqsEnvelopeMapper(Type defaultMessageType, JsonSerializerOptions s
_serializerOptions = serializerOptions;
}
+ public override string ToString() => "Raw JSON";
+
public string BuildMessageBody(Envelope envelope)
{
return JsonSerializer.Serialize(
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.cs
index def0db8fd..b730d1337 100644
--- a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.cs
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusTransport.cs
@@ -3,6 +3,7 @@
using Azure.Messaging.ServiceBus;
using Azure.Messaging.ServiceBus.Administration;
using JasperFx.Core;
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using Wolverine.AzureServiceBus.Internal;
using Wolverine.Configuration;
@@ -84,9 +85,14 @@ public override string SanitizeIdentifier(string identifier)
///
public bool SystemQueuesEnabled { get; set; } = true;
+ [IgnoreDescription]
public LightweightCache Queues { get; }
+ [IgnoreDescription]
public LightweightCache Topics { get; }
+ // Contains shared access key / password — raw value hidden to avoid
+ // leaking secrets in diagnostic views. Surfaced via ConnectionSummary below.
+ [IgnoreDescription]
public string? ConnectionString { get; set; }
///
@@ -94,13 +100,34 @@ public override string SanitizeIdentifier(string identifier)
/// When set, the management client uses this instead of ConnectionString.
/// Useful for the Azure Service Bus Emulator which exposes management on a different port.
///
+ [IgnoreDescription]
public string? ManagementConnectionString { get; set; }
+ ///
+ /// The configured connection string with the SharedAccessKey value
+ /// masked. Safe to render in diagnostic output.
+ ///
+ public string? ConnectionSummary => ConnectionString == null
+ ? null
+ : ConnectionStringRedactor.Redact(ConnectionString, "SharedAccessKey");
+
+ ///
+ /// The configured management connection string with the SharedAccessKey
+ /// value masked. Safe to render in diagnostic output.
+ ///
+ public string? ManagementConnectionSummary => ManagementConnectionString == null
+ ? null
+ : ConnectionStringRedactor.Redact(ManagementConnectionString, "SharedAccessKey");
+
public string? FullyQualifiedNamespace { get; set; }
+ [IgnoreDescription]
public TokenCredential? TokenCredential { get; set; }
+ [IgnoreDescription]
public AzureNamedKeyCredential? NamedKeyCredential { get; set; }
+ [IgnoreDescription]
public AzureSasCredential? SasCredential { get; set; }
+ [ChildDescription]
public ServiceBusClientOptions ClientOptions { get; } = new()
{
TransportType = ServiceBusTransportType.AmqpTcp
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusEndpoint.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusEndpoint.cs
index 1816d0791..01ad6cbf6 100644
--- a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusEndpoint.cs
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusEndpoint.cs
@@ -1,5 +1,6 @@
using Azure.Messaging.ServiceBus;
using JasperFx.Core;
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using Wolverine.Configuration;
using Wolverine.Runtime;
@@ -31,6 +32,7 @@ public AzureServiceBusEndpoint(AzureServiceBusTransport parent, Uri uri, Endpoin
Parent = parent;
}
+ [IgnoreDescription]
public AzureServiceBusTransport Parent { get; }
///
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusQueue.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusQueue.cs
index f19235c52..f80e6cbf8 100644
--- a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusQueue.cs
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusQueue.cs
@@ -4,6 +4,7 @@
using Azure.Messaging.ServiceBus.Administration;
using JasperFx.Core;
using JasperFx.Core.Reflection;
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Wolverine.Configuration;
@@ -36,6 +37,7 @@ public AzureServiceBusQueue(AzureServiceBusTransport parent, string queueName,
};
}
+ [ChildDescription]
public CreateQueueOptions Options { get; }
public string QueueName { get; }
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusSubscription.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusSubscription.cs
index 4d3d4f4b5..e95bbad49 100644
--- a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusSubscription.cs
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusSubscription.cs
@@ -4,6 +4,7 @@
using Azure.Messaging.ServiceBus.Administration;
using JasperFx.Core;
using JasperFx.Core.Reflection;
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Wolverine.Configuration;
@@ -41,12 +42,16 @@ public AzureServiceBusSubscription(AzureServiceBusTransport parent, AzureService
RuleOptions = new CreateRuleOptions();
}
+ [ChildDescription]
public CreateSubscriptionOptions Options { get; }
+ [ChildDescription]
public CreateRuleOptions RuleOptions { get; }
public string SubscriptionName { get; }
+ // No attribute needed — AzureServiceBusTopic.ToString() returns TopicName,
+ // so this property renders as the topic name string (per audit decision).
public AzureServiceBusTopic Topic { get; }
public override ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver)
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusTopic.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusTopic.cs
index 5662c3788..85059448c 100644
--- a/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusTopic.cs
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus/Internal/AzureServiceBusTopic.cs
@@ -3,6 +3,7 @@
using Azure.Messaging.ServiceBus.Administration;
using JasperFx.Core;
using JasperFx.Core.Reflection;
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Wolverine.Configuration;
@@ -33,6 +34,12 @@ public AzureServiceBusTopic(AzureServiceBusTransport parent, string topicName) :
public string TopicName { get; }
+ ///
+ /// Used by OptionsDescription to render references to this topic (for example
+ /// from ) as just the topic name.
+ ///
+ public override string ToString() => TopicName;
+
public override ValueTask BuildListenerAsync(IWolverineRuntime runtime, IReceiver receiver)
{
throw new NotSupportedException();
@@ -60,6 +67,7 @@ public override ValueTask TeardownAsync(ILogger logger)
return new ValueTask(Parent.WithManagementClientAsync(client => client.DeleteTopicAsync(TopicName)));
}
+ [ChildDescription]
public CreateTopicOptions Options { get; }
public override ValueTask SetupAsync(ILogger logger)
diff --git a/src/Transports/Kafka/Wolverine.Kafka/Internals/KafkaTransport.cs b/src/Transports/Kafka/Wolverine.Kafka/Internals/KafkaTransport.cs
index e3e5aea7e..54babe66b 100644
--- a/src/Transports/Kafka/Wolverine.Kafka/Internals/KafkaTransport.cs
+++ b/src/Transports/Kafka/Wolverine.Kafka/Internals/KafkaTransport.cs
@@ -1,5 +1,6 @@
using Confluent.Kafka;
using JasperFx.Core;
+using JasperFx.Descriptors;
using Wolverine.Configuration;
using Wolverine.Runtime;
using Wolverine.Runtime.Routing;
@@ -16,17 +17,24 @@ public enum KafkaUsage
public class KafkaTransport : BrokerTransport
{
+ [IgnoreDescription]
public Cache Topics { get; }
internal List TopicGroups { get; } = new();
+ [ChildDescription]
public ProducerConfig ProducerConfig { get; } = new();
+ [IgnoreDescription]
public Action> ConfigureProducerBuilders { get; internal set; } = _ => {};
+ [ChildDescription]
public ConsumerConfig ConsumerConfig { get; } = new();
+ [IgnoreDescription]
public Action> ConfigureConsumerBuilders { get; internal set; } = _ => {};
+ [ChildDescription]
public AdminClientConfig AdminClientConfig { get; } = new();
+ [IgnoreDescription]
public Action ConfigureAdminClientBuilders { get; internal set; } = _ => {};
public KafkaTransport() : this("kafka")
diff --git a/src/Transports/Kafka/Wolverine.Kafka/KafkaTopic.cs b/src/Transports/Kafka/Wolverine.Kafka/KafkaTopic.cs
index b35eda901..928e60645 100644
--- a/src/Transports/Kafka/Wolverine.Kafka/KafkaTopic.cs
+++ b/src/Transports/Kafka/Wolverine.Kafka/KafkaTopic.cs
@@ -1,5 +1,6 @@
using Confluent.Kafka;
using Confluent.Kafka.Admin;
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using System.Text;
using Wolverine.Configuration;
@@ -15,6 +16,7 @@ public class KafkaTopic : Endpoint, I
// Strictly an identifier for the endpoint
public const string WolverineTopicsName = "wolverine.topics";
+ [IgnoreDescription]
public KafkaTransport Parent { get; }
public KafkaTopic(KafkaTransport parent, string topicName, EndpointRole role) : base(new Uri($"{parent.Protocol}://topic/" + topicName), role)
@@ -36,6 +38,7 @@ public override bool AutoStartSendingAgent()
return true;
}
+ [ChildDescription]
public TopicSpecification Specification { get; } = new();
public string TopicName { get; }
@@ -43,11 +46,13 @@ public override bool AutoStartSendingAgent()
///
/// Override for this specific Kafka Topic
///
+ [ChildDescription]
public ConsumerConfig? ConsumerConfig { get; internal set; }
///
/// Override for this specific Kafka Topic
///
+ [ChildDescription]
public ProducerConfig? ProducerConfig { get; internal set; }
///
@@ -204,6 +209,7 @@ public override async ValueTask InitializeAsync(ILogger logger)
///
/// Override how this Kafka topic is created
///
+ [IgnoreDescription]
public Func CreateTopicFunc { get; internal set; } = (c, t) => c.CreateTopicsAsync([t.Specification]);
}
diff --git a/src/Transports/MQTT/Wolverine.MQTT/Internals/MqttTransport.cs b/src/Transports/MQTT/Wolverine.MQTT/Internals/MqttTransport.cs
index fc07c20d2..ded7eb063 100644
--- a/src/Transports/MQTT/Wolverine.MQTT/Internals/MqttTransport.cs
+++ b/src/Transports/MQTT/Wolverine.MQTT/Internals/MqttTransport.cs
@@ -1,5 +1,6 @@
using ImTools;
using JasperFx.Core;
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using MQTTnet;
using MQTTnet.Client;
@@ -15,6 +16,7 @@ namespace Wolverine.MQTT.Internals;
public class MqttTransport : TransportBase, IAsyncDisposable
{
+ [IgnoreDescription]
public LightweightCache Topics { get; } = new();
private List _listeners = new();
private ImHashMap _topicListeners = ImHashMap.Empty;
@@ -164,6 +166,7 @@ internal bool tryFindListener(string topicName, out MqttListener listener)
internal IManagedMqttClient Client { get; private set; } = null!;
internal MqttJwtAuthenticationOptions? JwtAuthenticationOptions { get; set; }
+ [ChildDescription]
public ManagedMqttClientOptions Options { get; set; } = new ManagedMqttClientOptions
{ ClientOptions = new MqttClientOptions() };
diff --git a/src/Transports/MQTT/Wolverine.MQTT/MqttTopic.cs b/src/Transports/MQTT/Wolverine.MQTT/MqttTopic.cs
index 334b633c5..1702038b8 100644
--- a/src/Transports/MQTT/Wolverine.MQTT/MqttTopic.cs
+++ b/src/Transports/MQTT/Wolverine.MQTT/MqttTopic.cs
@@ -1,3 +1,4 @@
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using MQTTnet;
using MQTTnet.Extensions.ManagedClient;
@@ -42,6 +43,7 @@ public MqttTopic(string topicName, MqttTransport parent, EndpointRole role) : ba
Mode = EndpointMode.BufferedInMemory;
}
+ [IgnoreDescription]
public MqttTransport Parent { get; }
///
@@ -55,6 +57,7 @@ public MqttTopic(string topicName, MqttTransport parent, EndpointRole role) : ba
/// When set, overrides the built in envelope mapping with a custom
/// implementation
///
+ [IgnoreDescription]
public IMqttEnvelopeMapper EnvelopeMapper { get; set; }
public override bool AutoStartSendingAgent()
diff --git a/src/Transports/NATS/Wolverine.Nats/Internal/NatsEndpoint.cs b/src/Transports/NATS/Wolverine.Nats/Internal/NatsEndpoint.cs
index 4546f1640..abbb9b177 100644
--- a/src/Transports/NATS/Wolverine.Nats/Internal/NatsEndpoint.cs
+++ b/src/Transports/NATS/Wolverine.Nats/Internal/NatsEndpoint.cs
@@ -1,3 +1,4 @@
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using NATS.Client.Core;
using NATS.Client.JetStream.Models;
@@ -27,6 +28,7 @@ public NatsEndpoint(string subject, NatsTransport transport, EndpointRole role)
}
public string Subject { get; }
+ [IgnoreDescription]
public object? NatsSerializer { get; set; }
public Dictionary CustomHeaders { get; set; } = new();
public string? QueueGroup { get; set; }
diff --git a/src/Transports/NATS/Wolverine.Nats/Internal/NatsTransport.cs b/src/Transports/NATS/Wolverine.Nats/Internal/NatsTransport.cs
index 3f758aa2e..887ca13e6 100644
--- a/src/Transports/NATS/Wolverine.Nats/Internal/NatsTransport.cs
+++ b/src/Transports/NATS/Wolverine.Nats/Internal/NatsTransport.cs
@@ -1,3 +1,4 @@
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using NATS.Client.Core;
using NATS.Client.JetStream;
@@ -49,6 +50,7 @@ public NatsTransport()
public string ResponseSubject { get; private set; } = "wolverine.response";
+ [ChildDescription]
public NatsTransportConfiguration Configuration { get; } = new();
public NatsConnection Connection =>
diff --git a/src/Transports/Pulsar/Wolverine.Pulsar/DeadLetterTopic.cs b/src/Transports/Pulsar/Wolverine.Pulsar/DeadLetterTopic.cs
index 705699026..2c49d4a7e 100644
--- a/src/Transports/Pulsar/Wolverine.Pulsar/DeadLetterTopic.cs
+++ b/src/Transports/Pulsar/Wolverine.Pulsar/DeadLetterTopic.cs
@@ -39,6 +39,11 @@ public string? TopicName
set => _topicName = value ?? throw new ArgumentNullException(nameof(TopicName));
}
+ ///
+ /// Used by OptionsDescription to render this as the bare topic name.
+ ///
+ public override string ToString() => _topicName ?? string.Empty;
+
protected bool Equals(DeadLetterTopic other)
{
return _topicName == other._topicName;
diff --git a/src/Transports/Pulsar/Wolverine.Pulsar/PulsarTransport.cs b/src/Transports/Pulsar/Wolverine.Pulsar/PulsarTransport.cs
index 1c52055d6..2aaf054fa 100644
--- a/src/Transports/Pulsar/Wolverine.Pulsar/PulsarTransport.cs
+++ b/src/Transports/Pulsar/Wolverine.Pulsar/PulsarTransport.cs
@@ -1,6 +1,7 @@
using DotPulsar;
using DotPulsar.Abstractions;
using JasperFx.Core;
+using JasperFx.Descriptors;
using Wolverine.Configuration;
using Wolverine.Runtime;
using Wolverine.Transports;
@@ -23,6 +24,7 @@ public PulsarTransport() : base(ProtocolName, "Pulsar", ["pulsar"])
public PulsarEndpoint this[Uri uri] => _endpoints[uri];
+ [IgnoreDescription]
public IPulsarClientBuilder Builder { get; }
internal IPulsarClient? Client { get; private set; }
diff --git a/src/Transports/Pulsar/Wolverine.Pulsar/RetryLetterTopic.cs b/src/Transports/Pulsar/Wolverine.Pulsar/RetryLetterTopic.cs
index f286cf0ee..b0f40f354 100644
--- a/src/Transports/Pulsar/Wolverine.Pulsar/RetryLetterTopic.cs
+++ b/src/Transports/Pulsar/Wolverine.Pulsar/RetryLetterTopic.cs
@@ -39,6 +39,11 @@ public string? TopicName
set => _topicName = value ?? throw new ArgumentNullException(nameof(TopicName));
}
+ ///
+ /// Used by OptionsDescription to render this as the bare topic name.
+ ///
+ public override string ToString() => _topicName ?? string.Empty;
+
public List Retry => _retries.ToList();
protected bool Equals(RetryLetterTopic other)
diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/DeadLetterQueue.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/DeadLetterQueue.cs
index 4c9051ba5..570853032 100644
--- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/DeadLetterQueue.cs
+++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/DeadLetterQueue.cs
@@ -74,6 +74,13 @@ public DeadLetterQueue Clone()
};
}
+ public override string ToString()
+ {
+ // Used by OptionsDescription to render the value as the bare queue name
+ // rather than the default type-qualified name.
+ return _queueName;
+ }
+
protected bool Equals(DeadLetterQueue other)
{
return _queueName == other._queueName && ExchangeName == other.ExchangeName;
diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqConnectionDescription.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqConnectionDescription.cs
new file mode 100644
index 000000000..a42d509a9
--- /dev/null
+++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqConnectionDescription.cs
@@ -0,0 +1,53 @@
+using JasperFx.Descriptors;
+using RabbitMQ.Client;
+
+namespace Wolverine.RabbitMQ.Internal;
+
+///
+/// Secret-safe description of a for use with
+/// . Exposes non-secret connection fields
+/// (host, port, vhost, username, SSL, heartbeat) and deliberately omits the
+/// password and any credential-carrying properties.
+///
+public sealed class RabbitMqConnectionDescription : IDescribeMyself
+{
+ private readonly ConnectionFactory _factory;
+
+ public RabbitMqConnectionDescription(ConnectionFactory factory)
+ {
+ _factory = factory;
+ }
+
+ public OptionsDescription ToDescription()
+ {
+ var description = new OptionsDescription
+ {
+ Subject = "Wolverine.RabbitMQ.ConnectionFactory",
+ Title = "Connection"
+ };
+
+ description.AddValue(nameof(_factory.HostName), _factory.HostName);
+ description.AddValue(nameof(_factory.Port), _factory.Port);
+ description.AddValue(nameof(_factory.VirtualHost), _factory.VirtualHost);
+ description.AddValue(nameof(_factory.UserName), _factory.UserName);
+ // Password intentionally omitted.
+
+ description.AddValue(nameof(_factory.RequestedHeartbeat), _factory.RequestedHeartbeat);
+ description.AddValue(nameof(_factory.RequestedConnectionTimeout), _factory.RequestedConnectionTimeout);
+ description.AddValue(nameof(_factory.ClientProvidedName), _factory.ClientProvidedName ?? string.Empty);
+ description.AddValue(nameof(_factory.AutomaticRecoveryEnabled), _factory.AutomaticRecoveryEnabled);
+ description.AddValue(nameof(_factory.TopologyRecoveryEnabled), _factory.TopologyRecoveryEnabled);
+
+ if (_factory.Ssl != null)
+ {
+ description.AddValue("Ssl.Enabled", _factory.Ssl.Enabled);
+ if (_factory.Ssl.Enabled)
+ {
+ description.AddValue("Ssl.ServerName", _factory.Ssl.ServerName ?? string.Empty);
+ description.AddValue("Ssl.Version", _factory.Ssl.Version.ToString());
+ }
+ }
+
+ return description;
+ }
+}
diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqExchange.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqExchange.cs
index 86ecf2fe9..ce3cc05f3 100644
--- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqExchange.cs
+++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqExchange.cs
@@ -1,4 +1,5 @@
using JasperFx.Core;
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using Wolverine.Configuration;
@@ -32,11 +33,13 @@ internal RabbitMqExchange(string name, RabbitMqTransport parent)
///
/// All active topic endpoints by name
///
+ [IgnoreDescription]
public LightweightCache Topics { get; }
-
+
///
/// All active routing keys
///
+ [IgnoreDescription]
public LightweightCache Routings { get; }
public override bool AutoStartSendingAgent()
@@ -57,6 +60,7 @@ public override bool AutoStartSendingAgent()
public ExchangeType ExchangeType { get; set; } = ExchangeType.Fanout;
public bool AutoDelete { get; set; } = false;
+ [IgnoreDescription]
public IDictionary Arguments { get; } = new Dictionary();
internal bool HasExchangeBindings => _exchangeBindings.Count > 0;
diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqQueue.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqQueue.cs
index eb54434d4..d4bcd1056 100644
--- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqQueue.cs
+++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqQueue.cs
@@ -1,3 +1,4 @@
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using RabbitMQ.Client.Exceptions;
@@ -201,11 +202,13 @@ await _parent.WithAdminChannelAsync(async channel =>
/// Arguments for Rabbit MQ queue declarations. See the Rabbit MQ .NET client documentation at
/// https://www.rabbitmq.com/dotnet.html
///
+ [IgnoreDescription]
public IDictionary Arguments { get; } = new Dictionary();
///
/// Arguments for Rabbit MQ channel consume operations
///
+ [IgnoreDescription]
public IDictionary ConsumerArguments { get; } = new Dictionary();
///
diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqTransport.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqTransport.cs
index ec1d5c77d..5591a2f2b 100644
--- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqTransport.cs
+++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqTransport.cs
@@ -1,5 +1,6 @@
using System.Diagnostics;
using JasperFx.Core;
+using JasperFx.Descriptors;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -100,10 +101,21 @@ private void configureDefaults(ConnectionFactory factory)
/// Allows users to modify properties and behaviors of channels used in RabbitMQ communication
/// by providing a delegate to apply specific settings.
///
+ [ChildDescription]
public Action? ChannelCreationOptions { get; set; }
+ [IgnoreDescription]
public ConnectionFactory? ConnectionFactory { get; private set; }
+ ///
+ /// Secret-safe summary of for diagnostic
+ /// rendering. Exposes host / port / vhost / user / SSL / heartbeat and
+ /// omits the password.
+ ///
+ [ChildDescription]
+ public RabbitMqConnectionDescription? ConnectionDescription =>
+ ConnectionFactory == null ? null : new RabbitMqConnectionDescription(ConnectionFactory);
+
internal void ConfigureFactory(Action configure)
{
var factory = new ConnectionFactory
@@ -118,11 +130,15 @@ internal void ConfigureFactory(Action configure)
ConnectionFactory = factory;
}
+ [IgnoreDescription]
public IList AmqpTcpEndpoints { get; } = new List();
+ [IgnoreDescription]
public LightweightCache Topics { get; }
+ [IgnoreDescription]
public LightweightCache Exchanges { get; }
+ [IgnoreDescription]
public LightweightCache Queues { get; }
internal bool DeclareRequestReplySystemQueue { get; set; } = true;
@@ -336,6 +352,7 @@ internal ConnectionMonitor BuildConnection(ConnectionRole role)
return new ConnectionMonitor(this, role);
}
+ [IgnoreDescription]
public ILogger Logger { get; private set; } = NullLogger.Instance;
///
diff --git a/src/Transports/Redis/Wolverine.Redis/Internal/RedisTransport.cs b/src/Transports/Redis/Wolverine.Redis/Internal/RedisTransport.cs
index 972633fcf..3c7e7bd9f 100644
--- a/src/Transports/Redis/Wolverine.Redis/Internal/RedisTransport.cs
+++ b/src/Transports/Redis/Wolverine.Redis/Internal/RedisTransport.cs
@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using System.Linq;
using JasperFx.Core;
+using JasperFx.Descriptors;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
using Spectre.Console;
@@ -34,6 +35,7 @@ public class RedisTransport : BrokerTransport, IAsyncDispos
/// Customizable selector to build a stable consumer name for listeners when an endpoint-level ConsumerName is not set.
/// Defaults to ServiceName-NodeNumber-MachineName (lowercased and sanitized).
///
+ [DescribeAsConfigurationState]
public Func? DefaultConsumerNameSelector { get; set; }
///
@@ -71,16 +73,22 @@ public override Uri ResourceUri
// Parse connection string to build resource URI
var options = ConfigurationOptions.Parse(_connectionString);
var endpoint = options.EndPoints.FirstOrDefault();
-
+
if (endpoint == null)
{
return new Uri($"{ProtocolName}://localhost:6379");
}
-
+
return new Uri($"{ProtocolName}://{endpoint}");
}
}
+ ///
+ /// The configured Redis connection string with the password value
+ /// masked. Safe to render in diagnostic output.
+ ///
+ public string ConnectionSummary => SanitizeConnectionStringForLogging(_connectionString);
+
internal IDatabase GetDatabase(string? connectionString = null, int database = 0)
{
var connection = GetConnection(connectionString);
diff --git a/src/Transports/SignalR/Wolverine.SignalR/Client/SignalRClientEndpoint.cs b/src/Transports/SignalR/Wolverine.SignalR/Client/SignalRClientEndpoint.cs
index 5df9b9d7e..e9c690853 100644
--- a/src/Transports/SignalR/Wolverine.SignalR/Client/SignalRClientEndpoint.cs
+++ b/src/Transports/SignalR/Wolverine.SignalR/Client/SignalRClientEndpoint.cs
@@ -1,4 +1,5 @@
using JasperFx.Core;
+using JasperFx.Descriptors;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Logging;
@@ -37,8 +38,10 @@ public SignalRClientEndpoint(Uri uri, SignalRClientTransport parent) : base(Tran
JsonOptions = new SignalRTransport().JsonOptions;
}
+ [IgnoreDescription]
public JsonSerializerOptions JsonOptions { get; set; }
+ [DescribeAsConfigurationState]
public Func>> AccessTokenProvider { get; set; } = null!;
public Uri SignalRUri { get; }
@@ -105,6 +108,7 @@ internal async Task ReceiveAsync(string json)
}
}
+ [IgnoreDescription]
public IReceiver? Receiver { get; private set; }
protected override ISender CreateSender(IWolverineRuntime runtime)
@@ -119,6 +123,7 @@ protected override ISender CreateSender(IWolverineRuntime runtime)
return this;
}
+ [IgnoreDescription]
public IHandlerPipeline? Pipeline { get; private set; }
ValueTask IChannelCallback.CompleteAsync(Envelope envelope)
diff --git a/src/Transports/SignalR/Wolverine.SignalR/Internals/SignalRTransport.cs b/src/Transports/SignalR/Wolverine.SignalR/Internals/SignalRTransport.cs
index 771733d98..721413c23 100644
--- a/src/Transports/SignalR/Wolverine.SignalR/Internals/SignalRTransport.cs
+++ b/src/Transports/SignalR/Wolverine.SignalR/Internals/SignalRTransport.cs
@@ -1,6 +1,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using JasperFx.Core;
+using JasperFx.Descriptors;
using JasperFx.Resources;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
@@ -69,6 +70,7 @@ ValueTask ITransport.InitializeAsync(IWolverineRuntime runtime)
return new ValueTask();
}
+ [IgnoreDescription]
public IHubContext? HubContext { get; private set; }
public Type HubType { get; internal set; } = typeof(WolverineHub);
@@ -80,8 +82,10 @@ bool ITransport.TryBuildStatefulResource(IWolverineRuntime runtime, out IStatefu
internal ILogger? Logger { get; set; }
+ [IgnoreDescription]
public JsonSerializerOptions JsonOptions { get; set; }
+ [IgnoreDescription]
public IReceiver? Receiver { get; private set; }
internal async Task ReceiveAsync(HubCallerContext context, string json)
@@ -116,6 +120,7 @@ public override ValueTask BuildListenerAsync(IWolverineRuntime runtim
return new ValueTask(this);
}
+ [IgnoreDescription]
public IHandlerPipeline? Pipeline => Receiver?.Pipeline;
ValueTask IChannelCallback.CompleteAsync(Envelope envelope)
diff --git a/src/Wolverine/ErrorHandling/FailureRule.cs b/src/Wolverine/ErrorHandling/FailureRule.cs
index 2e7b01022..98107827a 100644
--- a/src/Wolverine/ErrorHandling/FailureRule.cs
+++ b/src/Wolverine/ErrorHandling/FailureRule.cs
@@ -55,4 +55,23 @@ public FailureSlot AddSlot(IContinuationSource source)
return slot;
}
+
+ public override string ToString()
+ {
+ var parts = new List(_slots.Count + 1);
+
+ foreach (var slot in _slots)
+ {
+ parts.Add($"attempt {slot.Attempt}: {slot.Describe()}");
+ }
+
+ if (InfiniteSource != null)
+ {
+ var prefix = _slots.Count > 0 ? "then repeat" : "repeat";
+ parts.Add($"{prefix}: {InfiniteSource.Description}");
+ }
+
+ var actions = parts.Count > 0 ? string.Join("; ", parts) : "no action";
+ return $"On {Match.Description} \u2014 {actions}";
+ }
}
\ No newline at end of file
diff --git a/src/Wolverine/ErrorHandling/SendingFailurePolicies.cs b/src/Wolverine/ErrorHandling/SendingFailurePolicies.cs
index c8ccbcc35..6b92b53d5 100644
--- a/src/Wolverine/ErrorHandling/SendingFailurePolicies.cs
+++ b/src/Wolverine/ErrorHandling/SendingFailurePolicies.cs
@@ -1,3 +1,4 @@
+using JasperFx.Descriptors;
using Wolverine.Runtime;
namespace Wolverine.ErrorHandling;
@@ -7,8 +8,15 @@ namespace Wolverine.ErrorHandling;
/// Unlike handler failure policies, unmatched exceptions return null
/// so the existing retry/circuit-breaker behavior is preserved.
///
-public class SendingFailurePolicies : IWithFailurePolicies
+public class SendingFailurePolicies : IWithFailurePolicies, IOptionsValueAsStringArray
{
+ ///
+ /// Renders the failure rules as a string array so
+ /// shows one entry per rule instead of the default class ToString().
+ ///
+ public IReadOnlyList ToOptionsValueStrings()
+ => Failures.Select(rule => rule.ToString()!).ToArray();
+
///
/// Collection of error handling policies for exception handling during the sending of a message
///
diff --git a/src/Wolverine/Runtime/Handlers/HandlerGraph.ISystemDescribedPart.cs b/src/Wolverine/Runtime/Handlers/HandlerGraph.ISystemDescribedPart.cs
index 5e0a3d607..17cd9afec 100644
--- a/src/Wolverine/Runtime/Handlers/HandlerGraph.ISystemDescribedPart.cs
+++ b/src/Wolverine/Runtime/Handlers/HandlerGraph.ISystemDescribedPart.cs
@@ -62,7 +62,7 @@ internal Task WriteToConsole()
}
}
- AnsiConsole.Render(table);
+ AnsiConsole.Write(table);
}
return Task.CompletedTask;
diff --git a/src/Wolverine/Runtime/Interop/CloudEventsMapper.cs b/src/Wolverine/Runtime/Interop/CloudEventsMapper.cs
index da388f1e1..4929e3613 100644
--- a/src/Wolverine/Runtime/Interop/CloudEventsMapper.cs
+++ b/src/Wolverine/Runtime/Interop/CloudEventsMapper.cs
@@ -92,6 +92,8 @@ public CloudEventsMapper(HandlerGraph handlers, JsonSerializerOptions options)
_options = options;
}
+ public override string ToString() => "Cloud Events";
+
public string WriteToString(Envelope envelope)
{
return JsonSerializer.Serialize(new CloudEventsEnvelope(envelope), _options);
diff --git a/src/Wolverine/Transports/BrokerResource.cs b/src/Wolverine/Transports/BrokerResource.cs
index a2dac1ee4..1c647c4ff 100644
--- a/src/Wolverine/Transports/BrokerResource.cs
+++ b/src/Wolverine/Transports/BrokerResource.cs
@@ -95,10 +95,7 @@ public async Task Setup(CancellationToken token)
public async Task DetermineStatus(CancellationToken token)
{
- var table = new Table
- {
- Alignment = Justify.Left
- };
+ var table = new Table();
var columns = _transport.DiagnosticColumns().ToArray();
if (columns.Length == 0) return table;
diff --git a/src/Wolverine/Transports/ConnectionStringRedactor.cs b/src/Wolverine/Transports/ConnectionStringRedactor.cs
new file mode 100644
index 000000000..17ea967e2
--- /dev/null
+++ b/src/Wolverine/Transports/ConnectionStringRedactor.cs
@@ -0,0 +1,61 @@
+using System.Text;
+
+namespace Wolverine.Transports;
+
+///
+/// Small helper for masking credential-bearing keys in a connection string
+/// while preserving the rest of the content. Used to render a transport's
+/// connection string in
+/// output without leaking secrets into logs or diagnostic UIs.
+///
+public static class ConnectionStringRedactor
+{
+ ///
+ /// Mask the values of any key=value segments whose key (case-insensitive)
+ /// appears in . Segments are delimited by
+ /// ;. Unknown keys pass through unchanged.
+ ///
+ public static string Redact(string? connectionString, params string[] secretKeys)
+ {
+ if (string.IsNullOrWhiteSpace(connectionString)) return string.Empty;
+ if (secretKeys == null || secretKeys.Length == 0) return connectionString!;
+
+ var builder = new StringBuilder(connectionString!.Length);
+ var segments = connectionString.Split(';');
+ for (var i = 0; i < segments.Length; i++)
+ {
+ var segment = segments[i];
+ if (segment.Length == 0) continue;
+
+ var equalsIndex = segment.IndexOf('=');
+ if (equalsIndex > 0)
+ {
+ var key = segment.Substring(0, equalsIndex);
+ if (IsSecretKey(key, secretKeys))
+ {
+ if (builder.Length > 0) builder.Append(';');
+ builder.Append(key).Append("=****");
+ continue;
+ }
+ }
+
+ if (builder.Length > 0) builder.Append(';');
+ builder.Append(segment);
+ }
+
+ return builder.ToString();
+ }
+
+ private static bool IsSecretKey(string key, string[] secretKeys)
+ {
+ foreach (var secret in secretKeys)
+ {
+ if (string.Equals(key.Trim(), secret, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}