Browse Source

partial topic attributes (#617)

* Added support for defining topic attribute partials

* Updated readme

* Small improvements to partial topic implementation.

Co-authored-by: Pascal Slegtenhorst <pslegtenhorst@inforit.nl>
master
Pascal Slegtenhorst 4 years ago
committed by GitHub
parent
commit
0d603a7bde
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 104 additions and 35 deletions
  1. +15
    -0
      README.md
  2. +1
    -1
      src/DotNetCore.CAP.Dashboard/Pages/SubscriberPage.cshtml
  3. +1
    -1
      src/DotNetCore.CAP.Dashboard/Pages/SubscriberPage.generated.cs
  4. +7
    -7
      src/DotNetCore.CAP/CAP.Attribute.cs
  5. +26
    -0
      src/DotNetCore.CAP/Internal/ConsumerExecutorDescriptor.cs
  6. +1
    -1
      src/DotNetCore.CAP/Internal/IConsumerRegister.Default.cs
  7. +21
    -19
      src/DotNetCore.CAP/Internal/IConsumerServiceSelector.Default.cs
  8. +9
    -1
      src/DotNetCore.CAP/Internal/TopicAttribute.cs
  9. +23
    -5
      test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs

+ 15
- 0
README.md View File

@@ -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



+ 1
- 1
src/DotNetCore.CAP.Dashboard/Pages/SubscriberPage.cshtml View File

@@ -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">


+ 1
- 1
src/DotNetCore.CAP.Dashboard/Pages/SubscriberPage.generated.cs View File

@@ -200,7 +200,7 @@ WriteLiteral(" <td>");

#line 48 "..\..\Pages\SubscriberPage.cshtml"
Write(column.Attribute.Name);
Write(column.TopicName);

#line default


+ 7
- 7
src/DotNetCore.CAP/CAP.Attribute.cs View File

@@ -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;


+ 26
- 0
src/DotNetCore.CAP/Internal/ConsumerExecutorDescriptor.cs View File

@@ -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


+ 1
- 1
src/DotNetCore.CAP/Internal/IConsumerRegister.Default.cs View File

@@ -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);
}


+ 21
- 19
src/DotNetCore.CAP/Internal/IConsumerServiceSelector.Default.cs View File

@@ -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);


+ 9
- 1
src/DotNetCore.CAP/Internal/TopicAttribute.cs View File

@@ -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


+ 23
- 5
test/DotNetCore.CAP.Test/ConsumerServiceSelectorTest.cs View File

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


Loading…
Cancel
Save