* Added support for defining topic attribute partials * Updated readme * Small improvements to partial topic implementation. Co-authored-by: Pascal Slegtenhorst <pslegtenhorst@inforit.nl>master
@@ -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 | #### Subscribe Group | ||||
@@ -45,7 +45,7 @@ | |||||
{ | { | ||||
<td rowspan="@rowCount">@subscriber.Key</td> | <td rowspan="@rowCount">@subscriber.Key</td> | ||||
} | } | ||||
<td>@column.Attribute.Name</td> | |||||
<td>@column.TopicName</td> | |||||
<td> | <td> | ||||
<span style="color: #00bcd4">@column.ImplTypeInfo.Name</span>: | <span style="color: #00bcd4">@column.ImplTypeInfo.Name</span>: | ||||
<div class="job-snippet-code"> | <div class="job-snippet-code"> | ||||
@@ -200,7 +200,7 @@ WriteLiteral(" <td>"); | |||||
#line 48 "..\..\Pages\SubscriberPage.cshtml" | #line 48 "..\..\Pages\SubscriberPage.cshtml" | ||||
Write(column.Attribute.Name); | |||||
Write(column.TopicName); | |||||
#line default | #line default | ||||
@@ -10,13 +10,13 @@ using DotNetCore.CAP.Internal; | |||||
namespace DotNetCore.CAP | namespace DotNetCore.CAP | ||||
{ | { | ||||
public class CapSubscribeAttribute : TopicAttribute | public class CapSubscribeAttribute : TopicAttribute | ||||
{ | |||||
public CapSubscribeAttribute(string name) | |||||
: base(name) | |||||
{ | |||||
} | |||||
{ | |||||
public CapSubscribeAttribute(string name, bool isPartial = false) | |||||
: base(name, isPartial) | |||||
{ | |||||
} | |||||
public override string ToString() | public override string ToString() | ||||
{ | { | ||||
return Name; | return Name; | ||||
@@ -20,7 +20,33 @@ namespace DotNetCore.CAP.Internal | |||||
public TopicAttribute Attribute { get; set; } | public TopicAttribute Attribute { get; set; } | ||||
public TopicAttribute ClassAttribute { get; set; } | |||||
public IList<ParameterDescriptor> Parameters { get; set; } | public IList<ParameterDescriptor> Parameters { get; set; } | ||||
private string _topicName; | |||||
/// <summary> | |||||
/// Topic name based on both <see cref="Attribute"/> and <see cref="ClassAttribute"/>. | |||||
/// </summary> | |||||
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 | public class ParameterDescriptor | ||||
@@ -79,7 +79,7 @@ namespace DotNetCore.CAP.Internal | |||||
RegisterMessageProcessor(client); | RegisterMessageProcessor(client); | ||||
client.Subscribe(matchGroup.Value.Select(x => x.Attribute.Name)); | |||||
client.Subscribe(matchGroup.Value.Select(x => x.TopicName)); | |||||
client.Listening(_pollingDelay, _cts.Token); | client.Listening(_pollingDelay, _cts.Token); | ||||
} | } | ||||
@@ -116,17 +116,24 @@ namespace DotNetCore.CAP.Internal | |||||
protected IEnumerable<ConsumerExecutorDescriptor> GetTopicAttributesDescription(TypeInfo typeInfo, TypeInfo serviceTypeInfo = null) | protected IEnumerable<ConsumerExecutorDescriptor> GetTopicAttributesDescription(TypeInfo typeInfo, TypeInfo serviceTypeInfo = null) | ||||
{ | { | ||||
var topicClassAttribute = typeInfo.GetCustomAttribute<TopicAttribute>(true); | |||||
foreach (var method in typeInfo.DeclaredMethods) | foreach (var method in typeInfo.DeclaredMethods) | ||||
{ | { | ||||
var topicAttr = method.GetCustomAttributes<TopicAttribute>(true); | |||||
var topicAttributes = topicAttr as IList<TopicAttribute> ?? topicAttr.ToList(); | |||||
var topicMethodAttributes = method.GetCustomAttributes<TopicAttribute>(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; | continue; | ||||
} | } | ||||
foreach (var attr in topicAttributes) | |||||
foreach (var attr in topicMethodAttributes) | |||||
{ | { | ||||
SetSubscribeAttribute(attr); | SetSubscribeAttribute(attr); | ||||
@@ -138,21 +145,14 @@ namespace DotNetCore.CAP.Internal | |||||
IsFromCap = parameter.GetCustomAttributes(typeof(FromCapAttribute)).Any() | IsFromCap = parameter.GetCustomAttributes(typeof(FromCapAttribute)).Any() | ||||
}).ToList(); | }).ToList(); | ||||
yield return InitDescriptor(attr, method, typeInfo, serviceTypeInfo, parameters); | |||||
yield return InitDescriptor(attr, method, typeInfo, serviceTypeInfo, parameters, topicClassAttribute); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
protected virtual void SetSubscribeAttribute(TopicAttribute attribute) | 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( | private static ConsumerExecutorDescriptor InitDescriptor( | ||||
@@ -160,11 +160,13 @@ namespace DotNetCore.CAP.Internal | |||||
MethodInfo methodInfo, | MethodInfo methodInfo, | ||||
TypeInfo implType, | TypeInfo implType, | ||||
TypeInfo serviceTypeInfo, | TypeInfo serviceTypeInfo, | ||||
IList<ParameterDescriptor> parameters) | |||||
IList<ParameterDescriptor> parameters, | |||||
TopicAttribute classAttr = null) | |||||
{ | { | ||||
var descriptor = new ConsumerExecutorDescriptor | var descriptor = new ConsumerExecutorDescriptor | ||||
{ | { | ||||
Attribute = attr, | Attribute = attr, | ||||
ClassAttribute = classAttr, | |||||
MethodInfo = methodInfo, | MethodInfo = methodInfo, | ||||
ImplTypeInfo = implType, | ImplTypeInfo = implType, | ||||
ServiceTypeInfo = serviceTypeInfo, | ServiceTypeInfo = serviceTypeInfo, | ||||
@@ -176,7 +178,7 @@ namespace DotNetCore.CAP.Internal | |||||
private ConsumerExecutorDescriptor MatchUsingName(string key, IReadOnlyList<ConsumerExecutorDescriptor> executeDescriptor) | private ConsumerExecutorDescriptor MatchUsingName(string key, IReadOnlyList<ConsumerExecutorDescriptor> 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<ConsumerExecutorDescriptor> executeDescriptor) | private ConsumerExecutorDescriptor MatchAsteriskUsingRegex(string key, IReadOnlyList<ConsumerExecutorDescriptor> executeDescriptor) | ||||
@@ -184,10 +186,10 @@ namespace DotNetCore.CAP.Internal | |||||
var group = executeDescriptor.First().Attribute.Group; | var group = executeDescriptor.First().Attribute.Group; | ||||
if (!_asteriskList.TryGetValue(group, out var tmpList)) | 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<ConsumerExecutorDescriptor> | .Select(x => new RegexExecuteDescriptor<ConsumerExecutorDescriptor> | ||||
{ | { | ||||
Name = ("^" + x.Attribute.Name + "$").Replace("*", "[0-9_a-zA-Z]+").Replace(".", "\\."), | |||||
Name = ("^" + x.TopicName + "$").Replace("*", "[0-9_a-zA-Z]+").Replace(".", "\\."), | |||||
Descriptor = x | Descriptor = x | ||||
}).ToList(); | }).ToList(); | ||||
_asteriskList.TryAdd(group, tmpList); | _asteriskList.TryAdd(group, tmpList); | ||||
@@ -210,10 +212,10 @@ namespace DotNetCore.CAP.Internal | |||||
if (!_poundList.TryGetValue(group, out var tmpList)) | if (!_poundList.TryGetValue(group, out var tmpList)) | ||||
{ | { | ||||
tmpList = executeDescriptor | tmpList = executeDescriptor | ||||
.Where(x => x.Attribute.Name.IndexOf('#') >= 0) | |||||
.Where(x => x.TopicName.IndexOf('#') >= 0) | |||||
.Select(x => new RegexExecuteDescriptor<ConsumerExecutorDescriptor> | .Select(x => new RegexExecuteDescriptor<ConsumerExecutorDescriptor> | ||||
{ | { | ||||
Name = ("^" + x.Attribute.Name.Replace(".", "\\.") + "$").Replace("#", "[0-9_a-zA-Z\\.]+"), | |||||
Name = ("^" + x.TopicName.Replace(".", "\\.") + "$").Replace("#", "[0-9_a-zA-Z\\.]+"), | |||||
Descriptor = x | Descriptor = x | ||||
}).ToList(); | }).ToList(); | ||||
_poundList.TryAdd(group, tmpList); | _poundList.TryAdd(group, tmpList); | ||||
@@ -12,9 +12,10 @@ namespace DotNetCore.CAP.Internal | |||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] | ||||
public abstract class TopicAttribute : Attribute | public abstract class TopicAttribute : Attribute | ||||
{ | { | ||||
protected TopicAttribute(string name) | |||||
protected TopicAttribute(string name, bool isPartial = false) | |||||
{ | { | ||||
Name = name; | Name = name; | ||||
IsPartial = isPartial; | |||||
} | } | ||||
/// <summary> | /// <summary> | ||||
@@ -22,6 +23,13 @@ namespace DotNetCore.CAP.Internal | |||||
/// </summary> | /// </summary> | ||||
public string Name { get; } | public string Name { get; } | ||||
/// <summary> | |||||
/// 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". | |||||
/// </summary> | |||||
public bool IsPartial { get; } | |||||
/// <summary> | /// <summary> | ||||
/// Default group name is CapOptions setting.(Assembly name) | /// Default group name is CapOptions setting.(Assembly name) | ||||
/// kafka --> groups.id | /// kafka --> groups.id | ||||
@@ -29,15 +29,18 @@ namespace DotNetCore.CAP.Test | |||||
var selector = _provider.GetRequiredService<IConsumerServiceSelector>(); | var selector = _provider.GetRequiredService<IConsumerServiceSelector>(); | ||||
var candidates = selector.SelectCandidates(); | 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<IConsumerServiceSelector>(); | var selector = _provider.GetRequiredService<IConsumerServiceSelector>(); | ||||
var candidates = selector.SelectCandidates(); | var candidates = selector.SelectCandidates(); | ||||
var bestCandidates = selector.SelectBestCandidate("Candidates.Foo", candidates); | |||||
var bestCandidates = selector.SelectBestCandidate(topic, candidates); | |||||
Assert.NotNull(bestCandidates); | Assert.NotNull(bestCandidates); | ||||
Assert.NotNull(bestCandidates.MethodInfo); | Assert.NotNull(bestCandidates.MethodInfo); | ||||
@@ -116,7 +119,7 @@ namespace DotNetCore.CAP.Test | |||||
public class CandidatesTopic : TopicAttribute | 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 | public class CandidatesFooTest : IFooTest, ICapSubscribe | ||||
{ | { | ||||
[CandidatesTopic("Candidates.Foo")] | [CandidatesTopic("Candidates.Foo")] | ||||
@@ -144,6 +148,20 @@ namespace DotNetCore.CAP.Test | |||||
Console.WriteLine("GetFoo2() method has bee excuted."); | 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")] | ||||
[CandidatesTopic("*.Asterisk")] | [CandidatesTopic("*.Asterisk")] | ||||
public void GetFooAsterisk() | public void GetFooAsterisk() | ||||