diff --git a/Directory.Packages.props b/Directory.Packages.props
index ccd07c441..d8a949727 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -75,6 +75,7 @@
+
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Playing.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Playing.cs
new file mode 100644
index 000000000..142ab7ea3
--- /dev/null
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Playing.cs
@@ -0,0 +1,28 @@
+using DotNet.Testcontainers.Builders;
+using Testcontainers.ServiceBus;
+using Xunit;
+
+namespace Wolverine.AzureServiceBus.Tests;
+
+public class Playing
+{
+ private readonly ServiceBusContainer _serviceBusContainer;
+ public const ushort ServiceBusPort = 5672;
+ public const ushort ServiceBusHttpPort = 5300;
+
+ [Fact]
+ public async Task spin_up_container()
+ {
+
+ var container = new ServiceBusBuilder()
+ .WithImage("mcr.microsoft.com/azure-messaging/servicebus-emulator:latest")
+ .WithAcceptLicenseAgreement(true)
+ .WithPortBinding(ServiceBusPort, true)
+ .WithPortBinding(ServiceBusHttpPort, true)
+ .WithEnvironment("SQL_WAIT_INTERVAL", "0")
+ .WithResourceMapping("Config.json", "/ServiceBus_Emulator/ConfigFiles/")
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request =>
+ request.ForPort(ServiceBusHttpPort).ForPath("/health")))
+ .Build();
+ }
+}
\ No newline at end of file
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Wolverine.AzureServiceBus.Tests.csproj b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Wolverine.AzureServiceBus.Tests.csproj
index 6c9b78049..8c0eb4e24 100644
--- a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Wolverine.AzureServiceBus.Tests.csproj
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Wolverine.AzureServiceBus.Tests.csproj
@@ -6,6 +6,7 @@
+
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/Emulator/Code.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/Emulator/Code.cs
new file mode 100644
index 000000000..5e375d5b8
--- /dev/null
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus/Emulator/Code.cs
@@ -0,0 +1,661 @@
+//
+//
+// To parse this JSON data, add NuGet 'System.Text.Json' then do:
+//
+// using QuickType;
+//
+// var azureServiceBusEmulatorConfig = AzureServiceBusEmulatorConfig.FromJson(jsonString);
+#nullable enable
+#pragma warning disable CS8618
+#pragma warning disable CS8601
+#pragma warning disable CS8603
+
+namespace Wolverine.AzureServiceBus.Emulator
+{
+ using System;
+ using System.Collections.Generic;
+
+ using System.Text.Json;
+ using System.Text.Json.Serialization;
+ using System.Globalization;
+
+ ///
+ /// Main user configuration object
+ ///
+ public partial class AzureServiceBusEmulatorConfig
+ {
+ [JsonPropertyName("UserConfig")]
+ public UserConfig UserConfig { get; set; }
+ }
+
+ public partial class UserConfig
+ {
+ ///
+ /// Configuration for logging is defined here
+ ///
+ [JsonPropertyName("Logging")]
+ public Logging Logging { get; set; }
+
+ ///
+ /// Namespace configuration is defined here
+ ///
+ [JsonPropertyName("Namespaces")]
+ public List Namespaces { get; set; }
+ }
+
+ ///
+ /// Configuration for logging is defined here
+ ///
+ public partial class Logging
+ {
+ ///
+ /// Types of logging
+ ///
+ [JsonPropertyName("Type")]
+ public TypeEnum Type { get; set; }
+ }
+
+ public partial class Namespace
+ {
+ ///
+ /// Name of the namespace
+ ///
+ [JsonPropertyName("Name")]
+ public Name Name { get; set; }
+
+ ///
+ /// Queue configuration is defined here. Max Queues + Topics per namespace is 50
+ ///
+ [JsonPropertyName("Queues")]
+ public List Queues { get; set; }
+
+ ///
+ /// Topic configuration is defined here. Max Queues + Topics per namespace is 50
+ ///
+ [JsonPropertyName("Topics")]
+ public List Topics { get; set; }
+ }
+
+ public partial class Queue
+ {
+ ///
+ /// Name of the queue
+ ///
+ [JsonPropertyName("Name")]
+ public string Name { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("Properties")]
+ public QueueProperties Properties { get; set; }
+ }
+
+ public partial class QueueProperties
+ {
+ ///
+ /// Indicates whether this queue has dead letter support when a message expires
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("DeadLetteringOnMessageExpiration")]
+ public bool? DeadLetteringOnMessageExpiration { get; set; }
+
+ ///
+ /// Specifies the default time-to-live for messages. Must be a valid ISO 8601 datetime
+ /// string. The default value is PT1H (1 hour), the minimum value is PT5M (5 minutes) and the
+ /// maximum value is PT1H (1 hour).
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("DefaultMessageTimeToLive")]
+ public string DefaultMessageTimeToLive { get; set; }
+
+ ///
+ /// Specifies the time window for duplicate detection. Must be a valid ISO 8601 datetime
+ /// string. The default value is PT20S (20 seconds), the minimum value is PT5S (5 seconds)
+ /// and the maximum value is PT5M (5 minutes).
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("DuplicateDetectionHistoryTimeWindow")]
+ public string DuplicateDetectionHistoryTimeWindow { get; set; }
+
+ ///
+ /// Forward dead lettered messages to
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("ForwardDeadLetteredMessagesTo")]
+ public string ForwardDeadLetteredMessagesTo { get; set; }
+
+ ///
+ /// Forward to
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("ForwardTo")]
+ public string ForwardTo { get; set; }
+
+ ///
+ /// Specifies the duration of a peek lock. Must be a valid ISO 8601 datetime string. The
+ /// default value is PT1M (1 minute), the minimum value is PT5S (5 seconds) and the maximum
+ /// value is PT5M (5 minutes).
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("LockDuration")]
+ public string LockDuration { get; set; }
+
+ ///
+ /// Specifies the maximum number of times a message delivery is attempted before it is moved
+ /// to the dead-letter queue.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("MaxDeliveryCount")]
+ public long? MaxDeliveryCount { get; set; }
+
+ ///
+ /// Indicates whether this queue requires duplicate detection
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("RequiresDuplicateDetection")]
+ public bool? RequiresDuplicateDetection { get; set; }
+
+ ///
+ /// Indicates whether this queue requires session support
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("RequiresSession")]
+ public bool? RequiresSession { get; set; }
+ }
+
+ public partial class Topic
+ {
+ ///
+ /// Name of the topic
+ ///
+ [JsonPropertyName("Name")]
+ public string Name { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("Properties")]
+ public TopicProperties Properties { get; set; }
+
+ ///
+ /// Subscription configuration is defined here
+ ///
+ [JsonPropertyName("Subscriptions")]
+ public List Subscriptions { get; set; }
+ }
+
+ public partial class TopicProperties
+ {
+ ///
+ /// Specifies the default time-to-live for messages. Must be a valid ISO 8601 datetime
+ /// string. The default value is PT1H (1 hour), the minimum value is PT5M (5 minutes) and the
+ /// maximum value is PT1H (1 hour).
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("DefaultMessageTimeToLive")]
+ public string DefaultMessageTimeToLive { get; set; }
+
+ ///
+ /// Specifies the time window for duplicate detection. Must be a valid ISO 8601 datetime
+ /// string. The default value is PT20S (20 seconds), the minimum value is PT5S (5 seconds)
+ /// and the maximum value is PT5M (5 minutes).
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("DuplicateDetectionHistoryTimeWindow")]
+ public string DuplicateDetectionHistoryTimeWindow { get; set; }
+
+ ///
+ /// Indicates whether this topic requires duplicate detection
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("RequiresDuplicateDetection")]
+ public bool? RequiresDuplicateDetection { get; set; }
+ }
+
+ public partial class Subscription
+ {
+ ///
+ /// Name of the subscription
+ ///
+ [JsonPropertyName("Name")]
+ public string Name { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("Properties")]
+ public SubscriptionProperties Properties { get; set; }
+
+ ///
+ /// Rule configuration is defined here. Maximum Rules per topic is 1000
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("Rules")]
+ public List Rules { get; set; }
+ }
+
+ public partial class SubscriptionProperties
+ {
+ ///
+ /// Indicates whether this subscription has dead letter support when a message expires
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("DeadLetteringOnMessageExpiration")]
+ public bool? DeadLetteringOnMessageExpiration { get; set; }
+
+ ///
+ /// Specifies the default time-to-live for messages. Must be a valid ISO 8601 datetime
+ /// string. The default value is PT1H (1 hour), the minimum value is PT5M (5 minutes) and the
+ /// maximum value is PT1H (1 hour).
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("DefaultMessageTimeToLive")]
+ public string DefaultMessageTimeToLive { get; set; }
+
+ ///
+ /// Forward dead lettered messages to
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("ForwardDeadLetteredMessagesTo")]
+ public string ForwardDeadLetteredMessagesTo { get; set; }
+
+ ///
+ /// Forward to
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("ForwardTo")]
+ public string ForwardTo { get; set; }
+
+ ///
+ /// Specifies the duration of a peek lock. Must be a valid ISO 8601 datetime string. The
+ /// default value is PT1M (1 minute), the minimum value is PT1M (1 minute) and the maximum
+ /// value is PT5M (5 minutes).
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("LockDuration")]
+ public string LockDuration { get; set; }
+
+ ///
+ /// Specifies the maximum number of times a message delivery is attempted before it is moved
+ /// to the dead-letter queue.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("MaxDeliveryCount")]
+ public long? MaxDeliveryCount { get; set; }
+
+ ///
+ /// Indicates whether this queue requires session support
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("RequiresSession")]
+ public bool? RequiresSession { get; set; }
+ }
+
+ public partial class Rule
+ {
+ ///
+ /// Name of the rule
+ ///
+ [JsonPropertyName("Name")]
+ public string Name { get; set; }
+
+ [JsonPropertyName("Properties")]
+ public RuleProperties Properties { get; set; }
+ }
+
+ public partial class RuleProperties
+ {
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("Action")]
+ public Action Action { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("CorrelationFilter")]
+ public CorrelationFilter CorrelationFilter { get; set; }
+
+ ///
+ /// Type of filter
+ ///
+ [JsonPropertyName("FilterType")]
+ public FilterType FilterType { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("SqlFilter")]
+ public SqlFilter SqlFilter { get; set; }
+ }
+
+ public partial class Action
+ {
+ ///
+ /// SQL expression for the action
+ ///
+ [JsonPropertyName("SqlExpression")]
+ public string SqlExpression { get; set; }
+ }
+
+ public partial class CorrelationFilter
+ {
+ ///
+ /// Content type
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("ContentType")]
+ public string ContentType { get; set; }
+
+ ///
+ /// Correlation id
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("CorrelationId")]
+ public string CorrelationId { get; set; }
+
+ ///
+ /// Label
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("Label")]
+ public string Label { get; set; }
+
+ ///
+ /// Message id
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("MessageId")]
+ public string MessageId { get; set; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("Properties")]
+ public Dictionary Properties { get; set; }
+
+ ///
+ /// Reply to
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("ReplyTo")]
+ public string ReplyTo { get; set; }
+
+ ///
+ /// Reply to session id
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("ReplyToSessionId")]
+ public string ReplyToSessionId { get; set; }
+
+ ///
+ /// Session id
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("SessionId")]
+ public string SessionId { get; set; }
+
+ ///
+ /// To
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("To")]
+ public string To { get; set; }
+ }
+
+ public partial class SqlFilter
+ {
+ ///
+ /// SQL expression for the filter
+ ///
+ [JsonPropertyName("SqlExpression")]
+ public string SqlExpression { get; set; }
+ }
+
+ ///
+ /// Types of logging
+ ///
+ public enum TypeEnum { Console, ConsoleFile, File, FileConsole };
+
+ ///
+ /// Name of the namespace
+ ///
+ public enum Name { Sbemulatorns };
+
+ ///
+ /// Type of filter
+ ///
+ public enum FilterType { Correlation, Sql };
+
+ public partial class AzureServiceBusEmulatorConfig
+ {
+ public static AzureServiceBusEmulatorConfig FromJson(string json) => JsonSerializer.Deserialize(json, Converter.Settings);
+ }
+
+ public static class Serialize
+ {
+ public static string ToJson(this AzureServiceBusEmulatorConfig self) => JsonSerializer.Serialize(self, Converter.Settings);
+ }
+
+ internal static class Converter
+ {
+ public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General)
+ {
+ Converters =
+ {
+ TypeEnumConverter.Singleton,
+ NameConverter.Singleton,
+ FilterTypeConverter.Singleton,
+ new DateOnlyConverter(),
+ new TimeOnlyConverter(),
+ IsoDateTimeOffsetConverter.Singleton
+ },
+ };
+ }
+
+ internal class TypeEnumConverter : JsonConverter
+ {
+ public override bool CanConvert(Type t) => t == typeof(TypeEnum);
+
+ public override TypeEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+ switch (value)
+ {
+ case "Console":
+ return TypeEnum.Console;
+ case "Console,File":
+ return TypeEnum.ConsoleFile;
+ case "File":
+ return TypeEnum.File;
+ case "File,Console":
+ return TypeEnum.FileConsole;
+ }
+ throw new Exception("Cannot unmarshal type TypeEnum");
+ }
+
+ public override void Write(Utf8JsonWriter writer, TypeEnum value, JsonSerializerOptions options)
+ {
+ switch (value)
+ {
+ case TypeEnum.Console:
+ JsonSerializer.Serialize(writer, "Console", options);
+ return;
+ case TypeEnum.ConsoleFile:
+ JsonSerializer.Serialize(writer, "Console,File", options);
+ return;
+ case TypeEnum.File:
+ JsonSerializer.Serialize(writer, "File", options);
+ return;
+ case TypeEnum.FileConsole:
+ JsonSerializer.Serialize(writer, "File,Console", options);
+ return;
+ }
+ throw new Exception("Cannot marshal type TypeEnum");
+ }
+
+ public static readonly TypeEnumConverter Singleton = new TypeEnumConverter();
+ }
+
+ internal class NameConverter : JsonConverter
+ {
+ public override bool CanConvert(Type t) => t == typeof(Name);
+
+ public override Name Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+ if (value == "sbemulatorns")
+ {
+ return Name.Sbemulatorns;
+ }
+ throw new Exception("Cannot unmarshal type Name");
+ }
+
+ public override void Write(Utf8JsonWriter writer, Name value, JsonSerializerOptions options)
+ {
+ if (value == Name.Sbemulatorns)
+ {
+ JsonSerializer.Serialize(writer, "sbemulatorns", options);
+ return;
+ }
+ throw new Exception("Cannot marshal type Name");
+ }
+
+ public static readonly NameConverter Singleton = new NameConverter();
+ }
+
+ internal class FilterTypeConverter : JsonConverter
+ {
+ public override bool CanConvert(Type t) => t == typeof(FilterType);
+
+ public override FilterType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+ switch (value)
+ {
+ case "Correlation":
+ return FilterType.Correlation;
+ case "Sql":
+ return FilterType.Sql;
+ }
+ throw new Exception("Cannot unmarshal type FilterType");
+ }
+
+ public override void Write(Utf8JsonWriter writer, FilterType value, JsonSerializerOptions options)
+ {
+ switch (value)
+ {
+ case FilterType.Correlation:
+ JsonSerializer.Serialize(writer, "Correlation", options);
+ return;
+ case FilterType.Sql:
+ JsonSerializer.Serialize(writer, "Sql", options);
+ return;
+ }
+ throw new Exception("Cannot marshal type FilterType");
+ }
+
+ public static readonly FilterTypeConverter Singleton = new FilterTypeConverter();
+ }
+
+ public class DateOnlyConverter : JsonConverter
+ {
+ private readonly string serializationFormat;
+ public DateOnlyConverter() : this(null) { }
+
+ public DateOnlyConverter(string? serializationFormat)
+ {
+ this.serializationFormat = serializationFormat ?? "yyyy-MM-dd";
+ }
+
+ public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+ return DateOnly.Parse(value!);
+ }
+
+ public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
+ => writer.WriteStringValue(value.ToString(serializationFormat));
+ }
+
+ public class TimeOnlyConverter : JsonConverter
+ {
+ private readonly string serializationFormat;
+
+ public TimeOnlyConverter() : this(null) { }
+
+ public TimeOnlyConverter(string? serializationFormat)
+ {
+ this.serializationFormat = serializationFormat ?? "HH:mm:ss.fff";
+ }
+
+ public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+ return TimeOnly.Parse(value!);
+ }
+
+ public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options)
+ => writer.WriteStringValue(value.ToString(serializationFormat));
+ }
+
+ internal class IsoDateTimeOffsetConverter : JsonConverter
+ {
+ public override bool CanConvert(Type t) => t == typeof(DateTimeOffset);
+
+ private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK";
+
+ private DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind;
+ private string? _dateTimeFormat;
+ private CultureInfo? _culture;
+
+ public DateTimeStyles DateTimeStyles
+ {
+ get => _dateTimeStyles;
+ set => _dateTimeStyles = value;
+ }
+
+ public string? DateTimeFormat
+ {
+ get => _dateTimeFormat ?? string.Empty;
+ set => _dateTimeFormat = (string.IsNullOrEmpty(value)) ? null : value;
+ }
+
+ public CultureInfo Culture
+ {
+ get => _culture ?? CultureInfo.CurrentCulture;
+ set => _culture = value;
+ }
+
+ public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
+ {
+ string text;
+
+
+ if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal
+ || (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal)
+ {
+ value = value.ToUniversalTime();
+ }
+
+ text = value.ToString(_dateTimeFormat ?? DefaultDateTimeFormat, Culture);
+
+ writer.WriteStringValue(text);
+ }
+
+ public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ string? dateText = reader.GetString();
+
+ if (string.IsNullOrEmpty(dateText) == false)
+ {
+ if (!string.IsNullOrEmpty(_dateTimeFormat))
+ {
+ return DateTimeOffset.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles);
+ }
+ else
+ {
+ return DateTimeOffset.Parse(dateText, Culture, _dateTimeStyles);
+ }
+ }
+ else
+ {
+ return default(DateTimeOffset);
+ }
+ }
+
+
+ public static readonly IsoDateTimeOffsetConverter Singleton = new IsoDateTimeOffsetConverter();
+ }
+}
+#pragma warning restore CS8618
+#pragma warning restore CS8601
+#pragma warning restore CS8603
diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/Emulator/Config.json b/src/Transports/Azure/Wolverine.AzureServiceBus/Emulator/Config.json
new file mode 100644
index 000000000..49fe1fd31
--- /dev/null
+++ b/src/Transports/Azure/Wolverine.AzureServiceBus/Emulator/Config.json
@@ -0,0 +1,135 @@
+{
+ "UserConfig": {
+ "Namespaces": [
+ {
+ "Name": "sbemulatorns",
+ "Queues": [
+ {
+ "Name": "queue.1",
+ "Properties": {
+ "DeadLetteringOnMessageExpiration": false,
+ "DefaultMessageTimeToLive": "PT1H",
+ "DuplicateDetectionHistoryTimeWindow": "PT20S",
+ "ForwardDeadLetteredMessagesTo": "",
+ "ForwardTo": "",
+ "LockDuration": "PT1M",
+ "MaxDeliveryCount": 3,
+ "RequiresDuplicateDetection": false,
+ "RequiresSession": false
+ }
+ }
+ ],
+
+ "Topics": [
+ {
+ "Name": "topic.1",
+ "Properties": {
+ "DefaultMessageTimeToLive": "PT1H",
+ "DuplicateDetectionHistoryTimeWindow": "PT20S",
+ "RequiresDuplicateDetection": false
+ },
+ "Subscriptions": [
+ {
+ "Name": "subscription.1",
+ "Properties": {
+ "DeadLetteringOnMessageExpiration": false,
+ "DefaultMessageTimeToLive": "PT1H",
+ "LockDuration": "PT1M",
+ "MaxDeliveryCount": 3,
+ "ForwardDeadLetteredMessagesTo": "",
+ "ForwardTo": "",
+ "RequiresSession": false
+ },
+ "Rules": [
+ {
+ "Name": "app-prop-filter-1",
+ "Properties": {
+ "FilterType": "Correlation",
+ "CorrelationFilter": {
+ "ContentType": "application/json"
+ // Other supported properties
+ // "CorrelationId": "id1",
+ // "Label": "subject1",
+ // "MessageId": "msgid1",
+ // "ReplyTo": "someQueue",
+ // "ReplyToSessionId": "sessionId",
+ // "SessionId": "session1",
+ // "To": "xyz"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "Name": "subscription.2",
+ "Properties": {
+ "DeadLetteringOnMessageExpiration": false,
+ "DefaultMessageTimeToLive": "PT1H",
+ "LockDuration": "PT1M",
+ "MaxDeliveryCount": 3,
+ "ForwardDeadLetteredMessagesTo": "",
+ "ForwardTo": "",
+ "RequiresSession": false
+ },
+ "Rules": [
+ {
+ "Name": "user-prop-filter-1",
+ "Properties": {
+ "FilterType": "Correlation",
+ "CorrelationFilter": {
+ "Properties": {
+ "prop1": "value1"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "Name": "subscription.3",
+ "Properties": {
+ "DeadLetteringOnMessageExpiration": false,
+ "DefaultMessageTimeToLive": "PT1H",
+ "LockDuration": "PT1M",
+ "MaxDeliveryCount": 3,
+ "ForwardDeadLetteredMessagesTo": "",
+ "ForwardTo": "",
+ "RequiresSession": false
+ }
+ },
+ {
+ "Name": "subscription.4",
+ "Properties": {
+ "DeadLetteringOnMessageExpiration": false,
+ "DefaultMessageTimeToLive": "PT1H",
+ "LockDuration": "PT1M",
+ "MaxDeliveryCount": 3,
+ "ForwardDeadLetteredMessagesTo": "",
+ "ForwardTo": "",
+ "RequiresSession": false
+ },
+ "Rules": [
+ {
+ "Name": "sql-filter-1",
+ "Properties": {
+ "FilterType": "Sql",
+ "SqlFilter": {
+ "SqlExpression": "sys.MessageId = '123456' AND userProp1 = 'value1'"
+ },
+ "Action" : {
+ "SqlExpression": "SET sys.To = 'Entity'"
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "Logging": {
+ "Type": "File"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/end_to_end.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/end_to_end.cs
index 14280a20b..5eca04a66 100644
--- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/end_to_end.cs
+++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/end_to_end.cs
@@ -81,12 +81,6 @@ public async Task rabbitmq_transport_is_exposed_as_a_resource()
[Fact]
public async Task find_endpoints_through_conventions_as_part_of_find_resources()
{
- if (DateTimeOffset.UtcNow > new DateTime(2025, 12, 25))
- {
- // Uncomment code in WolverineSystemPart
- throw new Exception("Jeremy, you need to go try to fix this again!");
- }
-
using var host = Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
diff --git a/src/Wolverine/WolverineSystemPart.cs b/src/Wolverine/WolverineSystemPart.cs
index e29a30136..1084999d5 100644
--- a/src/Wolverine/WolverineSystemPart.cs
+++ b/src/Wolverine/WolverineSystemPart.cs
@@ -210,18 +210,26 @@ public override async ValueTask> FindResources(
list.Add(resource!);
}
}
-
- // TODO -- this did not work. Try again by the end of 2025
-
+
// Force Wolverine to find all message types...
- // var messageTypes = _runtime.Options.Discovery.FindAllMessages(_runtime.Options.HandlerGraph);
- //
- // // ...and force Wolverine to *also* execute the routing, which
- // // may discover new endpoints
- // foreach (var messageType in messageTypes.Where(x => x.Assembly != GetType().Assembly))
- // {
- // _runtime.RoutingFor(messageType);
- // }
+ var messageTypes = _runtime.Options.Discovery.FindAllMessages(_runtime.Options.HandlerGraph);
+
+ // ...and force Wolverine to *also* execute the routing, which
+ // may discover new endpoints
+ foreach (var messageType in messageTypes.Where(x => x.Assembly != GetType().Assembly))
+ {
+ _runtime.RoutingFor(messageType);
+ }
+
+ // Clear the cached routes created above so they'll be properly
+ // rebuilt with active sending agents when the host actually starts.
+ // Routes created during FindResources() have null Sender because
+ // WithinDescription=true allows that, but those cached routes would
+ // cause NullReferenceExceptions if reused for actual message sending.
+ foreach (var messageType in messageTypes.Where(x => x.Assembly != GetType().Assembly))
+ {
+ _runtime.ClearRoutingFor(messageType);
+ }
}
var stores = await _runtime.Stores.FindAllAsync();
diff --git a/wolverine.sln b/wolverine.sln
index 0f06c1097..c9a462b9f 100644
--- a/wolverine.sln
+++ b/wolverine.sln
@@ -214,12 +214,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.MQTT", "src\Trans
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.MQTT.Tests", "src\Transports\MQTT\Wolverine.MQTT.Tests\Wolverine.MQTT.Tests.csproj", "{F0F8EA19-0AB7-4D23-944A-59DA514B20D0}"
EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NATS", "NATS", "{85FFF74F-5762-4E11-A328-2BD4794281E6}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.Nats", "src\Transports\NATS\Wolverine.Nats\Wolverine.Nats.csproj", "{F939E430-43AB-435D-B74F-9FC7DAFE5074}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.Nats.Tests", "src\Transports\NATS\Wolverine.Nats.Tests\Wolverine.Nats.Tests.csproj", "{3C9ACC27-F486-4765-B703-ED64A34E28C6}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.AdminApi", "src\Http\Wolverine.AdminApi\Wolverine.AdminApi.csproj", "{9A3741FD-C0EF-4275-8CB0-77D6EB407E88}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChaosSender", "src\Samples\ChaosSender\ChaosSender.csproj", "{6F6FB8FC-564C-4B04-B254-EB53A7E4562F}"
@@ -350,6 +344,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.Oracle", "src\Per
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OracleTests", "src\Persistence\Oracle\OracleTests\OracleTests.csproj", "{C86E3CE6-3A97-451F-945D-61B3D5070160}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NATS", "NATS", "{3FE8E499-BE44-4E27-815C-39A4DB2C4EA1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.Nats", "src\Transports\NATS\Wolverine.Nats\Wolverine.Nats.csproj", "{1F04214E-E901-436A-A05F-6BB9ED375019}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wolverine.Nats.Tests", "src\Transports\NATS\Wolverine.Nats.Tests\Wolverine.Nats.Tests.csproj", "{77B49C73-29B7-47A5-9475-AC290F53D76D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1248,30 +1248,6 @@ Global
{F0F8EA19-0AB7-4D23-944A-59DA514B20D0}.Release|x64.Build.0 = Release|Any CPU
{F0F8EA19-0AB7-4D23-944A-59DA514B20D0}.Release|x86.ActiveCfg = Release|Any CPU
{F0F8EA19-0AB7-4D23-944A-59DA514B20D0}.Release|x86.Build.0 = Release|Any CPU
- {F939E430-43AB-435D-B74F-9FC7DAFE5074}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F939E430-43AB-435D-B74F-9FC7DAFE5074}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F939E430-43AB-435D-B74F-9FC7DAFE5074}.Debug|x64.ActiveCfg = Debug|Any CPU
- {F939E430-43AB-435D-B74F-9FC7DAFE5074}.Debug|x64.Build.0 = Debug|Any CPU
- {F939E430-43AB-435D-B74F-9FC7DAFE5074}.Debug|x86.ActiveCfg = Debug|Any CPU
- {F939E430-43AB-435D-B74F-9FC7DAFE5074}.Debug|x86.Build.0 = Debug|Any CPU
- {F939E430-43AB-435D-B74F-9FC7DAFE5074}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F939E430-43AB-435D-B74F-9FC7DAFE5074}.Release|Any CPU.Build.0 = Release|Any CPU
- {F939E430-43AB-435D-B74F-9FC7DAFE5074}.Release|x64.ActiveCfg = Release|Any CPU
- {F939E430-43AB-435D-B74F-9FC7DAFE5074}.Release|x64.Build.0 = Release|Any CPU
- {F939E430-43AB-435D-B74F-9FC7DAFE5074}.Release|x86.ActiveCfg = Release|Any CPU
- {F939E430-43AB-435D-B74F-9FC7DAFE5074}.Release|x86.Build.0 = Release|Any CPU
- {3C9ACC27-F486-4765-B703-ED64A34E28C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3C9ACC27-F486-4765-B703-ED64A34E28C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3C9ACC27-F486-4765-B703-ED64A34E28C6}.Debug|x64.ActiveCfg = Debug|Any CPU
- {3C9ACC27-F486-4765-B703-ED64A34E28C6}.Debug|x64.Build.0 = Debug|Any CPU
- {3C9ACC27-F486-4765-B703-ED64A34E28C6}.Debug|x86.ActiveCfg = Debug|Any CPU
- {3C9ACC27-F486-4765-B703-ED64A34E28C6}.Debug|x86.Build.0 = Debug|Any CPU
- {3C9ACC27-F486-4765-B703-ED64A34E28C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3C9ACC27-F486-4765-B703-ED64A34E28C6}.Release|Any CPU.Build.0 = Release|Any CPU
- {3C9ACC27-F486-4765-B703-ED64A34E28C6}.Release|x64.ActiveCfg = Release|Any CPU
- {3C9ACC27-F486-4765-B703-ED64A34E28C6}.Release|x64.Build.0 = Release|Any CPU
- {3C9ACC27-F486-4765-B703-ED64A34E28C6}.Release|x86.ActiveCfg = Release|Any CPU
- {3C9ACC27-F486-4765-B703-ED64A34E28C6}.Release|x86.Build.0 = Release|Any CPU
{9A3741FD-C0EF-4275-8CB0-77D6EB407E88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9A3741FD-C0EF-4275-8CB0-77D6EB407E88}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A3741FD-C0EF-4275-8CB0-77D6EB407E88}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -1956,6 +1932,30 @@ Global
{C86E3CE6-3A97-451F-945D-61B3D5070160}.Release|x64.Build.0 = Release|Any CPU
{C86E3CE6-3A97-451F-945D-61B3D5070160}.Release|x86.ActiveCfg = Release|Any CPU
{C86E3CE6-3A97-451F-945D-61B3D5070160}.Release|x86.Build.0 = Release|Any CPU
+ {1F04214E-E901-436A-A05F-6BB9ED375019}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1F04214E-E901-436A-A05F-6BB9ED375019}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1F04214E-E901-436A-A05F-6BB9ED375019}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1F04214E-E901-436A-A05F-6BB9ED375019}.Debug|x64.Build.0 = Debug|Any CPU
+ {1F04214E-E901-436A-A05F-6BB9ED375019}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1F04214E-E901-436A-A05F-6BB9ED375019}.Debug|x86.Build.0 = Debug|Any CPU
+ {1F04214E-E901-436A-A05F-6BB9ED375019}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1F04214E-E901-436A-A05F-6BB9ED375019}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1F04214E-E901-436A-A05F-6BB9ED375019}.Release|x64.ActiveCfg = Release|Any CPU
+ {1F04214E-E901-436A-A05F-6BB9ED375019}.Release|x64.Build.0 = Release|Any CPU
+ {1F04214E-E901-436A-A05F-6BB9ED375019}.Release|x86.ActiveCfg = Release|Any CPU
+ {1F04214E-E901-436A-A05F-6BB9ED375019}.Release|x86.Build.0 = Release|Any CPU
+ {77B49C73-29B7-47A5-9475-AC290F53D76D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {77B49C73-29B7-47A5-9475-AC290F53D76D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {77B49C73-29B7-47A5-9475-AC290F53D76D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {77B49C73-29B7-47A5-9475-AC290F53D76D}.Debug|x64.Build.0 = Debug|Any CPU
+ {77B49C73-29B7-47A5-9475-AC290F53D76D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {77B49C73-29B7-47A5-9475-AC290F53D76D}.Debug|x86.Build.0 = Debug|Any CPU
+ {77B49C73-29B7-47A5-9475-AC290F53D76D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {77B49C73-29B7-47A5-9475-AC290F53D76D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {77B49C73-29B7-47A5-9475-AC290F53D76D}.Release|x64.ActiveCfg = Release|Any CPU
+ {77B49C73-29B7-47A5-9475-AC290F53D76D}.Release|x64.Build.0 = Release|Any CPU
+ {77B49C73-29B7-47A5-9475-AC290F53D76D}.Release|x86.ActiveCfg = Release|Any CPU
+ {77B49C73-29B7-47A5-9475-AC290F53D76D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2050,9 +2050,6 @@ Global
{7040CD14-EF4B-40B4-B702-0D59D1F3AEE5} = {63E9B289-95E8-4F2B-A064-156971A6853C}
{92A1F3D3-1AF1-4112-B956-38E8DB1B345B} = {F8DCB579-56BE-49D2-9A9B-BEAA0C457A8B}
{F0F8EA19-0AB7-4D23-944A-59DA514B20D0} = {F8DCB579-56BE-49D2-9A9B-BEAA0C457A8B}
- {85FFF74F-5762-4E11-A328-2BD4794281E6} = {84D32C8B-9CCE-4925-9AEC-8F445C7A2E3D}
- {F939E430-43AB-435D-B74F-9FC7DAFE5074} = {85FFF74F-5762-4E11-A328-2BD4794281E6}
- {3C9ACC27-F486-4765-B703-ED64A34E28C6} = {85FFF74F-5762-4E11-A328-2BD4794281E6}
{9A3741FD-C0EF-4275-8CB0-77D6EB407E88} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C}
{6F6FB8FC-564C-4B04-B254-EB53A7E4562F} = {D953D733-D154-4DF2-B2B9-30BF942E1B6B}
{A484AD9E-04C7-4CF9-BB59-5C7DE772851C} = {4B0BC1E5-17F9-4DD0-AC93-DDC522E1BE3C}
@@ -2117,6 +2114,9 @@ Global
{57B8F129-50EA-4803-AEB7-FE655B6D1B81} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210}
{B1294A26-2C75-42A8-8A9F-9758664F6988} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210}
{C86E3CE6-3A97-451F-945D-61B3D5070160} = {7A9E0EAE-9ABF-40F6-9DB9-8FB1243F4210}
+ {3FE8E499-BE44-4E27-815C-39A4DB2C4EA1} = {84D32C8B-9CCE-4925-9AEC-8F445C7A2E3D}
+ {1F04214E-E901-436A-A05F-6BB9ED375019} = {3FE8E499-BE44-4E27-815C-39A4DB2C4EA1}
+ {77B49C73-29B7-47A5-9475-AC290F53D76D} = {3FE8E499-BE44-4E27-815C-39A4DB2C4EA1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {30422362-0D90-4DBE-8C97-DD2B5B962768}