diff --git a/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs b/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs index 9d821999..a8071778 100644 --- a/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs +++ b/examples/AspNetCore/ControllerSample/Controllers/SampleController.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -155,5 +155,18 @@ namespace ControllerSample.Controllers { return Ok(); } + + /// + /// Method which uses for binding this endpoint to a subscription and adds routingkey metadata. + /// + /// + /// + [Topic("pubsub", "topicmetadata")] + [TopicMetadata("routingKey", "keyA")] + [HttpPost("examplecustomtopicmetadata")] + public ActionResult ExampleCustomTopicMetadata(Transaction transaction) + { + return Ok(); + } } } diff --git a/src/Dapr.AspNetCore/DaprEndpointConventionBuilderExtensions.cs b/src/Dapr.AspNetCore/DaprEndpointConventionBuilderExtensions.cs index 40b7e966..b29b8150 100644 --- a/src/Dapr.AspNetCore/DaprEndpointConventionBuilderExtensions.cs +++ b/src/Dapr.AspNetCore/DaprEndpointConventionBuilderExtensions.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Builder { using System; + using System.Collections.Generic; using Dapr; /// @@ -35,6 +36,24 @@ namespace Microsoft.AspNetCore.Builder return WithTopic(builder, pubsubName, name, false); } + /// + /// Adds metadata to the provided . + /// + /// The .\ + /// The name of the pubsub component to use. + /// The topic name. + /// + /// A collection of metadata key-value pairs that will be provided to the pubsub. The valid metadata keys and values + /// are determined by the type of pubsub component used. + /// + /// The type. + /// The builder object. + public static T WithTopic(this T builder, string pubsubName, string name, IDictionary metadata) + where T : IEndpointConventionBuilder + { + return WithTopic(builder, pubsubName, name, false, metadata); + } + /// /// Adds metadata to the provided . /// @@ -46,6 +65,25 @@ namespace Microsoft.AspNetCore.Builder /// The builder object. public static T WithTopic(this T builder, string pubsubName, string name, bool enableRawPayload) where T : IEndpointConventionBuilder + { + return WithTopic(builder, pubsubName, name, enableRawPayload, null); + } + + /// + /// Adds metadata to the provided . + /// + /// The .\ + /// The name of the pubsub component to use. + /// The topic name. + /// The enable/disable raw pay load flag. + /// + /// A collection of metadata key-value pairs that will be provided to the pubsub. The valid metadata keys and values + /// are determined by the type of pubsub component used. + /// + /// The type. + /// The builder object. + public static T WithTopic(this T builder, string pubsubName, string name, bool enableRawPayload, IDictionary metadata) + where T : IEndpointConventionBuilder { if (builder is null) { @@ -56,6 +94,13 @@ namespace Microsoft.AspNetCore.Builder ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); builder.WithMetadata(new TopicAttribute(pubsubName, name, enableRawPayload)); + if (metadata is not null) + { + foreach (var md in metadata) + { + builder.WithMetadata(new TopicMetadataAttribute(md.Key, md.Value)); + } + } return builder; } } diff --git a/src/Dapr.AspNetCore/DaprEndpointRouteBuilderExtensions.cs b/src/Dapr.AspNetCore/DaprEndpointRouteBuilderExtensions.cs index 6eda1d9c..6269fb7d 100644 --- a/src/Dapr.AspNetCore/DaprEndpointRouteBuilderExtensions.cs +++ b/src/Dapr.AspNetCore/DaprEndpointRouteBuilderExtensions.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -70,7 +70,9 @@ namespace Microsoft.AspNetCore.Builder .SelectMany(e => { var topicMetadata = e.Metadata.GetOrderedMetadata(); - var subs = new List<(string PubsubName, string Name, bool? EnableRawPayload, string Match, int Priority, RoutePattern RoutePattern)>(); + var originalTopicMetadata = e.Metadata.GetOrderedMetadata(); + + var subs = new List<(string PubsubName, string Name, bool? EnableRawPayload, string Match, int Priority, Dictionary OriginalTopicMetadata, string MetadataSeparator, RoutePattern RoutePattern)>(); for (int i = 0; i < topicMetadata.Count(); i++) { @@ -79,6 +81,10 @@ namespace Microsoft.AspNetCore.Builder (topicMetadata[i] as IRawTopicMetadata)?.EnableRawPayload, topicMetadata[i].Match, topicMetadata[i].Priority, + originalTopicMetadata.Where(m => (topicMetadata[i] as IOwnedOriginalTopicMetadata)?.OwnedMetadatas?.Any(o => o.Equals(m.Id)) == true || string.IsNullOrEmpty(m.Id)) + .GroupBy(c => c.Name) + .ToDictionary(m => m.Key, m => m.Select(c => c.Value).Distinct().ToArray()), + (topicMetadata[i] as IOwnedOriginalTopicMetadata)?.MetadataSeparator, e.RoutePattern)); } @@ -91,10 +97,18 @@ namespace Microsoft.AspNetCore.Builder { var first = e.First(); var rawPayload = e.Any(e => e.EnableRawPayload.GetValueOrDefault()); + var metadataSeparator = e.FirstOrDefault(e => string.IsNullOrEmpty(e.MetadataSeparator)).MetadataSeparator ?? ","; var rules = e.Where(e => !string.IsNullOrEmpty(e.Match)).ToList(); var defaultRoutes = e.Where(e => string.IsNullOrEmpty(e.Match)).Select(e => RoutePatternToString(e.RoutePattern)).ToList(); var defaultRoute = defaultRoutes.FirstOrDefault(); + //multiple identical names. use comma separation. + var metadata = new Metadata(e.SelectMany(c => c.OriginalTopicMetadata).GroupBy(c => c.Key).ToDictionary(c => c.Key, c => string.Join(metadataSeparator, c.SelectMany(c => c.Value).Distinct()))); + if (rawPayload || options?.EnableRawPayload is true) + { + metadata.Add(Metadata.RawPayload, "true"); + } + if (logger != null) { if (defaultRoutes.Count > 1) @@ -116,10 +130,7 @@ namespace Microsoft.AspNetCore.Builder { Topic = first.Name, PubsubName = first.PubsubName, - Metadata = rawPayload ? new Metadata - { - RawPayload = "true", - } : null, + Metadata = metadata.Count > 0 ? metadata : null, }; // Use the V2 routing rules structure @@ -154,7 +165,8 @@ namespace Microsoft.AspNetCore.Builder }); } - private static string RoutePatternToString(RoutePattern routePattern) { + private static string RoutePatternToString(RoutePattern routePattern) + { return string.Join("/", routePattern.PathSegments .Select(segment => string.Concat(segment.Parts.Cast() .Select(part => part.Content)))); diff --git a/src/Dapr.AspNetCore/IOriginalTopicMetadata.cs b/src/Dapr.AspNetCore/IOriginalTopicMetadata.cs new file mode 100644 index 00000000..71aa952c --- /dev/null +++ b/src/Dapr.AspNetCore/IOriginalTopicMetadata.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr +{ + /// + /// IOriginalTopicMetadata that describes subscribe endpoint to a topic original metadata. + /// + public interface IOriginalTopicMetadata + { + /// + /// Gets the topic metadata id. + /// + /// + /// It is only used for simple identification,. When it is empty, it can be used for all topics in the current context. + /// + string Id { get; } + + /// + /// Gets the topic metadata name. + /// + /// Multiple identical names. only the first is valid. + string Name { get; } + + /// + /// Gets the topic metadata value. + /// + string Value { get; } + } +} diff --git a/src/Dapr.AspNetCore/IOwnedOriginalTopicMetadata.cs b/src/Dapr.AspNetCore/IOwnedOriginalTopicMetadata.cs new file mode 100644 index 00000000..36ae45e5 --- /dev/null +++ b/src/Dapr.AspNetCore/IOwnedOriginalTopicMetadata.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr +{ + /// + /// IOwnedOriginalTopicMetadata that describes subscribe endpoint to topic owned metadata. + /// + public interface IOwnedOriginalTopicMetadata + { + /// + /// Gets the owned by topic. + /// + /// When the is not empty, the metadata owned by topic. + string[] OwnedMetadatas { get; } + + /// + /// Get separator to use for metadata + /// + /// Separator to be used for when multiple values exist for a . + string MetadataSeparator { get; } + } +} diff --git a/src/Dapr.AspNetCore/Subscription.cs b/src/Dapr.AspNetCore/Subscription.cs index 828e2c8b..54e41956 100644 --- a/src/Dapr.AspNetCore/Subscription.cs +++ b/src/Dapr.AspNetCore/Subscription.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -49,12 +49,16 @@ namespace Dapr /// /// This class defines the metadata for subscribe endpoint. /// - internal class Metadata + internal class Metadata : Dictionary { + public Metadata() { } + + public Metadata(IDictionary dictionary) : base(dictionary) { } + /// - /// Gets or sets the raw payload + /// RawPayload key /// - public string RawPayload { get; set; } + internal const string RawPayload = "rawPayload"; } internal class Routes diff --git a/src/Dapr.AspNetCore/TopicAttribute.cs b/src/Dapr.AspNetCore/TopicAttribute.cs index 08c3bac9..24d47f62 100644 --- a/src/Dapr.AspNetCore/TopicAttribute.cs +++ b/src/Dapr.AspNetCore/TopicAttribute.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,20 +19,24 @@ namespace Dapr /// TopicAttribute describes an endpoint as a subscriber to a topic. /// [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] - public class TopicAttribute : Attribute, ITopicMetadata, IRawTopicMetadata + public class TopicAttribute : Attribute, ITopicMetadata, IRawTopicMetadata, IOwnedOriginalTopicMetadata { /// /// Initializes a new instance of the class. /// /// The name of the pubsub component to use. /// The topic name. - public TopicAttribute(string pubsubName, string name) + /// The topic owned metadata ids. + /// Separator to use for metadata. + public TopicAttribute(string pubsubName, string name, string[] ownedMetadatas = null,string metadataSeparator = ",") { ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); this.Name = name; this.PubsubName = pubsubName; + this.OwnedMetadatas = ownedMetadatas; + this.MetadataSeparator = metadataSeparator; } /// @@ -41,7 +45,9 @@ namespace Dapr /// The name of the pubsub component to use. /// The topic name. /// The enable/disable raw pay load flag. - public TopicAttribute(string pubsubName, string name, bool enableRawPayload) + /// The topic owned metadata ids. + /// Separator to use for metadata. + public TopicAttribute(string pubsubName, string name, bool enableRawPayload, string[] ownedMetadatas = null, string metadataSeparator = ",") { ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); @@ -49,6 +55,8 @@ namespace Dapr this.Name = name; this.PubsubName = pubsubName; this.EnableRawPayload = enableRawPayload; + this.OwnedMetadatas = ownedMetadatas; + this.MetadataSeparator = metadataSeparator; } /// @@ -58,7 +66,9 @@ namespace Dapr /// The topic name. /// The CEL expression to test the cloud event with. /// The priority of the rule (low-to-high values). - public TopicAttribute(string pubsubName, string name, string match, int priority) + /// The topic owned metadata ids. + /// Separator to use for metadata. + public TopicAttribute(string pubsubName, string name, string match, int priority, string[] ownedMetadatas = null, string metadataSeparator = ",") { ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); @@ -67,6 +77,8 @@ namespace Dapr this.PubsubName = pubsubName; this.Match = match; this.Priority = priority; + this.OwnedMetadatas = ownedMetadatas; + this.MetadataSeparator = metadataSeparator; } /// @@ -77,7 +89,9 @@ namespace Dapr /// The enable/disable raw pay load flag. /// The CEL expression to test the cloud event with. /// The priority of the rule (low-to-high values). - public TopicAttribute(string pubsubName, string name, bool enableRawPayload, string match, int priority) + /// The topic owned metadata ids. + /// Separator to use for metadata. + public TopicAttribute(string pubsubName, string name, bool enableRawPayload, string match, int priority, string[] ownedMetadatas = null, string metadataSeparator = ",") { ArgumentVerifier.ThrowIfNullOrEmpty(pubsubName, nameof(pubsubName)); ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); @@ -87,6 +101,8 @@ namespace Dapr this.EnableRawPayload = enableRawPayload; this.Match = match; this.Priority = priority; + this.OwnedMetadatas = ownedMetadatas; + this.MetadataSeparator = metadataSeparator; } /// @@ -103,5 +119,11 @@ namespace Dapr /// public int Priority { get; } + + /// + public string[] OwnedMetadatas { get; } + + /// + public string MetadataSeparator { get; } } } diff --git a/src/Dapr.AspNetCore/TopicMetadataAttribute.cs b/src/Dapr.AspNetCore/TopicMetadataAttribute.cs new file mode 100644 index 00000000..eb3e4009 --- /dev/null +++ b/src/Dapr.AspNetCore/TopicMetadataAttribute.cs @@ -0,0 +1,62 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr +{ + using System; + + /// + /// IOriginalTopicMetadata that describes subscribe endpoint to a topic original metadata. + /// + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + public class TopicMetadataAttribute : Attribute, IOriginalTopicMetadata + { + /// + /// Initializes a new instance of the class. + /// + /// The metadata name. + /// The metadata value. + public TopicMetadataAttribute(string name, string value) + { + ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); + ArgumentVerifier.ThrowIfNullOrEmpty(value, nameof(value)); + Name = name; + Value = value; + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata id. + /// The metadata name. + /// The metadata value. + public TopicMetadataAttribute(string id, string name, string value) + { + ArgumentVerifier.ThrowIfNullOrEmpty(id, nameof(name)); + ArgumentVerifier.ThrowIfNullOrEmpty(name, nameof(name)); + ArgumentVerifier.ThrowIfNullOrEmpty(value, nameof(value)); + Id = id; + Name = name; + Value = value; + } + + /// + public string Id { get; } + + /// + public string Name { get; } + + /// + public string Value { get; } + } +} diff --git a/test/Dapr.AspNetCore.IntegrationTest.App/DaprController.cs b/test/Dapr.AspNetCore.IntegrationTest.App/DaprController.cs index 2a0c8dbd..960872be 100644 --- a/test/Dapr.AspNetCore.IntegrationTest.App/DaprController.cs +++ b/test/Dapr.AspNetCore.IntegrationTest.App/DaprController.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -68,6 +68,16 @@ namespace Dapr.AspNetCore.IntegrationTest.App { } + [Topic("pubsub", "metadata", new string[1] { "id1" })] + [Topic("pubsub", "metadata.1", true)] + [HttpPost("/multiMetadataTopicAttr")] + [TopicMetadata("n1", "v1")] + [TopicMetadata("id1", "n2", "v2")] + [TopicMetadata("id1", "n2", "v3")] + public void MultipleMetadataTopics() + { + } + [Topic("pubsub", "splitTopicAttr", true)] [HttpPost("/splitTopics")] public void SplitTopic() @@ -112,7 +122,7 @@ namespace Dapr.AspNetCore.IntegrationTest.App } [HttpPost("/echo-user")] - public ActionResult EchoUser([FromQuery]UserInfo user) + public ActionResult EchoUser([FromQuery] UserInfo user) { // To simulate an action where there's no Dapr attribute, yet MVC still checks the list of available model binder providers. return user; diff --git a/test/Dapr.AspNetCore.IntegrationTest.App/Startup.cs b/test/Dapr.AspNetCore.IntegrationTest.App/Startup.cs index 201ba7b5..e776fa46 100644 --- a/test/Dapr.AspNetCore.IntegrationTest.App/Startup.cs +++ b/test/Dapr.AspNetCore.IntegrationTest.App/Startup.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -66,6 +66,8 @@ namespace Dapr.AspNetCore.IntegrationTest.App endpoints.MapPost("/splitTopics", context => Task.CompletedTask).WithTopic("pubsub", "splitTopicBuilder"); + endpoints.MapPost("/splitMetadataTopics", context => Task.CompletedTask).WithTopic("pubsub", "splitMetadataTopicBuilder", new Dictionary { { "n1", "v1" }, { "n2", "v1" } }); + endpoints.MapPost("/routingwithstateentry/{widget}", async context => { var daprClient = context.RequestServices.GetRequiredService(); diff --git a/test/Dapr.AspNetCore.IntegrationTest/SubscribeEndpointTest.cs b/test/Dapr.AspNetCore.IntegrationTest/SubscribeEndpointTest.cs index 128c81b3..881a015f 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/SubscribeEndpointTest.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/SubscribeEndpointTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,9 +14,9 @@ namespace Dapr.AspNetCore.IntegrationTest { using System.Collections.Generic; + using System.Linq; using System.Net.Http; using System.Text.Json; - using System.Text.Json.Serialization; using System.Threading.Tasks; using FluentAssertions; using Xunit; @@ -39,21 +39,38 @@ namespace Dapr.AspNetCore.IntegrationTest var json = await JsonSerializer.DeserializeAsync(stream); json.ValueKind.Should().Be(JsonValueKind.Array); - json.GetArrayLength().Should().Be(12); - var subscriptions = new List<(string PubsubName, string Topic, string Route, string rawPayload, string match)>(); + json.GetArrayLength().Should().Be(15); + var subscriptions = new List<(string PubsubName, string Topic, string Route, string rawPayload, string match, string metadata)>(); foreach (var element in json.EnumerateArray()) { var pubsubName = element.GetProperty("pubsubName").GetString(); var topic = element.GetProperty("topic").GetString(); var rawPayload = string.Empty; + Dictionary originalMetadata = new Dictionary(); if (element.TryGetProperty("metadata", out JsonElement metadata)) { - rawPayload = metadata.GetProperty("rawPayload").GetString(); + if (metadata.TryGetProperty("rawPayload", out JsonElement rawPayloadJson)) + { + rawPayload = rawPayloadJson.GetString(); + } + + foreach (var originalMetadataProperty in metadata.EnumerateObject().OrderBy(c=>c.Name)) + { + if (!originalMetadataProperty.Name.Equals("rawPayload")) + { + originalMetadata.Add(originalMetadataProperty.Name, originalMetadataProperty.Value.GetString()); + } + } + } + var originalMetadataString= string.Empty; + if (originalMetadata.Count > 0) + { + originalMetadataString = string.Join(";", originalMetadata.Select(c => $"{c.Key}={c.Value}")); } if (element.TryGetProperty("route", out JsonElement route)) { - subscriptions.Add((pubsubName, topic, route.GetString(), rawPayload, string.Empty)); + subscriptions.Add((pubsubName, topic, route.GetString(), rawPayload, string.Empty, originalMetadataString)); } else if (element.TryGetProperty("routes", out JsonElement routes)) { @@ -63,30 +80,33 @@ namespace Dapr.AspNetCore.IntegrationTest { var match = rule.GetProperty("match").GetString(); var path = rule.GetProperty("path").GetString(); - subscriptions.Add((pubsubName, topic, path, rawPayload, match)); + subscriptions.Add((pubsubName, topic, path, rawPayload, match, originalMetadataString)); } } if (routes.TryGetProperty("default", out JsonElement defaultProperty)) { - subscriptions.Add((pubsubName, topic, defaultProperty.GetString(), rawPayload, string.Empty)); - } + subscriptions.Add((pubsubName, topic, defaultProperty.GetString(), rawPayload, string.Empty, originalMetadataString)); + } } } - subscriptions.Should().Contain(("testpubsub", "A", "topic-a", string.Empty, string.Empty)); - subscriptions.Should().Contain(("testpubsub", "A.1", "topic-a", string.Empty, string.Empty)); - subscriptions.Should().Contain(("pubsub", "B", "B", string.Empty, string.Empty)); - subscriptions.Should().Contain(("custom-pubsub", "custom-C", "C", string.Empty, string.Empty)); - subscriptions.Should().Contain(("pubsub", "register-user", "register-user", string.Empty, string.Empty)); - subscriptions.Should().Contain(("pubsub", "register-user-plaintext", "register-user-plaintext", string.Empty, string.Empty)); - subscriptions.Should().Contain(("pubsub", "D", "D", "true", string.Empty)); - subscriptions.Should().Contain(("pubsub", "E", "E", string.Empty, string.Empty)); - subscriptions.Should().Contain(("pubsub", "E", "E-Critical", string.Empty, "event.type == \"critical\"")); - subscriptions.Should().Contain(("pubsub", "E", "E-Important", string.Empty, "event.type == \"important\"")); - subscriptions.Should().Contain(("pubsub", "F", "multiTopicAttr", string.Empty, string.Empty)); - subscriptions.Should().Contain(("pubsub", "F.1", "multiTopicAttr", "true", string.Empty)); - subscriptions.Should().Contain(("pubsub", "splitTopicBuilder", "splitTopics", string.Empty, string.Empty)); - subscriptions.Should().Contain(("pubsub", "splitTopicAttr", "splitTopics", "true", string.Empty)); + subscriptions.Should().Contain(("testpubsub", "A", "topic-a", string.Empty, string.Empty, string.Empty)); + subscriptions.Should().Contain(("testpubsub", "A.1", "topic-a", string.Empty, string.Empty, string.Empty)); + subscriptions.Should().Contain(("pubsub", "B", "B", string.Empty, string.Empty, string.Empty)); + subscriptions.Should().Contain(("custom-pubsub", "custom-C", "C", string.Empty, string.Empty, string.Empty)); + subscriptions.Should().Contain(("pubsub", "register-user", "register-user", string.Empty, string.Empty, string.Empty)); + subscriptions.Should().Contain(("pubsub", "register-user-plaintext", "register-user-plaintext", string.Empty, string.Empty, string.Empty)); + subscriptions.Should().Contain(("pubsub", "D", "D", "true", string.Empty, string.Empty)); + subscriptions.Should().Contain(("pubsub", "E", "E", string.Empty, string.Empty, string.Empty)); + subscriptions.Should().Contain(("pubsub", "E", "E-Critical", string.Empty, "event.type == \"critical\"", string.Empty)); + subscriptions.Should().Contain(("pubsub", "E", "E-Important", string.Empty, "event.type == \"important\"", string.Empty)); + subscriptions.Should().Contain(("pubsub", "F", "multiTopicAttr", string.Empty, string.Empty, string.Empty)); + subscriptions.Should().Contain(("pubsub", "F.1", "multiTopicAttr", "true", string.Empty, string.Empty)); + subscriptions.Should().Contain(("pubsub", "splitTopicBuilder", "splitTopics", string.Empty, string.Empty, string.Empty)); + subscriptions.Should().Contain(("pubsub", "splitTopicAttr", "splitTopics", "true", string.Empty, string.Empty)); + subscriptions.Should().Contain(("pubsub", "metadata", "multiMetadataTopicAttr", string.Empty, string.Empty, "n1=v1;n2=v2,v3")); + subscriptions.Should().Contain(("pubsub", "metadata.1", "multiMetadataTopicAttr", "true", string.Empty, "n1=v1")); + subscriptions.Should().Contain(("pubsub", "splitMetadataTopicBuilder", "splitMetadataTopics", string.Empty, string.Empty, "n1=v1;n2=v1")); // Test priority route sorting var eTopic = subscriptions.FindAll(e => e.Topic == "E");