Implemented RoutingKey attribute

This commit is contained in:
Mark van Renswoude 2021-06-04 11:51:45 +02:00
parent bad7abd5bf
commit a3e3a266e2
6 changed files with 150 additions and 18 deletions

View File

@ -0,0 +1,52 @@
using System;
namespace Tapeti.Annotations
{
/// <inheritdoc />
/// <summary>
/// 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).
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[AttributeUsage(AttributeTargets.Class)]
public class RoutingKeyAttribute : Attribute
{
/// <summary>
/// If specified, the routing key strategy is skipped altogether and this value is used instead.
/// </summary>
/// <remarks>
/// The Prefix and Postfix properties will not have any effect if the Full property is specified.
/// </remarks>
public string Full { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// This property will not have any effect if the Full property is specified. Can be used in combination with Postfix.
/// </remarks>
public string Prefix { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// This property will not have any effect if the Full property is specified. Can be used in combination with Prefix.
/// </remarks>
public string Postfix { get; set; }
}
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -9,6 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">

View File

@ -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
/// <param name="messageType"></param>
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()));
});
}

View File

@ -0,0 +1,31 @@
using System;
using System.Reflection;
using Tapeti.Annotations;
namespace Tapeti.Helpers
{
/// <summary>
/// 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.
/// </summary>
public static class RoutingKeyHelper
{
/// <summary>
/// Applies the RoutingKey attribute for the specified messageClass.
/// </summary>
/// <param name="messageType"></param>
/// <param name="applyStrategy">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.</param>
public static string Decorate(Type messageType, Func<string> applyStrategy)
{
var routingKeyAttribute = messageType.GetCustomAttribute<RoutingKeyAttribute>();
if (routingKeyAttribute == null)
return applyStrategy();
if (!string.IsNullOrEmpty(routingKeyAttribute.Full))
return routingKeyAttribute.Full;
return (routingKeyAttribute.Prefix ?? "") + applyStrategy() + (routingKeyAttribute.Postfix ?? "");
}
}
}