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}