diff --git a/src/NATS.Client/Conn.cs b/src/NATS.Client/Conn.cs index 3aac4367b..5f7413718 100644 --- a/src/NATS.Client/Conn.cs +++ b/src/NATS.Client/Conn.cs @@ -3224,9 +3224,11 @@ public Task RequestAsync(string subject, byte[] data, int offset, int count /// A unique inbox string. public string NewInbox() { + var prefix = opts.CustomInboxPrefix ?? IC.inboxPrefix; + if (!opts.UseOldRequestStyle) { - return IC.inboxPrefix + Guid.NewGuid().ToString("N"); + return prefix + Guid.NewGuid().ToString("N"); } else { @@ -3237,7 +3239,7 @@ public string NewInbox() r.NextBytes(buf); - return IC.inboxPrefix + BitConverter.ToString(buf).Replace("-",""); + return prefix + BitConverter.ToString(buf).Replace("-",""); } } diff --git a/src/NATS.Client/Options.cs b/src/NATS.Client/Options.cs index 646160ad9..f22425d50 100644 --- a/src/NATS.Client/Options.cs +++ b/src/NATS.Client/Options.cs @@ -189,6 +189,8 @@ public void SetJWTEventHandlers(EventHandler JWTEventHandler, internal string token; internal string nkey; + internal string customInboxPrefix; + // Options can only be publicly created through // ConnectionFactory.GetDefaultOptions(); internal Options() { } @@ -222,6 +224,7 @@ internal Options(Options o) verbose = o.verbose; subscriberDeliveryTaskCount = o.subscriberDeliveryTaskCount; subscriptionBatchSize = o.subscriptionBatchSize; + customInboxPrefix = o.customInboxPrefix; if (o.url != null) { @@ -463,6 +466,21 @@ public string Token set { token = value; } } + /// + /// Gets or sets a custom inbox prefix. + /// + public string CustomInboxPrefix + { + get => customInboxPrefix; + set + { + if (value != null && !Subscription.IsValidPrefix(value)) + throw new ArgumentException("Prefix would result in an invalid subject."); + + customInboxPrefix = value; + } + } + /// /// Adds an X.509 certifcate from a file for use with a secure connection. /// diff --git a/src/NATS.Client/Subscription.cs b/src/NATS.Client/Subscription.cs index 13db3a097..df216788d 100644 --- a/src/NATS.Client/Subscription.cs +++ b/src/NATS.Client/Subscription.cs @@ -698,6 +698,19 @@ public static bool IsValidSubject(string subject) return true; } + /// + /// Checks if a prefix is valid. + /// + /// + /// + public static bool IsValidPrefix(string prefix) + { + if (ContainsInvalidChars(prefix)) + return false; + + return !prefix.StartsWith(".") && prefix.EndsWith("."); + } + /// /// Checks if the queue group name is valid. /// diff --git a/src/Tests/IntegrationTests/TestSubscriptions.cs b/src/Tests/IntegrationTests/TestSubscriptions.cs index 707ae1a85..5f8940937 100644 --- a/src/Tests/IntegrationTests/TestSubscriptions.cs +++ b/src/Tests/IntegrationTests/TestSubscriptions.cs @@ -1055,6 +1055,43 @@ public void TestRespond() } } + [Fact] + public void TestRespondWithCustomInbox() + { + var opts = Context.GetTestOptionsWithDefaultTimeout(Context.Server1.Port); + opts.CustomInboxPrefix = "_TEST."; + + using (NATSServer.CreateFastAndVerify(Context.Server1.Port)) + { + using (var cn = Context.ConnectionFactory.CreateConnection(opts)) + using (var requestSub = cn.SubscribeSync("foo")) + { + var replyTo = cn.NewInbox(); + Assert.StartsWith("_TEST.", replyTo); + + using (var responderSub = cn.SubscribeSync(replyTo)) + { + cn.Publish("foo", replyTo, SamplePayload.Random()); + + var request = requestSub.NextMessage(1000); + Assert.NotNull(request); + Assert.Equal(replyTo, request.Reply); + + var reply = SamplePayload.Random(); + request.Respond(reply); + + var response = responderSub.NextMessage(1000); + Assert.NotNull(response); + Assert.Equal(replyTo, response.Subject); + Assert.Equal(reply, response.Data); + + requestSub.Unsubscribe(); + responderSub.Unsubscribe(); + } + } + } + } + [Fact] public void TestRespondWithAutoUnsubscribe() { diff --git a/src/Tests/UnitTests/TestOptions.cs b/src/Tests/UnitTests/TestOptions.cs index fe05669b1..654e70912 100644 --- a/src/Tests/UnitTests/TestOptions.cs +++ b/src/Tests/UnitTests/TestOptions.cs @@ -12,6 +12,7 @@ // limitations under the License. using System; +using System.Reflection.Emit; using NATS.Client; using Xunit; @@ -38,5 +39,31 @@ public void TestBadOptionSubscriptionBatchSize() Assert.ThrowsAny(() => opts.SubscriptionBatchSize = 0); } + + [Theory] + [InlineData("")] + [InlineData("\r")] + [InlineData("\n")] + [InlineData("\t")] + [InlineData("Test")] + [InlineData(".Test.")] + public void TestBadCustomPrefix(string customPrefix) + { + var opts = GetDefaultOptions(); + + Assert.ThrowsAny(() => opts.CustomInboxPrefix = customPrefix); + } + + [Theory] + [InlineData("Test.")] + [InlineData("Test.SubTest.")] + [InlineData("_Test.")] + [InlineData("_Test.SubTest.")] + public void TestOkCustomPrefix(string customPrefix) + { + var opts = GetDefaultOptions(); + + opts.CustomInboxPrefix = customPrefix; + } } } \ No newline at end of file