* 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 | |||
@@ -45,7 +45,7 @@ | |||
{ | |||
<td rowspan="@rowCount">@subscriber.Key</td> | |||
} | |||
<td>@column.Attribute.Name</td> | |||
<td>@column.TopicName</td> | |||
<td> | |||
<span style="color: #00bcd4">@column.ImplTypeInfo.Name</span>: | |||
<div class="job-snippet-code"> | |||
@@ -200,7 +200,7 @@ WriteLiteral(" <td>"); | |||
#line 48 "..\..\Pages\SubscriberPage.cshtml" | |||
Write(column.Attribute.Name); | |||
Write(column.TopicName); | |||
#line default | |||
@@ -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; | |||
@@ -20,7 +20,33 @@ namespace DotNetCore.CAP.Internal | |||
public TopicAttribute Attribute { get; set; } | |||
public TopicAttribute ClassAttribute { 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 | |||
@@ -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); | |||
} | |||
@@ -116,17 +116,24 @@ namespace DotNetCore.CAP.Internal | |||
protected IEnumerable<ConsumerExecutorDescriptor> GetTopicAttributesDescription(TypeInfo typeInfo, TypeInfo serviceTypeInfo = null) | |||
{ | |||
var topicClassAttribute = typeInfo.GetCustomAttribute<TopicAttribute>(true); | |||
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; | |||
} | |||
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<ParameterDescriptor> parameters) | |||
IList<ParameterDescriptor> 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<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) | |||
@@ -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<ConsumerExecutorDescriptor> | |||
{ | |||
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<ConsumerExecutorDescriptor> | |||
{ | |||
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); | |||
@@ -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; | |||
} | |||
/// <summary> | |||
@@ -22,6 +23,13 @@ namespace DotNetCore.CAP.Internal | |||
/// </summary> | |||
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> | |||
/// Default group name is CapOptions setting.(Assembly name) | |||
/// kafka --> groups.id | |||
@@ -29,15 +29,18 @@ namespace DotNetCore.CAP.Test | |||
var selector = _provider.GetRequiredService<IConsumerServiceSelector>(); | |||
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 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() | |||