diff --git a/src/Saunter/Attributes/AsyncApiAttribute.cs b/src/Saunter/Attributes/AsyncApiAttribute.cs
index a886966..b92b3ba 100644
--- a/src/Saunter/Attributes/AsyncApiAttribute.cs
+++ b/src/Saunter/Attributes/AsyncApiAttribute.cs
@@ -3,9 +3,9 @@
namespace Saunter.Attributes
{
///
- /// Marks a class as containing asyncapi channels.
+ /// Marks a class or interface as containing asyncapi channels.
///
- [AttributeUsage(AttributeTargets.Class)]
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
public sealed class AsyncApiAttribute : Attribute
{
public string DocumentName { get; }
diff --git a/src/Saunter/Attributes/ChannelAttribute.cs b/src/Saunter/Attributes/ChannelAttribute.cs
index b7ea022..de8939d 100644
--- a/src/Saunter/Attributes/ChannelAttribute.cs
+++ b/src/Saunter/Attributes/ChannelAttribute.cs
@@ -2,7 +2,7 @@
namespace Saunter.Attributes
{
- [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface)]
public class ChannelAttribute : Attribute
{
///
diff --git a/src/Saunter/Attributes/ChannelParameterAttribute.cs b/src/Saunter/Attributes/ChannelParameterAttribute.cs
index d4301da..aab0c95 100644
--- a/src/Saunter/Attributes/ChannelParameterAttribute.cs
+++ b/src/Saunter/Attributes/ChannelParameterAttribute.cs
@@ -2,7 +2,7 @@
namespace Saunter.Attributes
{
- [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)]
public class ChannelParameterAttribute : Attribute
{
public ChannelParameterAttribute(string name, Type type)
diff --git a/src/Saunter/Attributes/OperationAttribute.cs b/src/Saunter/Attributes/OperationAttribute.cs
index 99315b4..77f2662 100644
--- a/src/Saunter/Attributes/OperationAttribute.cs
+++ b/src/Saunter/Attributes/OperationAttribute.cs
@@ -2,7 +2,7 @@
namespace Saunter.Attributes
{
- [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
+ [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Interface)]
public abstract class OperationAttribute : Attribute
{
public OperationType OperationType { get; protected set; }
diff --git a/test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs b/test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs
index 2e59b36..8fd8d2a 100644
--- a/test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs
+++ b/test/Saunter.Tests/Generation/DocumentGeneratorTests/ClassAttributesTests.cs
@@ -12,15 +12,17 @@ namespace Saunter.Tests.Generation.DocumentGeneratorTests
{
public class ClassAttributesTests
{
- [Fact]
- public void GetDocument_GeneratesDocumentWithMultipleMessagesPerChannel()
+ [Theory]
+ [InlineData(typeof(TenantMessageConsumer))]
+ [InlineData(typeof(ITenantMessageConsumer))]
+ public void GetDocument_GeneratesDocumentWithMultipleMessagesPerChannel(Type type)
{
// Arrange
var options = new AsyncApiOptions();
var documentGenerator = new DocumentGenerator();
// Act
- var document = documentGenerator.GenerateDocument(new[] { typeof(TenantMessageConsumer).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
// Assert
document.ShouldNotBeNull();
@@ -44,15 +46,17 @@ public void GetDocument_GeneratesDocumentWithMultipleMessagesPerChannel()
}
- [Fact]
- public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannelInTheSameMethod()
+ [Theory]
+ [InlineData(typeof(TenantGenericMessagePublisher))]
+ [InlineData(typeof(ITenantGenericMessagePublisher))]
+ public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannelInTheSameMethod(Type type)
{
// Arrange
var options = new AsyncApiOptions();
var documentGenerator = new DocumentGenerator();
// Act
- var document = documentGenerator.GenerateDocument(new[] { typeof(TenantGenericMessagePublisher).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
// Assert
document.ShouldNotBeNull();
@@ -76,15 +80,17 @@ public void GenerateDocument_GeneratesDocumentWithMultipleMessagesPerChannelInTh
}
- [Fact]
- public void GenerateDocument_GeneratesDocumentWithSingleMessage()
+ [Theory]
+ [InlineData(typeof(TenantSingleMessagePublisher))]
+ [InlineData(typeof(ITenantSingleMessagePublisher))]
+ public void GenerateDocument_GeneratesDocumentWithSingleMessage(Type type)
{
// Arrange
var options = new AsyncApiOptions();
var documentGenerator = new DocumentGenerator();
// Act
- var document = documentGenerator.GenerateDocument(new[] { typeof(TenantSingleMessagePublisher).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
// Assert
document.ShouldNotBeNull();
@@ -104,8 +110,10 @@ public void GenerateDocument_GeneratesDocumentWithSingleMessage()
}
- [Fact]
- public void GetDocument_WhenMultipleClassesUseSameChannelKey_GeneratesDocumentWithMultipleMessagesPerChannel()
+ [Theory]
+ [InlineData(typeof(TenantMessageConsumer), typeof(TenantMessagePublisher))]
+ [InlineData(typeof(ITenantMessageConsumer), typeof(ITenantMessagePublisher))]
+ public void GetDocument_WhenMultipleClassesUseSameChannelKey_GeneratesDocumentWithMultipleMessagesPerChannel(Type type1, Type type2)
{
// Arrange
var options = new AsyncApiOptions();
@@ -114,8 +122,8 @@ public void GetDocument_WhenMultipleClassesUseSameChannelKey_GeneratesDocumentWi
// Act
var document = documentGenerator.GenerateDocument(new[]
{
- typeof(TenantMessageConsumer).GetTypeInfo(),
- typeof(TenantMessagePublisher).GetTypeInfo()
+ type1.GetTypeInfo(),
+ type2.GetTypeInfo()
}, options, options.AsyncApi, ActivatorServiceProvider.Instance);
// Assert
@@ -153,15 +161,17 @@ public void GetDocument_WhenMultipleClassesUseSameChannelKey_GeneratesDocumentWi
}
- [Fact]
- public void GenerateDocument_GeneratesDocumentWithChannelParameters()
+ [Theory]
+ [InlineData(typeof(OneTenantMessageConsumer))]
+ [InlineData(typeof(IOneTenantMessageConsumer))]
+ public void GenerateDocument_GeneratesDocumentWithChannelParameters(Type type)
{
// Arrange
var options = new AsyncApiOptions();
var documentGenerator = new DocumentGenerator();
// Act
- var document = documentGenerator.GenerateDocument(new[] { typeof(OneTenantMessageConsumer).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
// Assert
document.ShouldNotBeNull();
@@ -188,15 +198,17 @@ public void GenerateDocument_GeneratesDocumentWithChannelParameters()
}
- [Fact]
- public void GenerateDocument_GeneratesDocumentWithMessageHeader()
+ [Theory]
+ [InlineData(typeof(MyMessagePublisher))]
+ [InlineData(typeof(IMyMessagePublisher))]
+ public void GenerateDocument_GeneratesDocumentWithMessageHeader(Type type)
{
// Arrange
var options = new AsyncApiOptions();
var documentGenerator = new DocumentGenerator();
// Act
- var document = documentGenerator.GenerateDocument(new[] { typeof(MyMessagePublisher).GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
// Assert
document.ShouldNotBeNull();
@@ -216,6 +228,15 @@ public class MyMessagePublisher
public void PublishMyMessage() { }
}
+ [AsyncApi]
+ [Channel("channel.my.message")]
+ [PublishOperation]
+ public interface IMyMessagePublisher
+ {
+ [Message(typeof(MyMessage), HeadersType = typeof(MyMessageHeader))]
+ void PublishMyMessage();
+ }
+
[AsyncApi]
[Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
[SubscribeOperation(OperationId = "TenantMessageConsumer", Summary = "Subscribe to domains events about tenants.")]
@@ -231,6 +252,21 @@ public void SubscribeTenantUpdatedEvent(Guid _, TenantUpdated __) { }
public void SubscribeTenantRemovedEvent(Guid _, TenantRemoved __) { }
}
+ [AsyncApi]
+ [Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
+ [SubscribeOperation(OperationId = "TenantMessageConsumer", Summary = "Subscribe to domains events about tenants.")]
+ public interface ITenantMessageConsumer
+ {
+ [Message(typeof(TenantCreated))]
+ void SubscribeTenantCreatedEvent(Guid _, TenantCreated __);
+
+ [Message(typeof(TenantUpdated))]
+ void SubscribeTenantUpdatedEvent(Guid _, TenantUpdated __);
+
+ [Message(typeof(TenantRemoved))]
+ void SubscribeTenantRemovedEvent(Guid _, TenantRemoved __);
+ }
+
[AsyncApi]
[Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
[PublishOperation(OperationId = "TenantMessagePublisher", Summary = "Publish domains events about tenants.")]
@@ -246,6 +282,21 @@ public void PublishTenantUpdatedEvent(Guid _, TenantUpdated __) { }
public void PublishTenantRemovedEvent(Guid _, TenantRemoved __) { }
}
+ [AsyncApi]
+ [Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
+ [PublishOperation(OperationId = "TenantMessagePublisher", Summary = "Publish domains events about tenants.")]
+ public interface ITenantMessagePublisher
+ {
+ [Message(typeof(TenantCreated))]
+ void PublishTenantCreatedEvent(Guid _, TenantCreated __);
+
+ [Message(typeof(TenantUpdated))]
+ void PublishTenantUpdatedEvent(Guid _, TenantUpdated __);
+
+ [Message(typeof(TenantRemoved))]
+ void PublishTenantRemovedEvent(Guid _, TenantRemoved __);
+ }
+
[AsyncApi]
[Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
[PublishOperation(OperationId = "TenantMessagePublisher", Summary = "Publish domains events about tenants.")]
@@ -260,6 +311,18 @@ public void PublishTenantEvent(Guid _, TEvent __)
}
}
+ [AsyncApi]
+ [Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
+ [PublishOperation(OperationId = "TenantMessagePublisher", Summary = "Publish domains events about tenants.")]
+ public interface ITenantGenericMessagePublisher
+ {
+ [Message(typeof(AnyTenantCreated))]
+ [Message(typeof(AnyTenantUpdated))]
+ [Message(typeof(AnyTenantRemoved))]
+ void PublishTenantEvent(Guid _, TEvent __)
+ where TEvent : IEvent;
+ }
+
[AsyncApi]
[Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
[PublishOperation(OperationId = "TenantSingleMessagePublisher", Summary = "Publish single domain event about tenants.")]
@@ -271,6 +334,15 @@ public void PublishTenantCreated(Guid _, AnyTenantCreated __)
}
}
+ [AsyncApi]
+ [Channel("asw.tenant_service.tenants_history", Description = "Tenant events.")]
+ [PublishOperation(OperationId = "TenantSingleMessagePublisher", Summary = "Publish single domain event about tenants.")]
+ public interface ITenantSingleMessagePublisher
+ {
+ [Message(typeof(AnyTenantCreated))]
+ public void PublishTenantCreated(Guid _, AnyTenantCreated __);
+ }
+
[AsyncApi]
[Channel("asw.tenant_service.{tenant_id}.{tenant_status}", Description = "A tenant events.")]
[ChannelParameter("tenant_id", typeof(long), Description = "The tenant identifier.")]
@@ -287,6 +359,23 @@ public void SubscribeTenantUpdatedEvent(Guid _, TenantUpdated __) { }
[Message(typeof(TenantRemoved))]
public void SubscribeTenantRemovedEvent(Guid _, TenantRemoved __) { }
}
+
+ [AsyncApi]
+ [Channel("asw.tenant_service.{tenant_id}.{tenant_status}", Description = "A tenant events.")]
+ [ChannelParameter("tenant_id", typeof(long), Description = "The tenant identifier.")]
+ [ChannelParameter("tenant_status", typeof(string), Description = "The tenant status.")]
+ [SubscribeOperation(OperationId = "OneTenantMessageConsumer", Summary = "Subscribe to domains events about a tenant.")]
+ public interface IOneTenantMessageConsumer
+ {
+ [Message(typeof(TenantCreated))]
+ void SubscribeTenantCreatedEvent(Guid _, TenantCreated __);
+
+ [Message(typeof(TenantUpdated))]
+ void SubscribeTenantUpdatedEvent(Guid _, TenantUpdated __);
+
+ [Message(typeof(TenantRemoved))]
+ void SubscribeTenantRemovedEvent(Guid _, TenantRemoved __);
+ }
}
public class TenantCreated { }
diff --git a/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs b/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs
new file mode 100644
index 0000000..a518389
--- /dev/null
+++ b/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs
@@ -0,0 +1,107 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Reflection;
+using Saunter.AsyncApiSchema.v2;
+using Saunter.Attributes;
+using Saunter.Generation;
+using Shouldly;
+using Xunit;
+using System.Linq;
+
+namespace Saunter.Tests.Generation.DocumentGeneratorTests
+{
+ public class InterfaceAttributeTests
+ {
+ [Theory]
+ [InlineData(typeof(IServiceEvents))]
+ [InlineData(typeof(ServiceEventsFromInterface))]
+ [InlineData(typeof(ServiceEventsFromAnnotatedInterface))] // Check that annotations are not inherited from the interface
+ public void NonAnnotatedTypesTest(Type type)
+ {
+ // Arrange
+ var options = new AsyncApiOptions();
+ var documentGenerator = new DocumentGenerator();
+
+ // Act
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+
+ // Assert
+ document.ShouldNotBeNull();
+ document.Channels.Count.ShouldBe(0);
+ }
+
+ [Theory]
+ [InlineData(typeof(IAnnotatedServiceEvents), "interface")]
+ [InlineData(typeof(AnnotatedServiceEventsFromInterface), "class")]
+ [InlineData(typeof(AnnotatedServiceEventsFromAnnotatedInterface), "class")] // Check that the actual type's annotation takes precedence of the inherited interface
+ public void AnnotatedTypesTest(Type type, string source)
+ {
+ // Arrange
+ var options = new AsyncApiOptions();
+ var documentGenerator = new DocumentGenerator();
+
+ // Act
+ var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance);
+
+ // Assert
+ document.ShouldNotBeNull();
+ document.Channels.Count.ShouldBe(1);
+
+ var channel = document.Channels.First();
+ channel.Key.ShouldBe($"{source}.event");
+ channel.Value.Description.ShouldBeNull();
+
+ var publish = channel.Value.Publish;
+ publish.ShouldNotBeNull();
+ publish.OperationId.ShouldBe("PublishEvent");
+ publish.Description.ShouldBe($"({source}) Subscribe to domains events about a tenant.");
+
+ var messageRef = publish.Message.ShouldBeOfType();
+ messageRef.Id.ShouldBe("tenantEvent");
+ }
+
+ [AsyncApi]
+ private interface IAnnotatedServiceEvents
+ {
+ [Channel("interface.event")]
+ [PublishOperation(typeof(TenantEvent), Description = "(interface) Subscribe to domains events about a tenant.")]
+ void PublishEvent(TenantEvent evt);
+ }
+
+ private interface IServiceEvents
+ {
+ void PublishEvent(TenantEvent evt);
+ }
+
+ private class ServiceEventsFromInterface : IServiceEvents
+ {
+ public void PublishEvent(TenantEvent evt) { }
+ }
+
+ private class ServiceEventsFromAnnotatedInterface : IAnnotatedServiceEvents
+ {
+ public void PublishEvent(TenantEvent evt) { }
+ }
+
+ [AsyncApi]
+ private class AnnotatedServiceEventsFromInterface : IAnnotatedServiceEvents
+ {
+ [Channel("class.event")]
+ [PublishOperation(typeof(TenantEvent), Description = "(class) Subscribe to domains events about a tenant.")]
+ public void PublishEvent(TenantEvent evt) { }
+ }
+
+ [AsyncApi]
+ private class AnnotatedServiceEventsFromAnnotatedInterface : IAnnotatedServiceEvents
+ {
+ [Channel("class.event")]
+ [PublishOperation(typeof(TenantEvent), Description = "(class) Subscribe to domains events about a tenant.")]
+ public void PublishEvent(TenantEvent evt) { }
+ }
+
+ private class TenantEvent { }
+ }
+}