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()
| |