diff --git a/Tapeti.Annotations/RoutingKeyAttribute.cs b/Tapeti.Annotations/RoutingKeyAttribute.cs new file mode 100644 index 0000000..7053372 --- /dev/null +++ b/Tapeti.Annotations/RoutingKeyAttribute.cs @@ -0,0 +1,52 @@ +using System; + +namespace Tapeti.Annotations +{ + /// + /// + /// Can be attached to a message class to override or extend the generated routing key. + /// One of the intended scenarios is for versioning messages, where you want to add for example a + /// ".v2" postfix but keep the class name itself the same (in a different namespace of course). + /// + /// + /// The implementation of IRoutingKeyStrategy must explicitly add support for this attribute for it to have effect. + /// The default implementation (TypeNameRoutingKeyStrategy) does, of course. Custom implementations can use + /// the Tapeti.Helpers.RoutingKeyHelper class to add support and keep up-to-date automatically. + /// + /// When using EnableDeclareDurableQueues to automatically generate the queue bindings, both the sender and receiver must + /// use a Tapeti version >= 2.5 to support this attribute or the binding will differ from the sent routing key. + /// + /// The routing keys are no longer used by Tapeti once the message is in the queue, the message handler + /// is instead based on the full message class name (thus including namespace), so if the binding is generated in any + /// other way this remark does not apply and prior versions of Tapeti are compatible. + /// + [AttributeUsage(AttributeTargets.Class)] + public class RoutingKeyAttribute : Attribute + { + /// + /// If specified, the routing key strategy is skipped altogether and this value is used instead. + /// + /// + /// The Prefix and Postfix properties will not have any effect if the Full property is specified. + /// + public string Full { get; set; } + + /// + /// If specified, the value generated by the default routing key strategy is prefixed with this value. + /// No dot will be added after this prefix, if you want to include it add it as part of the value. + /// + /// + /// This property will not have any effect if the Full property is specified. Can be used in combination with Postfix. + /// + public string Prefix { get; set; } + + /// + /// If specified, the value generated by the default routing key strategy is postfixed with this value. + /// No dot will be added before this postfix, if you want to include it add it as part of the value. + /// + /// + /// This property will not have any effect if the Full property is specified. Can be used in combination with Prefix. + /// + public string Postfix { get; set; } + } +} diff --git a/Tapeti.Tests/Default/TypeNameRoutingKeyStrategyTests.cs b/Tapeti.Tests/Default/TypeNameRoutingKeyStrategyTests.cs index ed133fb..26fc423 100644 --- a/Tapeti.Tests/Default/TypeNameRoutingKeyStrategyTests.cs +++ b/Tapeti.Tests/Default/TypeNameRoutingKeyStrategyTests.cs @@ -1,4 +1,6 @@ using System; +using FluentAssertions; +using Tapeti.Annotations; using Tapeti.Default; using Xunit; @@ -60,13 +62,54 @@ namespace Tapeti.Tests.Default AssertRoutingKey("acr.test.mixed.case", typeof(ACRTestMIXEDCaseMESSAGE)); } + + [RoutingKey(Prefix = "prefix.")] + private class PrefixAttributeTestMessage { } + + [Fact] + public void Prefix() + { + AssertRoutingKey("prefix.prefix.attribute.test", typeof(PrefixAttributeTestMessage)); + } + + + [RoutingKey(Postfix = ".postfix")] + private class PostfixAttributeTestMessage { } + + [Fact] + public void Postfix() + { + AssertRoutingKey("postfix.attribute.test.postfix", typeof(PostfixAttributeTestMessage)); + } + + + + [RoutingKey(Prefix = "prefix.", Postfix = ".postfix")] + private class PrefixPostfixAttributeTestMessage { } + + [Fact] + public void PrefixPostfix() + { + AssertRoutingKey("prefix.prefix.postfix.attribute.test.postfix", typeof(PrefixPostfixAttributeTestMessage)); + } + + + [RoutingKey(Full = "andnowforsomethingcompletelydifferent", Prefix = "ignore.", Postfix = ".me")] + private class FullAttributeTestMessage { } + + [Fact] + public void Full() + { + AssertRoutingKey("andnowforsomethingcompletelydifferent", typeof(FullAttributeTestMessage)); + } + + + // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local private static void AssertRoutingKey(string expected, Type messageType) { - if (expected == null) throw new ArgumentNullException(nameof(expected)); - if (messageType == null) throw new ArgumentNullException(nameof(messageType)); - - Assert.Equal(expected, new TypeNameRoutingKeyStrategy().GetRoutingKey(messageType)); + var routingKey = new TypeNameRoutingKeyStrategy().GetRoutingKey(messageType); + routingKey.Should().Be(expected); } } // ReSharper restore InconsistentNaming diff --git a/Tapeti.Tests/Helpers/ConnectionStringParser.cs b/Tapeti.Tests/Helpers/ConnectionStringParser.cs index dfd942f..924f3b5 100644 --- a/Tapeti.Tests/Helpers/ConnectionStringParser.cs +++ b/Tapeti.Tests/Helpers/ConnectionStringParser.cs @@ -1,4 +1,5 @@ -using Tapeti.Helpers; +using FluentAssertions; +using Tapeti.Helpers; using Xunit; namespace Tapeti.Tests.Helpers @@ -185,12 +186,12 @@ namespace Tapeti.Tests.Helpers { var parsed = ConnectionStringParser.Parse(connectionstring); - Assert.Equal(expected.HostName, parsed.HostName); - Assert.Equal(expected.Port, parsed.Port); - Assert.Equal(expected.VirtualHost, parsed.VirtualHost); - Assert.Equal(expected.Username, parsed.Username); - Assert.Equal(expected.Password, parsed.Password); - Assert.Equal(expected.PrefetchCount, parsed.PrefetchCount); + parsed.HostName.Should().Be(expected.HostName); + parsed.Port.Should().Be(expected.Port); + parsed.VirtualHost.Should().Be(expected.VirtualHost); + parsed.Username.Should().Be(expected.Username); + parsed.Password.Should().Be(expected.Password); + parsed.PrefetchCount.Should().Be(expected.PrefetchCount); } } } diff --git a/Tapeti.Tests/Tapeti.Tests.csproj b/Tapeti.Tests/Tapeti.Tests.csproj index cc4c3a6..949f4a3 100644 --- a/Tapeti.Tests/Tapeti.Tests.csproj +++ b/Tapeti.Tests/Tapeti.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/Tapeti/Default/TypeNameRoutingKeyStrategy.cs b/Tapeti/Default/TypeNameRoutingKeyStrategy.cs index 5b8af1f..f31af08 100644 --- a/Tapeti/Default/TypeNameRoutingKeyStrategy.cs +++ b/Tapeti/Default/TypeNameRoutingKeyStrategy.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using Tapeti.Helpers; namespace Tapeti.Default { @@ -45,15 +46,18 @@ namespace Tapeti.Default /// protected virtual string BuildRoutingKey(Type messageType) { - // Split PascalCase into dot-separated parts. If the class name ends in "Message" leave that out. - var words = SplitPascalCase(messageType.Name); - if (words == null) - return ""; + return RoutingKeyHelper.Decorate(messageType, () => + { + // Split PascalCase into dot-separated parts. If the class name ends in "Message" leave that out. + var words = SplitPascalCase(messageType.Name); + if (words == null) + return ""; - if (words.Count > 1 && words.Last().Equals("Message", StringComparison.InvariantCultureIgnoreCase)) - words.RemoveAt(words.Count - 1); + if (words.Count > 1 && words.Last().Equals("Message", StringComparison.InvariantCultureIgnoreCase)) + words.RemoveAt(words.Count - 1); - return string.Join(".", words.Select(s => s.ToLower())); + return string.Join(".", words.Select(s => s.ToLower())); + }); } diff --git a/Tapeti/Helpers/RoutingKeyHelper.cs b/Tapeti/Helpers/RoutingKeyHelper.cs new file mode 100644 index 0000000..af4c87c --- /dev/null +++ b/Tapeti/Helpers/RoutingKeyHelper.cs @@ -0,0 +1,31 @@ +using System; +using System.Reflection; +using Tapeti.Annotations; + +namespace Tapeti.Helpers +{ + /// + /// Helper class for compositing a routing key with support for the RoutingKey attribute. + /// Should be used by all implementations of IRoutingKeyStrategy unless there is a good reason not to. + /// + public static class RoutingKeyHelper + { + /// + /// Applies the RoutingKey attribute for the specified messageClass. + /// + /// + /// Called when the strategy needs to be applied to the message class to generate a routing key. + /// Will not be called if the Full property is specified on the RoutingKey attribute. + public static string Decorate(Type messageType, Func applyStrategy) + { + var routingKeyAttribute = messageType.GetCustomAttribute(); + if (routingKeyAttribute == null) + return applyStrategy(); + + if (!string.IsNullOrEmpty(routingKeyAttribute.Full)) + return routingKeyAttribute.Full; + + return (routingKeyAttribute.Prefix ?? "") + applyStrategy() + (routingKeyAttribute.Postfix ?? ""); + } + } +}