From 0d603a7bdeb0eb605027430615479155a3cbbf95 Mon Sep 17 00:00:00 2001 From: Pascal Slegtenhorst Date: Thu, 30 Jul 2020 03:18:18 +0200 Subject: [PATCH] partial topic attributes (#617) * Added support for defining topic attribute partials * Updated readme * Small improvements to partial topic implementation. Co-authored-by: Pascal Slegtenhorst --- README.md | 15 +++++++ .../Pages/SubscriberPage.cshtml | 2 +- .../Pages/SubscriberPage.generated.cs | 2 +- src/DotNetCore.CAP/CAP.Attribute.cs | 14 +++---- .../Internal/ConsumerExecutorDescriptor.cs | 26 ++++++++++++ .../Internal/IConsumerRegister.Default.cs | 2 +- .../IConsumerServiceSelector.Default.cs | 40 ++++++++++--------- src/DotNetCore.CAP/Internal/TopicAttribute.cs | 10 ++++- .../ConsumerServiceSelectorTest.cs | 28 ++++++++++--- 9 files changed, 104 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 66586f3..540b68d 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,21 @@ public void ConfigureServices(IServiceCollection services) }); } ``` +#### Use partials for topic subscriptions + +To group topic subscriptions on class level you're able to define a subscription on a method as a partial. Subscriptions on the message queue will then be a combination of the topic defined on the class and the topic defined on the method. In the following example the `Create(..)` function will be invoked when receiving a message on `customers.create` + +```c# +[CapSubscribe("customers")] +public class CustomersSubscriberService : ICapSubscribe +{ + [CapSubscribe("create", isPartial: true)] + public void Create(Customer customer) + { + } +} +``` + #### Subscribe Group diff --git a/src/DotNetCore.CAP.Dashboard/Pages/SubscriberPage.cshtml b/src/DotNetCore.CAP.Dashboard/Pages/SubscriberPage.cshtml index 0b6ba0d..3bd479e 100644 --- a/src/DotNetCore.CAP.Dashboard/Pages/SubscriberPage.cshtml +++ b/src/DotNetCore.CAP.Dashboard/Pages/SubscriberPage.cshtml @@ -45,7 +45,7 @@ { @subscriber.Key } - @column.Attribute.Name + @column.TopicName @column.ImplTypeInfo.Name
diff --git a/src/DotNetCore.CAP.Dashboard/Pages/SubscriberPage.generated.cs b/src/DotNetCore.CAP.Dashboard/Pages/SubscriberPage.generated.cs index b243943..3f05369 100644 --- a/src/DotNetCore.CAP.Dashboard/Pages/SubscriberPage.generated.cs +++ b/src/DotNetCore.CAP.Dashboard/Pages/SubscriberPage.generated.cs @@ -200,7 +200,7 @@ WriteLiteral(" "); #line 48 "..\..\Pages\SubscriberPage.cshtml" - Write(column.Attribute.Name); + Write(column.TopicName); #line default diff --git a/src/DotNetCore.CAP/CAP.Attribute.cs b/src/DotNetCore.CAP/CAP.Attribute.cs index 40092a5..d77df33 100644 --- a/src/DotNetCore.CAP/CAP.Attribute.cs +++ b/src/DotNetCore.CAP/CAP.Attribute.cs @@ -10,13 +10,13 @@ using DotNetCore.CAP.Internal; namespace DotNetCore.CAP { public class CapSubscribeAttribute : TopicAttribute - { - public CapSubscribeAttribute(string name) - : base(name) - { - - } - + { + public CapSubscribeAttribute(string name, bool isPartial = false) + : base(name, isPartial) + { + + } + public override string ToString() { return Name; diff --git a/src/DotNetCore.CAP/Internal/ConsumerExecutorDescriptor.cs b/src/DotNetCore.CAP/Internal/ConsumerExecutorDescriptor.cs index b7a0f8f..7a5cc0a 100644 --- a/src/DotNetCore.CAP/Internal/ConsumerExecutorDescriptor.cs +++ b/src/DotNetCore.CAP/Internal/ConsumerExecutorDescriptor.cs @@ -20,7 +20,33 @@ namespace DotNetCore.CAP.Internal public TopicAttribute Attribute { get; set; } + public TopicAttribute ClassAttribute { get; set; } + public IList Parameters { get; set; } + + private string _topicName; + /// + /// Topic name based on both and . + /// + public string TopicName + { + get + { + if (_topicName == null) + { + if (ClassAttribute != null && Attribute.IsPartial) + { + // Allows class level attribute name to end with a '.' and allows methods level attribute to start with a '.'. + _topicName = $"{ClassAttribute.Name.TrimEnd('.')}.{Attribute.Name.TrimStart('.')}"; + } + else + { + _topicName = Attribute.Name; + } + } + return _topicName; + } + } } public class ParameterDescriptor diff --git a/src/DotNetCore.CAP/Internal/IConsumerRegister.Default.cs b/src/DotNetCore.CAP/Internal/IConsumerRegister.Default.cs index 65cb999..c41c96d 100644 --- a/src/DotNetCore.CAP/Internal/IConsumerRegister.Default.cs +++ b/src/DotNetCore.CAP/Internal/IConsumerRegister.Default.cs @@ -79,7 +79,7 @@ namespace DotNetCore.CAP.Internal RegisterMessageProcessor(client); - client.Subscribe(matchGroup.Value.Select(x => x.Attribute.Name)); + client.Subscribe(matchGroup.Value.Select(x => x.TopicName)); client.Listening(_pollingDelay, _cts.Token); } diff --git a/src/DotNetCore.CAP/Internal/IConsumerServiceSelector.Default.cs b/src/DotNetCore.CAP/Internal/IConsumerServiceSelector.Default.cs index f77ed53..f8e278c 100644 --- a/src/DotNetCore.CAP/Internal/IConsumerServiceSelector.Default.cs +++ b/src/DotNetCore.CAP/Internal/IConsumerServiceSelector.Default.cs @@ -116,17 +116,24 @@ namespace DotNetCore.CAP.Internal protected IEnumerable GetTopicAttributesDescription(TypeInfo typeInfo, TypeInfo serviceTypeInfo = null) { + var topicClassAttribute = typeInfo.GetCustomAttribute(true); + foreach (var method in typeInfo.DeclaredMethods) { - var topicAttr = method.GetCustomAttributes(true); - var topicAttributes = topicAttr as IList ?? topicAttr.ToList(); + var topicMethodAttributes = method.GetCustomAttributes(true); + + // Ignore partial attributes when no topic attribute is defined on class. + if (topicClassAttribute is null) + { + topicMethodAttributes = topicMethodAttributes.Where(x => x.IsPartial); + } - if (!topicAttributes.Any()) + if (!topicMethodAttributes.Any()) { continue; } - foreach (var attr in topicAttributes) + foreach (var attr in topicMethodAttributes) { SetSubscribeAttribute(attr); @@ -138,21 +145,14 @@ namespace DotNetCore.CAP.Internal IsFromCap = parameter.GetCustomAttributes(typeof(FromCapAttribute)).Any() }).ToList(); - yield return InitDescriptor(attr, method, typeInfo, serviceTypeInfo, parameters); + yield return InitDescriptor(attr, method, typeInfo, serviceTypeInfo, parameters, topicClassAttribute); } } } protected virtual void SetSubscribeAttribute(TopicAttribute attribute) { - if (attribute.Group == null) - { - attribute.Group = _capOptions.DefaultGroup + "." + _capOptions.Version; - } - else - { - attribute.Group = attribute.Group + "." + _capOptions.Version; - } + attribute.Group = (attribute.Group ?? _capOptions.DefaultGroup) + "." + _capOptions.Version; } private static ConsumerExecutorDescriptor InitDescriptor( @@ -160,11 +160,13 @@ namespace DotNetCore.CAP.Internal MethodInfo methodInfo, TypeInfo implType, TypeInfo serviceTypeInfo, - IList parameters) + IList parameters, + TopicAttribute classAttr = null) { var descriptor = new ConsumerExecutorDescriptor { Attribute = attr, + ClassAttribute = classAttr, MethodInfo = methodInfo, ImplTypeInfo = implType, ServiceTypeInfo = serviceTypeInfo, @@ -176,7 +178,7 @@ namespace DotNetCore.CAP.Internal private ConsumerExecutorDescriptor MatchUsingName(string key, IReadOnlyList executeDescriptor) { - return executeDescriptor.FirstOrDefault(x => x.Attribute.Name.Equals(key, StringComparison.InvariantCultureIgnoreCase)); + return executeDescriptor.FirstOrDefault(x => x.TopicName.Equals(key, StringComparison.InvariantCultureIgnoreCase)); } private ConsumerExecutorDescriptor MatchAsteriskUsingRegex(string key, IReadOnlyList executeDescriptor) @@ -184,10 +186,10 @@ namespace DotNetCore.CAP.Internal var group = executeDescriptor.First().Attribute.Group; if (!_asteriskList.TryGetValue(group, out var tmpList)) { - tmpList = executeDescriptor.Where(x => x.Attribute.Name.IndexOf('*') >= 0) + tmpList = executeDescriptor.Where(x => x.TopicName.IndexOf('*') >= 0) .Select(x => new RegexExecuteDescriptor { - Name = ("^" + x.Attribute.Name + "$").Replace("*", "[0-9_a-zA-Z]+").Replace(".", "\\."), + Name = ("^" + x.TopicName + "$").Replace("*", "[0-9_a-zA-Z]+").Replace(".", "\\."), Descriptor = x }).ToList(); _asteriskList.TryAdd(group, tmpList); @@ -210,10 +212,10 @@ namespace DotNetCore.CAP.Internal if (!_poundList.TryGetValue(group, out var tmpList)) { tmpList = executeDescriptor - .Where(x => x.Attribute.Name.IndexOf('#') >= 0) + .Where(x => x.TopicName.IndexOf('#') >= 0) .Select(x => new RegexExecuteDescriptor { - Name = ("^" + x.Attribute.Name.Replace(".", "\\.") + "$").Replace("#", "[0-9_a-zA-Z\\.]+"), + Name = ("^" + x.TopicName.Replace(".", "\\.") + "$").Replace("#", "[0-9_a-zA-Z\\.]+"), Descriptor = x }).ToList(); _poundList.TryAdd(group, tmpList); diff --git a/src/DotNetCore.CAP/Internal/TopicAttribute.cs b/src/DotNetCore.CAP/Internal/TopicAttribute.cs index 559cd5f..9c0fca9 100644 --- a/src/DotNetCore.CAP/Internal/TopicAttribute.cs +++ b/src/DotNetCore.CAP/Internal/TopicAttribute.cs @@ -12,9 +12,10 @@ namespace DotNetCore.CAP.Internal [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] public abstract class TopicAttribute : Attribute { - protected TopicAttribute(string name) + protected TopicAttribute(string name, bool isPartial = false) { Name = name; + IsPartial = isPartial; } /// @@ -22,6 +23,13 @@ namespace DotNetCore.CAP.Internal /// public string Name { get; } + /// + /// Defines wether this attribute defines a topic subscription partial. + /// The defined topic will be combined with a topic subscription defined on class level, + /// which results for example in subscription on "class.method". + /// + public bool IsPartial { get; } + /// /// Default group name is CapOptions setting.(Assembly name) /// kafka --> groups.id diff --git a/test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs b/test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs index 59f231d..bb9dcdb 100644 --- a/test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs +++ b/test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs @@ -29,15 +29,18 @@ namespace DotNetCore.CAP.Test var selector = _provider.GetRequiredService(); var candidates = selector.SelectCandidates(); - Assert.Equal(6, candidates.Count); + Assert.Equal(8, candidates.Count); } - [Fact] - public void CanFindSpecifiedTopic() + [Theory] + [InlineData("Candidates.Foo")] + [InlineData("Candidates.Foo3")] + [InlineData("Candidates.Foo4")] + public void CanFindSpecifiedTopic(string topic) { var selector = _provider.GetRequiredService(); var candidates = selector.SelectCandidates(); - var bestCandidates = selector.SelectBestCandidate("Candidates.Foo", candidates); + var bestCandidates = selector.SelectBestCandidate(topic, candidates); Assert.NotNull(bestCandidates); Assert.NotNull(bestCandidates.MethodInfo); @@ -116,7 +119,7 @@ namespace DotNetCore.CAP.Test public class CandidatesTopic : TopicAttribute { - public CandidatesTopic(string topicName) : base(topicName) + public CandidatesTopic(string topicName, bool isPartial = false) : base(topicName, isPartial) { } } @@ -129,6 +132,7 @@ namespace DotNetCore.CAP.Test { } + [CandidatesTopic("Candidates")] public class CandidatesFooTest : IFooTest, ICapSubscribe { [CandidatesTopic("Candidates.Foo")] @@ -144,6 +148,20 @@ namespace DotNetCore.CAP.Test Console.WriteLine("GetFoo2() method has bee excuted."); } + [CandidatesTopic("Foo3", isPartial: true)] + public Task GetFoo3() + { + Console.WriteLine("GetFoo3() method has bee excuted."); + return Task.CompletedTask; + } + + [CandidatesTopic(".Foo4", isPartial: true)] + public Task GetFoo4() + { + Console.WriteLine("GetFoo4() method has bee excuted."); + return Task.CompletedTask; + } + [CandidatesTopic("*.*.Asterisk")] [CandidatesTopic("*.Asterisk")] public void GetFooAsterisk()