diff --git a/src/Testing/CoreTests/Runtime/Green/Messages.cs b/src/Testing/CoreTests/Runtime/Green/Messages.cs index 65343ead8..55f7de0d9 100644 --- a/src/Testing/CoreTests/Runtime/Green/Messages.cs +++ b/src/Testing/CoreTests/Runtime/Green/Messages.cs @@ -4,4 +4,6 @@ public class GreenMessage1; public class GreenMessage2; -public class GreenMessage3; \ No newline at end of file +public class GreenMessage3; + +public class GreenAttribute : Attribute; diff --git a/src/Testing/CoreTests/Runtime/Red/Messages.cs b/src/Testing/CoreTests/Runtime/Red/Messages.cs index d6be920cd..aefde376a 100644 --- a/src/Testing/CoreTests/Runtime/Red/Messages.cs +++ b/src/Testing/CoreTests/Runtime/Red/Messages.cs @@ -1,7 +1,16 @@ namespace CoreTests.Runtime.Red; +[Red] public class RedMessage1; +[Crimson] public class RedMessage2; -public class RedMessage3; \ No newline at end of file +[Burgundy] +public class RedMessage3; + +public class RedAttribute : Attribute; + +public class CrimsonAttribute : RedAttribute; + +public class BurgundyAttribute : RedAttribute; diff --git a/src/Testing/CoreTests/Runtime/SubscriptionTester.cs b/src/Testing/CoreTests/Runtime/SubscriptionTester.cs index 114513fb9..ad2383241 100644 --- a/src/Testing/CoreTests/Runtime/SubscriptionTester.cs +++ b/src/Testing/CoreTests/Runtime/SubscriptionTester.cs @@ -45,6 +45,17 @@ public void description_of_type_name_rule() rule.ToString().ShouldBe("Message name is 'CoreTests.Runtime.RandomClass'"); } + [Fact] + public void description_of_attribute_rule() + { + var rule = new Subscription + { + BaseOrAttributeType = typeof(RandomClassAttribute), + Scope = RoutingScope.Attribute + }; + rule.ToString().ShouldBe("Message type is decorated with 'CoreTests.Runtime.RandomClassAttribute' or a derived type"); + } + [Fact] public void description_of_all_types() { @@ -85,6 +96,20 @@ public void positive_assembly_test() rule.Matches(typeof(DeleteUser)).ShouldBeTrue(); } + [Fact] + public void negative_attribute_test() + { + var rule = new Subscription + { + Scope = RoutingScope.Attribute, + BaseOrAttributeType = typeof(GreenAttribute) + }; + + rule.Matches(typeof(GreenMessage1)).ShouldBeFalse(); + rule.Matches(typeof(GreenMessage2)).ShouldBeFalse(); + rule.Matches(typeof(GreenMessage3)).ShouldBeFalse(); + } + [Fact] public void positive_namespace_test() { @@ -98,6 +123,23 @@ public void positive_namespace_test() rule.Matches(typeof(RedMessage2)).ShouldBeTrue(); rule.Matches(typeof(RedMessage3)).ShouldBeTrue(); } + + [Fact] + public void positive_attribute_test() + { + var rule = new Subscription + { + Scope = RoutingScope.Attribute, + BaseOrAttributeType = typeof(RedAttribute) + }; + + rule.Matches(typeof(RedMessage1)).ShouldBeTrue(); + rule.Matches(typeof(RedMessage2)).ShouldBeTrue(); + rule.Matches(typeof(RedMessage3)).ShouldBeTrue(); + } } -public class RandomClass; \ No newline at end of file +[RandomClass] +public class RandomClass; + +public class RandomClassAttribute : Attribute; \ No newline at end of file diff --git a/src/Wolverine/Configuration/PublishingExpression.cs b/src/Wolverine/Configuration/PublishingExpression.cs index 84f971731..62a5598f3 100644 --- a/src/Wolverine/Configuration/PublishingExpression.cs +++ b/src/Wolverine/Configuration/PublishingExpression.cs @@ -160,6 +160,35 @@ public PublishingExpression MessagesFromAssemblyContaining() return MessagesFromAssembly(typeof(T).Assembly); } + /// + /// Create a publishing rule for all messages decorated with the specified attribute type or a derived type + /// + /// + /// + public PublishingExpression MessagesDecoratedWith(Type attributeType) + { + AutoAddSubscriptions = true; + + _subscriptions.Add(new Subscription() + { + Scope = RoutingScope.Attribute, + BaseOrAttributeType = attributeType, + }); + + return this; + } + + /// + /// Create a publishing rule for all messages decorated with the attribute of type T or a derived type + /// + /// + /// + public PublishingExpression MessagesDecoratedWith() + where T : Attribute + { + return MessagesDecoratedWith(typeof(T)); + } + internal void AttachSubscriptions() { if (!_endpoints.Any()) @@ -182,6 +211,6 @@ internal void AddSubscriptionForAllMessages() /// public void MessagesImplementing() { - _subscriptions.Add(new Subscription { BaseType = typeof(T), Scope = RoutingScope.Implements }); + _subscriptions.Add(new Subscription { BaseOrAttributeType = typeof(T), Scope = RoutingScope.Implements }); } } \ No newline at end of file diff --git a/src/Wolverine/Runtime/Routing/RoutingScope.cs b/src/Wolverine/Runtime/Routing/RoutingScope.cs index 6edf130f2..bad655071 100644 --- a/src/Wolverine/Runtime/Routing/RoutingScope.cs +++ b/src/Wolverine/Runtime/Routing/RoutingScope.cs @@ -7,5 +7,6 @@ public enum RoutingScope Type, TypeName, All, - Implements + Implements, + Attribute } \ No newline at end of file diff --git a/src/Wolverine/Runtime/Routing/Subscription.cs b/src/Wolverine/Runtime/Routing/Subscription.cs index a2e97e603..5002a1f19 100644 --- a/src/Wolverine/Runtime/Routing/Subscription.cs +++ b/src/Wolverine/Runtime/Routing/Subscription.cs @@ -43,7 +43,10 @@ public string[] ContentTypes /// public string Match { get; init; } = string.Empty; - public Type? BaseType { get; init; } + /// + /// A base type if matching on implementation or an attribute type if matching on decoration + /// + public Type? BaseOrAttributeType { get; init; } /// /// Create a subscription for a specific message type @@ -80,7 +83,8 @@ public bool Matches(Type type) RoutingScope.Type => type.Name.EqualsIgnoreCase(Match) || type.FullName!.EqualsIgnoreCase(Match) || type.ToMessageTypeName().EqualsIgnoreCase(Match), RoutingScope.TypeName => type.ToMessageTypeName().EqualsIgnoreCase(Match), - RoutingScope.Implements => type.CanBeCastTo(BaseType!), + RoutingScope.Implements => type.CanBeCastTo(BaseOrAttributeType!), + RoutingScope.Attribute => type.IsDefined(BaseOrAttributeType!, inherit: false), _ => !type.CanBeCastTo() }; } @@ -140,6 +144,9 @@ public override string ToString() case RoutingScope.TypeName: return $"Message name is '{Match}'"; + + case RoutingScope.Attribute: + return $"Message type is decorated with '{BaseOrAttributeType?.FullName}' or a derived type"; } throw new ArgumentOutOfRangeException(); diff --git a/src/Wolverine/Transports/Local/LocalTransport.cs b/src/Wolverine/Transports/Local/LocalTransport.cs index d09392a6b..e20560ce4 100644 --- a/src/Wolverine/Transports/Local/LocalTransport.cs +++ b/src/Wolverine/Transports/Local/LocalTransport.cs @@ -36,7 +36,7 @@ public LocalTransport() : base(TransportConstants.Local, "Local (In Memory)") var agentQueue = _queues[TransportConstants.Agents]; agentQueue.TelemetryEnabled = false; agentQueue.Subscriptions.Add(new Subscription - { Scope = RoutingScope.Implements, BaseType = typeof(IAgentCommand) }); + { Scope = RoutingScope.Implements, BaseOrAttributeType = typeof(IAgentCommand) }); agentQueue.ExecutionOptions.MaxDegreeOfParallelism = 20; agentQueue.Role = EndpointRole.System; agentQueue.Mode = EndpointMode.BufferedInMemory;